ALwrity Version 0.5.0 (Fastapi + React )

This commit is contained in:
ajaysi
2025-08-06 12:48:02 +05:30
parent f28a919caa
commit 32f97fa6b3
476 changed files with 115544 additions and 28747 deletions

237
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Box, CircularProgress, Typography } from '@mui/material';
import Wizard from './components/OnboardingWizard/Wizard';
import MainDashboard from './components/MainDashboard/MainDashboard';
import SEODashboard from './components/SEODashboard/SEODashboard';
import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard';
import { apiClient } from './api/client';
interface OnboardingStatus {
onboarding_required: boolean;
onboarding_complete: boolean;
current_step?: number;
total_steps?: number;
completion_percentage?: number;
}
const App: React.FC = () => {
const [loading, setLoading] = useState(true);
const [onboardingStatus, setOnboardingStatus] = useState<OnboardingStatus | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
checkOnboardingStatus();
}, []);
const checkOnboardingStatus = async () => {
try {
setLoading(true);
// Use the correct endpoint that exists in our backend
const response = await apiClient.get('/api/onboarding/status');
const status: any = response.data;
// Transform the backend response to match frontend expectations
const transformedStatus: OnboardingStatus = {
onboarding_required: !status.is_completed,
onboarding_complete: status.is_completed || false,
current_step: status.current_step,
total_steps: 6, // We know there are 6 steps
completion_percentage: status.completion_percentage
};
setOnboardingStatus(transformedStatus);
} catch (err) {
console.error('Error checking onboarding status:', err);
// If the endpoint doesn't exist, assume onboarding is required
setOnboardingStatus({
onboarding_required: true,
onboarding_complete: false,
current_step: 1,
total_steps: 6,
completion_percentage: 0
});
} finally {
setLoading(false);
}
};
const handleOnboardingComplete = async () => {
// Refresh onboarding status after completion
await checkOnboardingStatus();
};
if (loading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
flexDirection="column"
>
<CircularProgress size={60} />
<Typography variant="h6" sx={{ mt: 2 }}>
Loading Alwrity...
</Typography>
</Box>
);
}
if (error) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
flexDirection="column"
>
<Typography variant="h6" color="error">
{error}
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Please refresh the page to try again.
</Typography>
</Box>
);
}
return (
<Router>
<Routes>
{/* Dashboard Route */}
<Route
path="/dashboard"
element={
<DashboardWrapper />
}
/>
{/* SEO Dashboard Route */}
<Route
path="/seo-dashboard"
element={
<SEODashboard />
}
/>
{/* Content Planning Dashboard Route */}
<Route
path="/content-planning"
element={
<ContentPlanningDashboard />
}
/>
{/* Root Route - Show onboarding or redirect to dashboard */}
<Route
path="/"
element={
onboardingStatus?.onboarding_required ? (
<Wizard onComplete={handleOnboardingComplete} />
) : (
<Navigate to="/dashboard" replace />
)
}
/>
{/* Catch all other routes */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>
);
};
// Separate component to handle dashboard logic
const DashboardWrapper: React.FC = () => {
const [dashboardLoading, setDashboardLoading] = useState(true);
const [onboardingComplete, setOnboardingComplete] = useState(false);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
const checkDashboardAccess = async () => {
try {
console.log('DashboardWrapper: Checking dashboard access...');
// Check if onboarding is complete
const response = await apiClient.get('/api/onboarding/status');
const status = response.data;
console.log('DashboardWrapper: Backend status:', status);
console.log('DashboardWrapper: is_completed:', status.is_completed);
console.log('DashboardWrapper: current_step:', status.current_step);
if (status.is_completed) {
console.log('DashboardWrapper: Onboarding is complete, showing dashboard');
setOnboardingComplete(true);
} else {
console.log('DashboardWrapper: Onboarding not complete, retry count:', retryCount);
// If onboarding is not complete, try a few times with delay
if (retryCount < 3) {
console.log('DashboardWrapper: Retrying in 1 second...');
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, 1000);
return;
} else {
console.log('DashboardWrapper: Max retries reached, redirecting to root');
// If onboarding is not complete after retries, redirect to root
window.location.href = '/';
return;
}
}
} catch (error) {
console.error('DashboardWrapper: Error checking dashboard access:', error);
// If there's an error, try a few times before redirecting
if (retryCount < 3) {
console.log('DashboardWrapper: Error occurred, retrying in 1 second...');
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, 1000);
return;
} else {
console.log('DashboardWrapper: Max retries reached after error, redirecting to root');
// If there's an error after retries, redirect to root
window.location.href = '/';
return;
}
} finally {
setDashboardLoading(false);
}
};
checkDashboardAccess();
}, [retryCount]);
if (dashboardLoading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
flexDirection="column"
>
<CircularProgress size={60} />
<Typography variant="h6" sx={{ mt: 2 }}>
Loading Dashboard...
</Typography>
{retryCount > 0 && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Checking onboarding status... (Attempt {retryCount + 1}/3)
</Typography>
)}
</Box>
);
}
if (!onboardingComplete) {
return <Navigate to="/" replace />;
}
return <MainDashboard />;
};
export default App;

View File

@@ -0,0 +1,92 @@
import axios from 'axios';
// Create a shared axios instance for all API calls
export const apiClient = axios.create({
baseURL: 'http://localhost:8000',
timeout: 60000, // Increased to 60 seconds for regular API calls
headers: {
'Content-Type': 'application/json',
},
});
// Create a specialized client for AI operations with extended timeout
export const aiApiClient = axios.create({
baseURL: 'http://localhost:8000',
timeout: 180000, // 3 minutes timeout for AI operations (matching 20-25 second responses)
headers: {
'Content-Type': 'application/json',
},
});
// Create a specialized client for long-running operations like SEO analysis
export const longRunningApiClient = axios.create({
baseURL: 'http://localhost:8000',
timeout: 300000, // 5 minutes timeout for SEO analysis
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor for logging (optional)
apiClient.interceptors.request.use(
(config) => {
console.log(`Making ${config.method?.toUpperCase()} request to ${config.url}`);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Add response interceptor for error handling (optional)
apiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error('API Error:', error.response?.status, error.response?.data);
return Promise.reject(error);
}
);
// Add interceptors for AI client
aiApiClient.interceptors.request.use(
(config) => {
console.log(`Making AI ${config.method?.toUpperCase()} request to ${config.url}`);
return config;
},
(error) => {
return Promise.reject(error);
}
);
aiApiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error('AI API Error:', error.response?.status, error.response?.data);
return Promise.reject(error);
}
);
// Add interceptors for long-running client
longRunningApiClient.interceptors.request.use(
(config) => {
console.log(`Making long-running ${config.method?.toUpperCase()} request to ${config.url}`);
return config;
},
(error) => {
return Promise.reject(error);
}
);
longRunningApiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error('Long-running API Error:', error.response?.status, error.response?.data);
return Promise.reject(error);
}
);

View File

@@ -0,0 +1,177 @@
// Component Logic API integration
import { AxiosResponse } from 'axios';
import { apiClient } from './client';
// AI Research Interfaces
export interface UserInfoRequest {
full_name: string;
email: string;
company: string;
role: string;
}
export interface UserInfoResponse {
valid: boolean;
user_info?: any;
errors: string[];
}
export interface ResearchPreferencesRequest {
research_depth: string;
content_types: string[];
auto_research: boolean;
factual_content: boolean;
}
export interface ResearchPreferencesResponse {
valid: boolean;
preferences?: any;
errors: string[];
}
export interface ResearchRequest {
topic: string;
preferences: ResearchPreferencesRequest;
}
export interface ResearchResponse {
success: boolean;
topic: string;
results?: any;
error?: string;
}
// Personalization Interfaces
export interface ContentStyleRequest {
writing_style: string;
tone: string;
content_length: string;
}
export interface ContentStyleResponse {
valid: boolean;
style_config?: any;
errors: string[];
}
export interface BrandVoiceRequest {
personality_traits: string[];
voice_description?: string;
keywords?: string;
}
export interface BrandVoiceResponse {
valid: boolean;
brand_config?: any;
errors: string[];
}
export interface AdvancedSettingsRequest {
seo_optimization: boolean;
readability_level: string;
content_structure: string[];
}
export interface PersonalizationSettingsRequest {
content_style: ContentStyleRequest;
brand_voice: BrandVoiceRequest;
advanced_settings: AdvancedSettingsRequest;
}
export interface PersonalizationSettingsResponse {
valid: boolean;
settings?: any;
errors: string[];
}
// Research Utilities Interfaces
export interface ResearchTopicRequest {
topic: string;
api_keys: Record<string, string>;
}
export interface ResearchResultResponse {
success: boolean;
topic: string;
data?: any;
error?: string;
metadata?: any;
}
// AI Research API Functions
export async function validateUserInfo(request: UserInfoRequest): Promise<UserInfoResponse> {
const res: AxiosResponse<UserInfoResponse> = await apiClient.post('/api/onboarding/ai-research/validate-user', request);
return res.data;
}
export async function configureResearchPreferences(request: ResearchPreferencesRequest): Promise<ResearchPreferencesResponse> {
const res: AxiosResponse<ResearchPreferencesResponse> = await apiClient.post('/api/onboarding/ai-research/configure-preferences', request);
return res.data;
}
export async function processResearchRequest(request: ResearchRequest): Promise<ResearchResponse> {
const res: AxiosResponse<ResearchResponse> = await apiClient.post('/api/onboarding/ai-research/process-research', request);
return res.data;
}
export async function getResearchConfigurationOptions(): Promise<any> {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/ai-research/configuration-options');
return res.data;
}
export async function getResearchPreferences(): Promise<ResearchPreferencesResponse> {
const res: AxiosResponse<ResearchPreferencesResponse> = await apiClient.get('/api/onboarding/ai-research/preferences');
return res.data;
}
// Personalization API Functions
export async function validateContentStyle(request: ContentStyleRequest): Promise<ContentStyleResponse> {
const res: AxiosResponse<ContentStyleResponse> = await apiClient.post('/api/onboarding/personalization/validate-style', request);
return res.data;
}
export async function configureBrandVoice(request: BrandVoiceRequest): Promise<BrandVoiceResponse> {
const res: AxiosResponse<BrandVoiceResponse> = await apiClient.post('/api/onboarding/personalization/configure-brand', request);
return res.data;
}
export async function processPersonalizationSettings(request: PersonalizationSettingsRequest): Promise<PersonalizationSettingsResponse> {
const res: AxiosResponse<PersonalizationSettingsResponse> = await apiClient.post('/api/onboarding/personalization/process-settings', request);
return res.data;
}
export async function getPersonalizationConfigurationOptions(): Promise<any> {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/personalization/configuration-options');
return res.data;
}
export async function generateContentGuidelines(settings: any): Promise<any> {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/personalization/generate-guidelines', settings);
return res.data;
}
// Research Utilities API Functions
export async function processResearchTopic(request: ResearchTopicRequest): Promise<ResearchResultResponse> {
const res: AxiosResponse<ResearchResultResponse> = await apiClient.post('/api/onboarding/research/process-topic', request);
return res.data;
}
export async function processResearchResults(results: any): Promise<any> {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/research/process-results', results);
return res.data;
}
export async function validateResearchRequest(topic: string, api_keys: Record<string, string>): Promise<any> {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/research/validate-request', { topic, api_keys });
return res.data;
}
export async function getResearchProvidersInfo(): Promise<any> {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/research/providers-info');
return res.data;
}
export async function generateResearchReport(results: any): Promise<any> {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/research/generate-report', results);
return res.data;
}

View File

@@ -0,0 +1,163 @@
// Make sure to install axios: npm install axios
import { AxiosResponse } from 'axios';
import { apiClient } from './client';
export interface APIKeyRequest {
provider: string;
api_key: string;
description?: string;
}
export interface APIKeyResponse {
provider: string;
api_key: string;
description?: string;
}
export interface OnboardingStepResponse {
step: number;
data?: any;
validation_errors?: string[];
}
export interface OnboardingSessionResponse {
id: number;
user_id: number;
current_step: number;
progress: number;
}
export interface OnboardingProgressResponse {
progress: number;
current_step: number;
total_steps: number;
completion_percentage: number;
}
export async function startOnboarding() {
const res: AxiosResponse<OnboardingSessionResponse> = await apiClient.post('/api/onboarding/start');
return res.data;
}
export async function getCurrentStep() {
// Get the current step from the onboarding status
console.log('getCurrentStep: Calling /api/onboarding/status');
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/status');
console.log('getCurrentStep: Backend returned:', res.data);
return { step: res.data.current_step || 1 };
}
export async function setCurrentStep(step: number) {
// Complete the current step to move to the next one
console.log('setCurrentStep: Completing step', step);
const res: AxiosResponse<OnboardingStepResponse> = await apiClient.post(`/api/onboarding/step/${step}/complete`, {
data: {},
validation_errors: []
});
console.log('setCurrentStep: Backend response:', res.data);
return { step };
}
export async function getApiKeys() {
const maxRetries = 3;
let lastError: any;
console.log('getApiKeys: Starting API call to /api/onboarding/api-keys');
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
console.log(`getApiKeys: Attempt ${attempt + 1}/${maxRetries}`);
const res: AxiosResponse<Record<string, string>> = await apiClient.get('/api/onboarding/api-keys');
console.log('getApiKeys: API call successful');
return res.data;
} catch (error: any) {
lastError = error;
console.log(`getApiKeys: Attempt ${attempt + 1} failed:`, error.response?.status, error.message);
// If it's a rate limit error (429), wait and retry
if (error.response?.status === 429) {
const retryAfter = error.response?.data?.retry_after || 60;
const delay = Math.min(retryAfter * 1000, 5000); // Max 5 seconds
console.log(`getApiKeys: Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// For other errors, don't retry
console.log('getApiKeys: Non-rate-limit error, not retrying');
throw error;
}
}
// If we've exhausted all retries, throw the last error
console.log('getApiKeys: All retries exhausted');
throw lastError;
}
export async function saveApiKey(provider: string, api_key: string, description?: string) {
const res: AxiosResponse<APIKeyResponse> = await apiClient.post('/api/onboarding/api-keys', {
provider,
api_key,
description
});
return res.data;
}
export async function getProgress() {
const res: AxiosResponse<OnboardingProgressResponse> = await apiClient.get('/api/onboarding/progress');
return { progress: res.data.completion_percentage || 0 };
}
export async function setProgress(progress: number) {
// Progress is managed automatically by the backend
// This function is kept for compatibility but doesn't make a backend call
return { progress };
}
// Additional functions for better integration
export async function getOnboardingConfig() {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/config');
return res.data;
}
export async function getStepData(stepNumber: number) {
const res: AxiosResponse<any> = await apiClient.get(`/api/onboarding/step/${stepNumber}`);
return res.data;
}
export async function skipStep(stepNumber: number) {
const res: AxiosResponse<any> = await apiClient.post(`/api/onboarding/step/${stepNumber}/skip`);
return res.data;
}
export async function validateApiKeys() {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/api-keys/validate');
return res.data;
}
export async function completeOnboarding() {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/complete');
return res.data;
}
export async function resetOnboarding() {
const res: AxiosResponse<any> = await apiClient.post('/api/onboarding/reset');
return res.data;
}
// New functions for FinalStep data loading
export async function getOnboardingSummary() {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/summary');
return res.data;
}
export async function getWebsiteAnalysisData() {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/website-analysis');
return res.data;
}
export async function getResearchPreferencesData() {
const res: AxiosResponse<any> = await apiClient.get('/api/onboarding/research-preferences');
return res.data;
}

View File

@@ -0,0 +1,85 @@
import { longRunningApiClient } from './client';
import { SEOAnalysisData } from '../components/shared/types';
// SEO Analysis API functions
export const seoAnalysisAPI = {
async analyzeURL(url: string, targetKeywords?: string[]): Promise<SEOAnalysisData | null> {
try {
console.log(`Starting SEO analysis for URL: ${url}`);
console.log(`Target keywords:`, targetKeywords);
const requestData = {
url,
target_keywords: targetKeywords
};
console.log('Request data:', requestData);
const response = await longRunningApiClient.post('/api/seo-dashboard/analyze-comprehensive', requestData);
console.log('Response received:', response);
console.log('Response data:', response.data);
if (response.data.success) {
console.log(`SEO analysis completed for ${url}`);
console.log('Analysis result:', response.data);
return response.data;
} else {
console.error('Analysis failed:', response.data.message);
throw new Error(response.data.message || 'Analysis failed');
}
} catch (error: any) {
console.error('Error analyzing URL:', error);
console.error('Error details:', {
message: error.message,
status: error.response?.status,
data: error.response?.data
});
throw error;
}
},
async getDetailedMetrics(url: string): Promise<any> {
try {
console.log(`Getting detailed metrics for URL: ${url}`);
const response = await longRunningApiClient.get(`/api/seo-dashboard/metrics/${encodeURIComponent(url)}`);
console.log(`Detailed metrics retrieved for ${url}`);
return response.data;
} catch (error) {
console.error('Error getting detailed metrics:', error);
throw error;
}
},
async getAnalysisSummary(): Promise<any> {
try {
console.log('Getting analysis summary');
const response = await longRunningApiClient.get('/api/seo-dashboard/summary');
console.log('Analysis summary retrieved');
return response.data;
} catch (error) {
console.error('Error getting analysis summary:', error);
throw error;
}
},
async batchAnalyzeURLs(urls: string[]): Promise<any[]> {
try {
console.log(`Starting batch analysis for ${urls.length} URLs`);
const response = await longRunningApiClient.post('/api/seo-dashboard/batch-analyze', { urls });
console.log(`Batch analysis completed for ${urls.length} URLs`);
return response.data;
} catch (error) {
console.error('Error in batch analysis:', error);
throw error;
}
},
async healthCheck(): Promise<boolean> {
try {
const response = await longRunningApiClient.get('/api/seo-dashboard/health');
return response.status === 200;
} catch (error) {
console.error('Health check failed:', error);
return false;
}
}
};

View File

@@ -0,0 +1,112 @@
import { apiClient } from './client';
export interface SEOHealthScore {
score: number;
change: number;
trend: string;
label: string;
color: string;
}
export interface SEOMetric {
value: number;
change: number;
trend: string;
description: string;
color: string;
}
export interface PlatformStatus {
status: string;
connected: boolean;
last_sync?: string;
data_points?: number;
}
export interface AIInsight {
insight: string;
priority: string;
category: string;
action_required: boolean;
tool_path?: string;
}
export interface SEODashboardData {
health_score: SEOHealthScore;
key_insight: string;
priority_alert: string;
metrics: Record<string, SEOMetric>;
platforms: Record<string, PlatformStatus>;
ai_insights: AIInsight[];
last_updated: string;
website_url?: string; // User's website URL from onboarding
}
// SEO Dashboard API functions
export const seoDashboardAPI = {
// Get complete dashboard data
async getDashboardData(): Promise<SEODashboardData> {
try {
const response = await apiClient.get('/api/seo-dashboard/data');
return response.data;
} catch (error) {
console.error('Error fetching SEO dashboard data:', error);
throw error;
}
},
// Get health score only
async getHealthScore(): Promise<SEOHealthScore> {
try {
const response = await apiClient.get('/api/seo-dashboard/health-score');
return response.data;
} catch (error) {
console.error('Error fetching SEO health score:', error);
throw error;
}
},
// Get metrics only
async getMetrics(): Promise<Record<string, SEOMetric>> {
try {
const response = await apiClient.get('/api/seo-dashboard/metrics');
return response.data;
} catch (error) {
console.error('Error fetching SEO metrics:', error);
throw error;
}
},
// Get platform status
async getPlatformStatus(): Promise<Record<string, PlatformStatus>> {
try {
const response = await apiClient.get('/api/seo-dashboard/platforms');
return response.data;
} catch (error) {
console.error('Error fetching platform status:', error);
throw error;
}
},
// Get AI insights
async getAIInsights(): Promise<AIInsight[]> {
try {
const response = await apiClient.get('/api/seo-dashboard/insights');
return response.data;
} catch (error) {
console.error('Error fetching AI insights:', error);
throw error;
}
},
// Health check
async healthCheck(): Promise<any> {
try {
const response = await apiClient.get('/api/seo-dashboard/health');
return response.data;
} catch (error) {
console.error('Error checking SEO dashboard health:', error);
throw error;
}
}
};

View File

@@ -0,0 +1,289 @@
/** Style Detection API Integration */
export interface StyleAnalysisRequest {
content: {
main_content: string;
title?: string;
description?: string;
};
analysis_type?: 'comprehensive' | 'patterns';
}
export interface StyleAnalysisResponse {
success: boolean;
analysis?: any;
patterns?: any;
guidelines?: any;
error?: string;
timestamp: string;
}
export interface WebCrawlRequest {
url?: string;
text_sample?: string;
}
export interface WebCrawlResponse {
success: boolean;
content?: any;
metrics?: any;
error?: string;
timestamp: string;
}
export interface StyleDetectionRequest {
url?: string;
text_sample?: string;
include_patterns?: boolean;
include_guidelines?: boolean;
}
export interface StyleDetectionResponse {
success: boolean;
crawl_result?: any;
style_analysis?: any;
style_patterns?: any;
style_guidelines?: any;
error?: string;
warning?: string;
timestamp: string;
}
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
/**
* Analyze content style using AI
*/
export const analyzeContentStyle = async (request: StyleAnalysisRequest): Promise<StyleAnalysisResponse> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error analyzing content style:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
};
}
};
/**
* Crawl website content for style analysis
*/
export const crawlWebsiteContent = async (request: WebCrawlRequest): Promise<WebCrawlResponse> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/crawl`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error crawling website content:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
};
}
};
/**
* Complete style detection workflow
*/
export const completeStyleDetection = async (request: StyleDetectionRequest): Promise<StyleDetectionResponse> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error in complete style detection:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
};
}
};
/**
* Get style detection configuration options
*/
export const getStyleDetectionConfiguration = async (): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/configuration-options`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error getting style detection configuration:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
/**
* Validate style detection request
*/
export const validateStyleDetectionRequest = (request: StyleDetectionRequest): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
if (!request.url && !request.text_sample) {
errors.push('Either URL or text sample is required');
}
if (request.url && !request.url.startsWith('http')) {
errors.push('URL must start with http:// or https://');
}
if (request.text_sample && request.text_sample.length < 50) {
errors.push('Text sample must be at least 50 characters');
}
if (request.text_sample && request.text_sample.length > 10000) {
errors.push('Text sample is too long (max 10,000 characters)');
}
return {
valid: errors.length === 0,
errors,
};
};
/**
* Check if analysis exists for a website URL
*/
export const checkExistingAnalysis = async (websiteUrl: string): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/check-existing/${encodeURIComponent(websiteUrl)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error checking existing analysis:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
/**
* Get analysis by ID
*/
export const getAnalysisById = async (analysisId: number): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/analysis/${analysisId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error getting analysis by ID:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
/**
* Get all analyses for the current session
*/
export const getSessionAnalyses = async (): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/session-analyses`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error getting session analyses:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
/**
* Delete an analysis
*/
export const deleteAnalysis = async (analysisId: number): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/api/onboarding/style-detection/analysis/${analysisId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error deleting analysis:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};

View File

@@ -0,0 +1,68 @@
import { apiClient } from './client';
export interface UserData {
website_url?: string;
session?: {
id: number;
current_step: number;
progress: number;
started_at?: string;
updated_at?: string;
};
website_analysis?: {
website_url: string;
industry: string;
target_audience: string;
content_goals: string[];
brand_voice: string;
content_style: string;
};
api_keys?: Array<{
id: number;
provider: string;
description?: string;
}>;
research_preferences?: {
target_keywords: string[];
competitor_urls: string[];
content_topics: string[];
};
}
export const userDataAPI = {
async getUserData(): Promise<UserData | null> {
try {
console.log('Fetching user data from backend...');
const response = await apiClient.get('/api/user-data');
console.log('User data received:', response.data);
return response.data;
} catch (error: any) {
console.error('Error fetching user data:', error);
return null;
}
},
async getWebsiteURL(): Promise<string | null> {
try {
console.log('Fetching website URL...');
const response = await apiClient.get('/api/user-data/website-url');
console.log('Website URL received:', response.data);
return response.data.website_url || null;
} catch (error: any) {
console.error('Error fetching website URL:', error);
return null;
}
},
async getOnboardingData(): Promise<any> {
try {
console.log('Fetching onboarding data...');
const response = await apiClient.get('/api/user-data/onboarding');
console.log('Onboarding data received:', response.data);
return response.data;
} catch (error: any) {
console.error('Error fetching onboarding data:', error);
return null;
}
}
};

View File

@@ -0,0 +1,320 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Tabs,
Tab,
Typography,
Container,
AppBar,
Toolbar,
IconButton,
Alert,
Drawer,
Button,
Badge
} from '@mui/material';
import {
Psychology as StrategyIcon,
CalendarToday as CalendarIcon,
Analytics as AnalyticsIcon,
Search as SearchIcon,
Lightbulb as AIInsightsIcon,
Close as CloseIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import ContentStrategyTab from './tabs/ContentStrategyTab';
import CalendarTab from './tabs/CalendarTab';
import AnalyticsTab from './tabs/AnalyticsTab';
import GapAnalysisTab from './tabs/GapAnalysisTab';
import AIInsightsPanel from './components/AIInsightsPanel';
import ServiceStatusPanel from './components/ServiceStatusPanel';
import ProgressIndicator from './components/ProgressIndicator';
import { useContentPlanningStore } from '../../stores/contentPlanningStore';
import {
contentPlanningOrchestrator,
ServiceStatus,
DashboardData
} from '../../services/contentPlanningOrchestrator';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`content-planning-tabpanel-${index}`}
aria-labelledby={`content-planning-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div>
);
}
function a11yProps(index: number) {
return {
id: `content-planning-tab-${index}`,
'aria-controls': `content-planning-tabpanel-${index}`,
};
}
const ContentPlanningDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState(0);
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
const [dashboardData, setDashboardData] = useState<DashboardData>({
strategies: [],
gapAnalyses: [],
aiInsights: [],
aiRecommendations: [],
calendarEvents: [],
healthStatus: {
backend: false,
database: false,
aiServices: false
}
});
const [statusPanelExpanded, setStatusPanelExpanded] = useState(false);
const [progressExpanded, setProgressExpanded] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [aiInsightsDrawerOpen, setAiInsightsDrawerOpen] = useState(false);
const {
updateStrategies,
updateCalendarEvents,
updateGapAnalyses,
updateAIInsights
} = useContentPlanningStore();
// Initialize orchestrator callbacks
useEffect(() => {
contentPlanningOrchestrator.setProgressCallback((statuses) => {
setServiceStatuses(statuses);
});
contentPlanningOrchestrator.setDataUpdateCallback((data) => {
setDashboardData(prev => ({ ...prev, ...data }));
// Update store with new data
if (data.strategies) updateStrategies(data.strategies);
if (data.calendarEvents) updateCalendarEvents(data.calendarEvents);
if (data.gapAnalyses) updateGapAnalyses(data.gapAnalyses);
if (data.aiInsights || data.aiRecommendations) {
updateAIInsights({
insights: data.aiInsights || [],
recommendations: data.aiRecommendations || []
});
}
});
}, [updateStrategies, updateCalendarEvents, updateGapAnalyses, updateAIInsights]);
// Load dashboard data using orchestrator
useEffect(() => {
const loadDashboardData = async () => {
try {
setLoading(true);
setError(null);
await contentPlanningOrchestrator.loadDashboardData();
} catch (error: any) {
console.error('Failed to load dashboard data:', error);
setError(error.message || 'Failed to load dashboard data');
} finally {
setLoading(false);
}
};
// Wrap in try-catch to handle any unexpected errors
try {
loadDashboardData();
} catch (error: any) {
console.error('Unexpected error in dashboard:', error);
setError('An unexpected error occurred while loading the dashboard');
setLoading(false);
}
}, []);
const handleRefreshService = (serviceName: string) => {
contentPlanningOrchestrator.refreshService(serviceName);
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
const getOverallHealthStatus = () => {
const { healthStatus } = dashboardData;
if (healthStatus.backend && healthStatus.database && healthStatus.aiServices) {
return { status: 'success', text: 'Connected' };
} else if (healthStatus.backend && healthStatus.database) {
return { status: 'warning', text: 'Connected API & DB' };
} else {
return { status: 'error', text: 'Disconnected' };
}
};
const overallHealth = getOverallHealthStatus();
const tabs = [
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
{ label: 'ANALYTICS', icon: <AnalyticsIcon />, component: <AnalyticsTab /> },
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> }
];
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
return (
<Container maxWidth={false} sx={{ height: '100vh', p: 0 }}>
<AppBar position="static" color="default" elevation={1}>
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Content Planning Dashboard
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ServiceStatusPanel
serviceStatuses={serviceStatuses}
onRefreshService={handleRefreshService}
expanded={statusPanelExpanded}
onToggleExpanded={() => setStatusPanelExpanded(!statusPanelExpanded)}
/>
{/* AI Insights Button with Badge */}
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
variant="outlined"
startIcon={<AIInsightsIcon />}
onClick={() => setAiInsightsDrawerOpen(true)}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'primary.main',
color: 'primary.main',
'&:hover': {
borderColor: 'primary.dark',
backgroundColor: 'primary.50'
}
}}
>
<Badge badgeContent={totalAIItems} color="primary" sx={{ mr: 1 }}>
AI Insights
</Badge>
</Button>
</motion.div>
</Box>
</Toolbar>
</AppBar>
{error && (
<Alert severity="error" sx={{ m: 2 }}>
{error}
</Alert>
)}
{/* Progress Indicator */}
{loading && (
<Box sx={{ m: 2 }}>
<ProgressIndicator
serviceStatuses={serviceStatuses}
onRefreshService={handleRefreshService}
expanded={progressExpanded}
onToggleExpanded={() => setProgressExpanded(!progressExpanded)}
/>
</Box>
)}
<Box sx={{ display: 'flex', height: 'calc(100vh - 64px)' }}>
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="content planning tabs"
sx={{ px: 2 }}
>
{tabs.map((tab, index) => (
<Tab
key={index}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{tab.icon}
{tab.label}
</Box>
}
{...a11yProps(index)}
/>
))}
</Tabs>
</Box>
{tabs.map((tab, index) => (
<TabPanel key={index} value={activeTab} index={index}>
{tab.component}
</TabPanel>
))}
</Box>
</Box>
{/* AI Insights Drawer */}
<Drawer
anchor="right"
open={aiInsightsDrawerOpen}
onClose={() => setAiInsightsDrawerOpen(false)}
PaperProps={{
sx: {
width: 400,
height: '100%',
backgroundColor: 'background.paper',
borderLeft: '1px solid',
borderColor: 'divider'
}
}}
>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center' }}>
<AIInsightsIcon sx={{ mr: 1 }} />
AI Insights
</Typography>
<IconButton
onClick={() => setAiInsightsDrawerOpen(false)}
size="small"
>
<CloseIcon />
</IconButton>
</Box>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<AIInsightsPanel />
</motion.div>
</AnimatePresence>
</Box>
</Drawer>
</Container>
);
};
export default ContentPlanningDashboard;

View File

@@ -0,0 +1,475 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
IconButton,
List,
ListItem,
ListItemText,
ListItemIcon,
Divider,
Alert,
CircularProgress
} from '@mui/material';
import {
Lightbulb as LightbulbIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
TrendingUp as TrendingUpIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AIInsightsPanel: React.FC = () => {
const {
aiInsights,
aiRecommendations,
loading,
error,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [expandedInsights, setExpandedInsights] = useState<Set<string>>(new Set());
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadAIData();
}, []);
const loadAIData = async () => {
try {
setDataLoading(true);
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
} catch (error) {
console.error('Error loading AI data:', error);
} finally {
setDataLoading(false);
}
};
const handleRefresh = async () => {
await loadAIData();
};
const handleForceRefresh = async () => {
try {
setDataLoading(true);
// Force refresh AI insights and recommendations
await Promise.all([
contentPlanningApi.getAIAnalyticsWithRefresh(undefined, true), // Force refresh
contentPlanningApi.getGapAnalysesWithRefresh(undefined, true) // Force refresh
]);
// Reload data from store
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
} catch (error) {
console.error('Error force refreshing AI data:', error);
} finally {
setDataLoading(false);
}
};
const toggleInsightExpansion = (insightId: string) => {
const newExpanded = new Set(expandedInsights);
if (newExpanded.has(insightId)) {
newExpanded.delete(insightId);
} else {
newExpanded.add(insightId);
}
setExpandedInsights(newExpanded);
};
const getInsightIcon = (type: string) => {
switch (type) {
case 'performance':
return <TrendingUpIcon color="success" />;
case 'opportunity':
return <LightbulbIcon color="primary" />;
case 'warning':
return <WarningIcon color="warning" />;
case 'trend':
return <AssessmentIcon color="info" />;
default:
return <CheckCircleIcon color="success" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return 'error';
case 'medium':
return 'warning';
case 'low':
return 'success';
default:
return 'default';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'performance':
return 'success';
case 'opportunity':
return 'primary';
case 'warning':
return 'warning';
case 'trend':
return 'info';
default:
return 'default';
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.3
}
}
};
const cardVariants = {
initial: { scale: 1 },
hover: {
scale: 1.02,
transition: { duration: 0.2 }
},
tap: { scale: 0.98 }
};
return (
<Box sx={{ p: 2, height: '100%', overflowY: 'auto' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center' }}>
<LightbulbIcon sx={{ mr: 1 }} />
AI Insights
</Typography>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<IconButton
onClick={handleRefresh}
disabled={dataLoading}
size="small"
>
<RefreshIcon />
</IconButton>
</motion.div>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* AI Insights */}
{aiInsights && aiInsights.length > 0 && (
<motion.div variants={itemVariants}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Recent Insights ({aiInsights.length})
</Typography>
<AnimatePresence>
{aiInsights.map((insight, index) => (
<motion.div
key={insight.id}
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
custom={index}
>
<motion.div
variants={cardVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
<Card
sx={{
mb: 2,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: 3,
borderColor: 'primary.main'
}
}}
onClick={() => toggleInsightExpansion(insight.id)}
>
<CardContent sx={{ py: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
{getInsightIcon(insight.type)}
</ListItemIcon>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle2" gutterBottom>
{insight.title}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
<Chip
label={insight.type}
color={getTypeColor(insight.type)}
size="small"
/>
<Chip
label={insight.priority}
color={getPriorityColor(insight.priority)}
size="small"
/>
</Box>
<Typography variant="caption" color="text.secondary">
{new Date(insight.created_at).toLocaleDateString()}
</Typography>
</Box>
</Box>
<motion.div
animate={{ rotate: expandedInsights.has(insight.id) ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<IconButton size="small">
<ExpandMoreIcon />
</IconButton>
</motion.div>
</Box>
<AnimatePresence>
{expandedInsights.has(insight.id) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Divider sx={{ my: 1 }} />
<Typography variant="body2" color="text.secondary">
{insight.description}
</Typography>
</motion.div>
)}
</AnimatePresence>
</CardContent>
</Card>
</motion.div>
</motion.div>
))}
</AnimatePresence>
</Box>
</motion.div>
)}
{/* AI Recommendations */}
{aiRecommendations && aiRecommendations.length > 0 && (
<motion.div variants={itemVariants}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
AI Recommendations ({aiRecommendations.length})
</Typography>
<AnimatePresence>
{aiRecommendations.map((recommendation, index) => (
<motion.div
key={recommendation.id}
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
custom={index}
>
<motion.div
variants={cardVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
<Card
sx={{
mb: 2,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: 3,
borderColor: 'primary.main'
}
}}
onClick={() => toggleInsightExpansion(recommendation.id)}
>
<CardContent sx={{ py: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<AssessmentIcon color="primary" />
</ListItemIcon>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle2" gutterBottom>
{recommendation.title}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
<Chip
label={recommendation.type}
color="primary"
size="small"
/>
<Chip
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
color="success"
size="small"
/>
</Box>
<Typography variant="caption" color="text.secondary">
Status: {recommendation.status}
</Typography>
</Box>
</Box>
<motion.div
animate={{ rotate: expandedInsights.has(recommendation.id) ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<IconButton size="small">
<ExpandMoreIcon />
</IconButton>
</motion.div>
</Box>
<AnimatePresence>
{expandedInsights.has(recommendation.id) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Divider sx={{ my: 1 }} />
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{recommendation.description}
</Typography>
{recommendation.reasoning && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
<strong>Reasoning:</strong> {recommendation.reasoning}
</Typography>
)}
{recommendation.action_items && recommendation.action_items.length > 0 && (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
<strong>Action Items:</strong>
</Typography>
<List dense>
{recommendation.action_items.map((action, actionIndex) => (
<motion.div
key={actionIndex}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: actionIndex * 0.1 }}
>
<ListItem sx={{ py: 0 }}>
<ListItemIcon sx={{ minWidth: 30 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={action}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
</motion.div>
))}
</List>
</Box>
)}
</motion.div>
)}
</AnimatePresence>
</CardContent>
</Card>
</motion.div>
</motion.div>
))}
</AnimatePresence>
</Box>
</motion.div>
)}
{/* No Data State */}
{(!aiInsights || aiInsights.length === 0) && (!aiRecommendations || aiRecommendations.length === 0) && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Box sx={{ textAlign: 'center', py: 3 }}>
<motion.div
animate={{
scale: [1, 1.1, 1],
rotate: [0, 5, -5, 0]
}}
transition={{
duration: 2,
repeat: Infinity,
repeatType: "reverse"
}}
>
<LightbulbIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
</motion.div>
<Typography variant="body2" color="text.secondary">
No AI insights available yet.
</Typography>
<Typography variant="caption" color="text.secondary">
Run content analysis to generate insights.
</Typography>
</Box>
</motion.div>
)}
</motion.div>
)}
</Box>
);
};
export default AIInsightsPanel;

View File

@@ -0,0 +1,210 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
LinearProgress,
Alert,
IconButton,
Collapse
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
Lightbulb as LightbulbIcon,
TrendingUp as TrendingUpIcon,
Psychology as PsychologyIcon,
Analytics as AnalyticsIcon,
CalendarToday as CalendarIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
interface AIRecommendationsPanelProps {
aiGenerating: boolean;
onGenerateRecommendations: () => void;
}
const AIRecommendationsPanel: React.FC<AIRecommendationsPanelProps> = ({
aiGenerating,
onGenerateRecommendations
}) => {
const [expanded, setExpanded] = React.useState(true);
// Mock AI recommendations data (this would come from the store)
const aiRecommendations = [
{
id: '1',
type: 'comprehensive_strategy',
title: 'Content Strategy Optimization',
description: 'Based on your business objectives, we recommend focusing on thought leadership content to establish authority in your industry.',
confidence: 0.85,
category: 'Strategy',
icon: <PsychologyIcon />
},
{
id: '2',
type: 'audience_intelligence',
title: 'Audience Targeting',
description: 'Your audience prefers video content and technical deep-dives. Consider increasing video production by 40%.',
confidence: 0.78,
category: 'Audience',
icon: <TrendingUpIcon />
},
{
id: '3',
type: 'competitive_intelligence',
title: 'Competitive Advantage',
description: 'Your competitors are weak in technical content. This presents an opportunity to differentiate through detailed tutorials.',
confidence: 0.92,
category: 'Competition',
icon: <LightbulbIcon />
},
{
id: '4',
type: 'performance_optimization',
title: 'Performance Improvement',
description: 'Your current content frequency is optimal. Focus on quality over quantity to improve engagement rates.',
confidence: 0.76,
category: 'Performance',
icon: <AnalyticsIcon />
},
{
id: '5',
type: 'content_calendar_optimization',
title: 'Publishing Schedule',
description: 'Publish technical content on Tuesdays and Thursdays when your audience is most engaged.',
confidence: 0.81,
category: 'Calendar',
icon: <CalendarIcon />
}
];
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'success';
if (confidence >= 0.6) return 'warning';
return 'error';
};
const getConfidenceLabel = (confidence: number) => {
if (confidence >= 0.8) return 'High';
if (confidence >= 0.6) return 'Medium';
return 'Low';
};
return (
<Card variant="outlined">
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<AutoAwesomeIcon color="primary" />
<Typography variant="h6">
AI Recommendations
</Typography>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
<Collapse in={expanded}>
{/* Generate Button */}
<Box sx={{ mb: 2 }}>
<Button
variant="contained"
fullWidth
startIcon={aiGenerating ? undefined : <AutoAwesomeIcon />}
onClick={onGenerateRecommendations}
disabled={aiGenerating}
sx={{ mb: 1 }}
>
{aiGenerating ? 'Generating...' : 'Generate AI Insights'}
</Button>
{aiGenerating && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LinearProgress sx={{ flexGrow: 1 }} />
<Typography variant="caption" color="text.secondary">
Analyzing...
</Typography>
</Box>
)}
</Box>
{/* AI Recommendations List */}
{aiRecommendations.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Recent Recommendations
</Typography>
<List dense>
{aiRecommendations.map((recommendation, index) => (
<React.Fragment key={recommendation.id}>
<ListItem sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
{recommendation.icon}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" fontWeight="medium">
{recommendation.title}
</Typography>
<Chip
label={recommendation.category}
size="small"
variant="outlined"
/>
<Chip
label={`${Math.round(recommendation.confidence * 100)}% confidence`}
size="small"
color={getConfidenceColor(recommendation.confidence)}
/>
</Box>
}
secondary={
<Typography variant="body2" color="text.secondary">
{recommendation.description}
</Typography>
}
/>
</ListItem>
{index < aiRecommendations.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</Box>
)}
{/* No Recommendations State */}
{aiRecommendations.length === 0 && !aiGenerating && (
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
Generate AI recommendations to get personalized insights for your content strategy.
</Typography>
</Alert>
)}
{/* AI Status */}
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
AI analyzes your inputs to provide personalized recommendations for your content strategy.
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
};
export default AIRecommendationsPanel;

View File

@@ -0,0 +1,69 @@
import React, { useState } from 'react';
import {
Box,
Paper,
Typography,
Button,
Alert,
CircularProgress
} from '@mui/material';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AITestComponent: React.FC = () => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const testAIConnection = async () => {
setLoading(true);
setError(null);
setResult(null);
try {
const response = await contentPlanningApi.getAIAnalyticsSafe();
setResult(response);
console.log('AI Test Response:', response);
} catch (err: any) {
setError(err.message || 'Failed to connect to AI service');
console.error('AI Test Error:', err);
} finally {
setLoading(false);
}
};
return (
<Paper sx={{ p: 2, m: 2 }}>
<Typography variant="h6" gutterBottom>
AI Integration Test
</Typography>
<Button
variant="contained"
onClick={testAIConnection}
disabled={loading}
sx={{ mb: 2 }}
>
{loading ? <CircularProgress size={20} /> : 'Test AI Connection'}
</Button>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{result && (
<Box>
<Typography variant="subtitle2" gutterBottom>
AI Test Results:
</Typography>
<pre style={{ fontSize: '12px', overflow: 'auto' }}>
{JSON.stringify(result, null, 2)}
</pre>
</Box>
)}
</Paper>
);
};
export default AITestComponent;

View File

@@ -0,0 +1,157 @@
import React from 'react';
import {
Box,
Typography,
LinearProgress,
Chip,
Card,
CardContent,
Grid,
Tooltip,
IconButton
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
TrendingUp as TrendingUpIcon,
Info as InfoIcon
} from '@mui/icons-material';
interface CompletionStats {
total_fields: number;
filled_fields: number;
completion_percentage: number;
category_completion: Record<string, number>;
}
interface CompletionTrackerProps {
completionPercentage: number;
completionStats: CompletionStats;
}
const CompletionTracker: React.FC<CompletionTrackerProps> = ({
completionPercentage,
completionStats
}) => {
const getCategoryColor = (percentage: number) => {
if (percentage >= 80) return 'success';
if (percentage >= 60) return 'warning';
return 'error';
};
const getCategoryIcon = (category: string) => {
const icons = {
business_context: '🏢',
audience_intelligence: '👥',
competitive_intelligence: '📈',
content_strategy: '📝',
performance_analytics: '📊'
};
return icons[category as keyof typeof icons] || '📋';
};
const getCategoryLabel = (category: string) => {
const labels = {
business_context: 'Business Context',
audience_intelligence: 'Audience Intelligence',
competitive_intelligence: 'Competitive Intelligence',
content_strategy: 'Content Strategy',
performance_analytics: 'Performance & Analytics'
};
return labels[category as keyof typeof labels] || category;
};
const getCompletionStatus = (percentage: number) => {
if (percentage >= 90) return { status: 'Excellent', color: 'success' as const };
if (percentage >= 70) return { status: 'Good', color: 'primary' as const };
if (percentage >= 50) return { status: 'Fair', color: 'warning' as const };
return { status: 'Needs Work', color: 'error' as const };
};
const status = getCompletionStatus(completionPercentage);
return (
<Card variant="outlined" sx={{ minWidth: 300 }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TrendingUpIcon color="primary" />
<Typography variant="h6">
Strategy Progress
</Typography>
<Chip
label={status.status}
color={status.color}
size="small"
/>
</Box>
{/* Overall Progress */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Overall Completion
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(completionPercentage)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={completionPercentage}
sx={{ height: 8, borderRadius: 4 }}
color={status.color}
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{completionStats.filled_fields} of {completionStats.total_fields} fields completed
</Typography>
</Box>
{/* Category Breakdown */}
<Box>
<Typography variant="subtitle2" gutterBottom>
Category Progress
</Typography>
<Grid container spacing={1}>
{Object.entries(completionStats.category_completion).map(([category, percentage]) => (
<Grid item xs={12} key={category}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Typography variant="body2" sx={{ minWidth: 20 }}>
{getCategoryIcon(category)}
</Typography>
<Typography variant="body2" sx={{ flexGrow: 1, fontSize: '0.875rem' }}>
{getCategoryLabel(category)}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 40 }}>
{Math.round(percentage)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={percentage}
color={getCategoryColor(percentage)}
sx={{ height: 4, borderRadius: 2 }}
/>
</Grid>
))}
</Grid>
</Box>
{/* Progress Insights */}
{completionPercentage > 0 && (
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
{completionPercentage >= 80 ? (
'🎉 Great progress! You\'re ready to generate AI recommendations.'
) : completionPercentage >= 50 ? (
'📈 Good progress! Consider filling more fields for better AI insights.'
) : (
'💡 Start with the Business Context section to build a strong foundation.'
)}
</Typography>
</Box>
)}
</CardContent>
</Card>
);
};
export default CompletionTracker;

View File

@@ -0,0 +1,235 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
LinearProgress,
Alert,
IconButton,
Collapse,
Tooltip
} from '@mui/material';
import {
DataUsage as DataUsageIcon,
AutoAwesome as AutoAwesomeIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
interface DataSourceTransparencyProps {
autoPopulatedFields: Record<string, any>;
dataSources: Record<string, string>;
}
const DataSourceTransparency: React.FC<DataSourceTransparencyProps> = ({
autoPopulatedFields,
dataSources
}) => {
const [expanded, setExpanded] = React.useState(true);
const getDataSourceIcon = (source: string) => {
const icons = {
website_analysis: '🌐',
research_preferences: '🔍',
api_keys: '🔑',
onboarding_session: '📋'
};
return icons[source as keyof typeof icons] || '📊';
};
const getDataSourceLabel = (source: string) => {
const labels = {
website_analysis: 'Website Analysis',
research_preferences: 'Research Preferences',
api_keys: 'API Configuration',
onboarding_session: 'Onboarding Session'
};
return labels[source as keyof typeof labels] || source;
};
const getDataQualityScore = (source: string) => {
// Mock quality scores based on data source
const scores = {
website_analysis: 0.85,
research_preferences: 0.92,
api_keys: 0.78,
onboarding_session: 0.88
};
return scores[source as keyof typeof scores] || 0.7;
};
const getDataQualityColor = (score: number) => {
if (score >= 0.8) return 'success';
if (score >= 0.6) return 'warning';
return 'error';
};
const getDataQualityLabel = (score: number) => {
if (score >= 0.8) return 'High Quality';
if (score >= 0.6) return 'Medium Quality';
return 'Low Quality';
};
const autoPopulatedFieldsList = Object.entries(autoPopulatedFields).map(([fieldId, value]) => ({
fieldId,
value,
source: dataSources[fieldId] || 'unknown',
qualityScore: getDataQualityScore(dataSources[fieldId] || 'unknown')
}));
const sourceSummary = Object.entries(dataSources).reduce((acc, [fieldId, source]) => {
if (!acc[source]) {
acc[source] = [];
}
acc[source].push(fieldId);
return acc;
}, {} as Record<string, string[]>);
if (Object.keys(autoPopulatedFields).length === 0) {
return null;
}
return (
<Card variant="outlined">
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<DataUsageIcon color="primary" />
<Typography variant="h6">
Data Sources
</Typography>
<Chip
icon={<AutoAwesomeIcon />}
label={`${Object.keys(autoPopulatedFields).length} auto-populated`}
color="info"
size="small"
/>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
<Collapse in={expanded}>
{/* Summary */}
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
{Object.keys(autoPopulatedFields).length} fields were automatically populated from your onboarding data.
</Typography>
</Alert>
{/* Data Sources Breakdown */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Data Sources
</Typography>
<List dense>
{Object.entries(sourceSummary).map(([source, fields]) => (
<ListItem key={source} sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<Typography variant="body1">
{getDataSourceIcon(source)}
</Typography>
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" fontWeight="medium">
{getDataSourceLabel(source)}
</Typography>
<Chip
label={`${fields.length} fields`}
size="small"
variant="outlined"
/>
</Box>
}
secondary={
<Box sx={{ mt: 0.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<LinearProgress
variant="determinate"
value={getDataQualityScore(source) * 100}
color={getDataQualityColor(getDataQualityScore(source))}
sx={{ flexGrow: 1, height: 4, borderRadius: 2 }}
/>
<Typography variant="caption" color="text.secondary">
{Math.round(getDataQualityScore(source) * 100)}%
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
{getDataQualityLabel(getDataQualityScore(source))}
</Typography>
</Box>
}
/>
</ListItem>
))}
</List>
</Box>
<Divider sx={{ my: 2 }} />
{/* Auto-populated Fields */}
<Box>
<Typography variant="subtitle2" gutterBottom>
Auto-populated Fields
</Typography>
<List dense>
{autoPopulatedFieldsList.map((field, index) => (
<React.Fragment key={field.fieldId}>
<ListItem sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" fontWeight="medium">
{field.fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Typography>
<Chip
label={getDataSourceLabel(field.source)}
size="small"
variant="outlined"
/>
</Box>
}
secondary={
<Typography variant="caption" color="text.secondary">
Source: {getDataSourceLabel(field.source)} Quality: {getDataQualityLabel(field.qualityScore)}
</Typography>
}
/>
</ListItem>
{index < autoPopulatedFieldsList.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</Box>
{/* Transparency Note */}
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
💡 You can modify any auto-populated field. The system learns from your changes to improve future recommendations.
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
};
export default DataSourceTransparency;

View File

@@ -0,0 +1,514 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
Stepper,
Step,
StepLabel,
StepContent,
Button,
LinearProgress,
Alert,
Chip,
IconButton,
Tooltip as MuiTooltip,
Card,
CardContent,
Grid,
Divider,
CircularProgress,
Badge
} from '@mui/material';
import {
Business as BusinessIcon,
People as PeopleIcon,
TrendingUp as TrendingUpIcon,
ContentPaste as ContentIcon,
Analytics as AnalyticsIcon,
Help as HelpIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
AutoAwesome as AutoAwesomeIcon,
Refresh as RefreshIcon,
Save as SaveIcon,
ArrowForward as ArrowForwardIcon,
ArrowBack as ArrowBackIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore, STRATEGIC_INPUT_FIELDS } from '../../../stores/enhancedStrategyStore';
import StrategicInputField from './StrategicInputField';
import EnhancedTooltip from './EnhancedTooltip';
import CompletionTracker from './CompletionTracker';
import AIRecommendationsPanel from './AIRecommendationsPanel';
import DataSourceTransparency from './/DataSourceTransparency';
const EnhancedStrategyBuilder: React.FC = () => {
const {
formData,
formErrors,
autoPopulatedFields,
dataSources,
loading,
error,
saving,
aiGenerating,
currentStep,
completedSteps,
disclosureSteps,
currentStrategy,
updateFormField,
validateFormField,
validateAllFields,
completeStep,
getNextStep,
getPreviousStep,
setCurrentStep,
canProceedToStep,
resetForm,
autoPopulateFromOnboarding,
generateAIRecommendations,
createEnhancedStrategy,
calculateCompletionPercentage,
getCompletionStats,
setError,
setCurrentStrategy,
setAIGenerating
} = useEnhancedStrategyStore();
const [showTooltip, setShowTooltip] = useState<string | null>(null);
const [autoPopulateAttempted, setAutoPopulateAttempted] = useState(false);
// Auto-populate from onboarding on first load
useEffect(() => {
if (!autoPopulateAttempted) {
autoPopulateFromOnboarding();
setAutoPopulateAttempted(true);
}
}, [autoPopulateAttempted, autoPopulateFromOnboarding]);
const handleStepComplete = () => {
const currentStepData = disclosureSteps[currentStep];
if (currentStepData) {
// Validate all fields in current step
const stepFields = currentStepData.fields;
const isValid = stepFields.every(fieldId => validateFormField(fieldId));
if (isValid) {
completeStep(currentStepData.id);
// Move to next step if available
const nextStep = getNextStep();
if (nextStep) {
setCurrentStep(currentStep + 1);
}
}
}
};
const handleNextStep = () => {
const nextStep = getNextStep();
if (nextStep) {
setCurrentStep(currentStep + 1);
}
};
const handlePreviousStep = () => {
const prevStep = getPreviousStep();
if (prevStep) {
setCurrentStep(currentStep - 1);
}
};
const handleSaveStrategy = async () => {
if (validateAllFields()) {
const completionStats = getCompletionStats();
const strategyData = {
...formData,
completion_percentage: completionStats.completion_percentage,
user_id: 1, // This would come from auth context
name: formData.name || 'Enhanced Content Strategy',
industry: formData.industry || 'General'
};
await createEnhancedStrategy(strategyData);
}
};
const handleCreateStrategy = async () => {
try {
setAIGenerating(true);
setError(null);
console.log('Starting strategy creation...');
console.log('Current formData:', formData);
console.log('FormData ID:', formData.id);
// If we have a saved strategy, use its ID
if (formData.id) {
console.log('Using existing strategy ID:', formData.id);
await generateAIRecommendations(formData.id);
} else {
console.log('No strategy ID found, creating new strategy...');
// If no strategy is saved yet, save it first, then generate AI insights
const isValid = validateAllFields();
console.log('Form validation result:', isValid);
if (isValid) {
const completionStats = getCompletionStats();
const strategyData = {
...formData,
completion_percentage: completionStats.completion_percentage,
user_id: 1, // This would come from auth context
name: formData.name || 'Enhanced Content Strategy',
industry: formData.industry || 'General'
};
console.log('Strategy data to create:', strategyData);
// Save the strategy first and get the created strategy
const newStrategy = await createEnhancedStrategy(strategyData);
console.log('Created strategy:', newStrategy);
if (newStrategy && newStrategy.id) {
console.log('Generating AI recommendations for strategy ID:', newStrategy.id);
// Now generate AI recommendations with the new strategy ID
await generateAIRecommendations(newStrategy.id);
// Set the current strategy and show success message
setCurrentStrategy(newStrategy);
setError(null); // Clear any previous errors
// Show success message
setTimeout(() => {
setError('Strategy created successfully! Check the Strategic Intelligence tab for detailed insights.');
}, 100);
// Auto-switch to Strategic Intelligence tab after creation
// This would need to be handled by the parent component
} else {
console.error('Failed to create strategy or get strategy ID');
setError('Failed to create strategy. Please try again.');
}
} else {
console.log('Form validation failed');
setError('Please complete all required fields before creating strategy');
}
}
} catch (error: any) {
console.error('Error creating strategy:', error);
setError(error.message || 'Failed to create strategy');
} finally {
setAIGenerating(false);
}
};
const getStepIcon = (stepId: string) => {
const icons = {
business_context: <BusinessIcon />,
audience_intelligence: <PeopleIcon />,
competitive_intelligence: <TrendingUpIcon />,
content_strategy: <ContentIcon />,
performance_analytics: <AnalyticsIcon />
};
return icons[stepId as keyof typeof icons] || <BusinessIcon />;
};
const getStepColor = (stepId: string) => {
if (completedSteps.includes(stepId)) return 'success';
if (currentStep === disclosureSteps.findIndex(s => s.id === stepId)) return 'primary';
return 'default';
};
const completionStats = getCompletionStats();
const completionPercentage = calculateCompletionPercentage();
// Debug logging
console.log('Completion percentage:', completionPercentage);
console.log('Form data keys:', Object.keys(formData));
console.log('Required fields:', STRATEGIC_INPUT_FIELDS.filter(f => f.required).map(f => f.id));
console.log('Filled required fields:', STRATEGIC_INPUT_FIELDS.filter(f => f.required && formData[f.id]).map(f => f.id));
return (
<Box sx={{ p: 3 }}>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom>
Enhanced Strategy Builder
</Typography>
<Typography variant="body2" color="text.secondary">
Build a comprehensive content strategy with 30+ strategic inputs
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<CompletionTracker
completionPercentage={completionPercentage}
completionStats={completionStats}
/>
<MuiTooltip
title={completionPercentage < 20 ? `Complete at least 20% of the form (currently ${Math.round(completionPercentage)}%)` : 'Create a comprehensive content strategy with AI insights'}
placement="top"
>
<span>
<Button
variant="outlined"
startIcon={<AutoAwesomeIcon />}
onClick={handleCreateStrategy}
disabled={aiGenerating || completionPercentage < 20}
>
{aiGenerating ? 'Creating...' : 'Create Strategy'}
</Button>
</span>
</MuiTooltip>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSaveStrategy}
disabled={saving || completionPercentage < 30}
>
{saving ? 'Saving...' : 'Save Strategy'}
</Button>
</Box>
</Box>
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{/* Success Alert */}
{!error && currentStrategy && (
<Alert severity="success" sx={{ mb: 3 }}>
Strategy "{currentStrategy.name}" created successfully! Check the Strategic Intelligence tab for detailed insights.
</Alert>
)}
{/* Strategy Display */}
{currentStrategy && (
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h5" gutterBottom>
Created Strategy: {currentStrategy.name}
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="subtitle1" color="text.secondary">
Industry: {currentStrategy.industry}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Completion: {currentStrategy.completion_percentage}%
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="subtitle1" color="text.secondary">
Created: {new Date(currentStrategy.created_at).toLocaleDateString()}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
ID: {currentStrategy.id}
</Typography>
</Grid>
</Grid>
<Box sx={{ mt: 2 }}>
<Button
variant="outlined"
onClick={() => window.location.href = '/content-planning?tab=strategic-intelligence'}
startIcon={<AssessmentIcon />}
>
View Strategic Intelligence
</Button>
</Box>
</Paper>
)}
{/* Auto-population Status */}
{autoPopulatedFields && Object.keys(autoPopulatedFields).length > 0 && (
<Alert
severity="info"
sx={{ mb: 3 }}
action={
<Button color="inherit" size="small" onClick={autoPopulateFromOnboarding}>
<RefreshIcon />
</Button>
}
>
{autoPopulatedFields && Object.keys(autoPopulatedFields).length} fields auto-populated from onboarding data
</Alert>
)}
<Grid container spacing={3}>
{/* Main Strategy Builder */}
<Grid item xs={12} md={8}>
<Paper sx={{ p: 3 }}>
{/* Progress Indicator */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Strategy Completion
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(calculateCompletionPercentage?.() || 0)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={calculateCompletionPercentage?.() || 0}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Stepper */}
<Stepper activeStep={currentStep} orientation="vertical">
{disclosureSteps.map((step, index) => (
<Step key={step.id} completed={completedSteps.includes(step.id)}>
<StepLabel
icon={
<Badge
badgeContent={step.fields.length}
color={getStepColor(step.id)}
sx={{ '& .MuiBadge-badge': { fontSize: '0.75rem' } }}
>
{getStepIcon(step.id)}
</Badge>
}
optional={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{completedSteps.includes(step.id) && (
<CheckCircleIcon color="success" fontSize="small" />
)}
<Chip
label={`${step.fields.length} fields`}
size="small"
variant="outlined"
/>
</Box>
}
>
<Typography variant="h6">{step.title}</Typography>
<Typography variant="body2" color="text.secondary">
{step.description}
</Typography>
</StepLabel>
<StepContent>
<Box sx={{ mt: 2 }}>
{/* Step Fields */}
<Grid container spacing={2}>
{step.fields.map((fieldId) => (
<Grid item xs={12} key={fieldId}>
<StrategicInputField
fieldId={fieldId}
value={formData[fieldId]}
error={formErrors[fieldId]}
autoPopulated={!!autoPopulatedFields[fieldId]}
dataSource={dataSources[fieldId]}
onChange={(value: any) => updateFormField(fieldId, value)}
onValidate={() => validateFormField(fieldId)}
onShowTooltip={() => setShowTooltip(fieldId)}
/>
</Grid>
))}
</Grid>
{/* Step Actions */}
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
variant="contained"
onClick={handleStepComplete}
disabled={!step.fields.every(fieldId => formData[fieldId])}
endIcon={<ArrowForwardIcon />}
>
{getNextStep() ? 'Complete & Continue' : 'Complete Strategy'}
</Button>
{getPreviousStep() && (
<Button
variant="outlined"
onClick={handlePreviousStep}
startIcon={<ArrowBackIcon />}
>
Previous Step
</Button>
)}
{getNextStep() && (
<Button
variant="outlined"
onClick={handleNextStep}
disabled={!canProceedToStep(getNextStep()!.id)}
endIcon={<ArrowForwardIcon />}
>
Skip to Next
</Button>
)}
</Box>
</Box>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
</Grid>
{/* Sidebar */}
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Data Source Transparency */}
<DataSourceTransparency
autoPopulatedFields={autoPopulatedFields}
dataSources={dataSources}
/>
{/* AI Recommendations Panel */}
<AIRecommendationsPanel
aiGenerating={aiGenerating}
onGenerateRecommendations={handleCreateStrategy}
/>
{/* Quick Actions */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Quick Actions
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={resetForm}
disabled={loading}
>
Reset Form
</Button>
<Button
variant="outlined"
size="small"
onClick={autoPopulateFromOnboarding}
disabled={loading}
>
Re-populate from Onboarding
</Button>
</Box>
</CardContent>
</Card>
</Box>
</Grid>
</Grid>
{/* Enhanced Tooltip */}
{showTooltip && (
<EnhancedTooltip
fieldId={showTooltip}
open={!!showTooltip}
onClose={() => setShowTooltip(null)}
/>
)}
</Box>
);
};
export default EnhancedStrategyBuilder;

View File

@@ -0,0 +1,288 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Chip,
Divider,
List,
ListItem,
ListItemIcon,
ListItemText,
Card,
CardContent,
Alert,
LinearProgress
} from '@mui/material';
import {
Help as HelpIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
AutoAwesome as AutoAwesomeIcon,
DataUsage as DataUsageIcon,
Close as CloseIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore } from '../../../stores/enhancedStrategyStore';
interface EnhancedTooltipProps {
fieldId: string;
open: boolean;
onClose: () => void;
}
const EnhancedTooltip: React.FC<EnhancedTooltipProps> = ({
fieldId,
open,
onClose
}) => {
const { getTooltipData, autoPopulatedFields, dataSources } = useEnhancedStrategyStore();
const tooltipData = getTooltipData(fieldId);
const isAutoPopulated = !!(autoPopulatedFields && autoPopulatedFields[fieldId]);
const dataSource = dataSources && dataSources[fieldId];
// Early return if no tooltip data
if (!tooltipData) {
return null;
}
const getFieldExamples = (fieldId: string) => {
const examples: Record<string, string[]> = {
business_objectives: [
'Primary: Increase brand awareness by 40%',
'Secondary: Generate 500 qualified leads per month',
'Secondary: Improve customer engagement by 25%'
],
target_metrics: [
'Traffic: 50% increase in organic traffic',
'Engagement: 3.5+ average time on page',
'Conversions: 15% improvement in conversion rate'
],
content_budget: [
'Monthly budget: $5,000 for content creation',
'Annual budget: $60,000 including tools and team',
'Per-piece budget: $500 average per content piece'
],
team_size: [
'Small team: 1-2 content creators',
'Medium team: 3-5 content creators + manager',
'Large team: 6+ creators, editors, and strategists'
],
content_preferences: [
'Formats: Blog posts, videos, infographics',
'Topics: Technology trends, industry insights',
'Tone: Professional but approachable'
],
preferred_formats: [
'Blog Posts: 40% of content mix',
'Videos: 30% of content mix',
'Infographics: 20% of content mix',
'Webinars: 10% of content mix'
],
content_frequency: [
'Daily: For news and trending topics',
'Weekly: For in-depth analysis pieces',
'Bi-weekly: For comprehensive guides',
'Monthly: For thought leadership content'
]
};
return examples[fieldId] || [
'Example 1: Provide specific, measurable examples',
'Example 2: Include both qualitative and quantitative data',
'Example 3: Align with your business objectives'
];
};
const getBestPractices = (fieldId: string) => {
const practices: Record<string, string[]> = {
business_objectives: [
'Make objectives SMART (Specific, Measurable, Achievable, Relevant, Time-bound)',
'Align with overall business goals',
'Include both primary and secondary objectives',
'Set realistic but ambitious targets'
],
target_metrics: [
'Choose metrics that directly impact business outcomes',
'Include leading and lagging indicators',
'Set baseline measurements before starting',
'Track metrics consistently over time'
],
content_preferences: [
'Base preferences on audience research and analytics',
'Consider your team\'s content creation capabilities',
'Balance audience preferences with business goals',
'Test different formats to find what works best'
],
preferred_formats: [
'Choose formats that align with your audience\'s consumption habits',
'Consider your team\'s expertise and resources',
'Mix different formats to reach different audience segments',
'Prioritize formats that drive your target metrics'
],
content_frequency: [
'Set realistic frequency based on team capacity',
'Consider your audience\'s content consumption patterns',
'Balance quality with quantity',
'Allow flexibility for trending topics and opportunities'
]
};
return practices[fieldId] || [
'Research your audience thoroughly before making decisions',
'Test and iterate based on performance data',
'Align all decisions with your business objectives',
'Consider your team\'s capabilities and resources'
];
};
const examples = getFieldExamples(fieldId);
const bestPractices = getBestPractices(fieldId);
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: 2,
maxHeight: '80vh'
}
}}
>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<HelpIcon color="primary" />
<Typography variant="h6">
{tooltipData.title}
</Typography>
{isAutoPopulated && (
<Chip
icon={<AutoAwesomeIcon />}
label="Auto-populated"
color="info"
size="small"
/>
)}
</Box>
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Description */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Description
</Typography>
<Typography variant="body2" color="text.secondary">
{tooltipData.description}
</Typography>
</Box>
{/* Data Source Information */}
{isAutoPopulated && dataSource && (
<Alert severity="info" icon={<DataUsageIcon />}>
<Typography variant="body2">
This field was automatically populated from your onboarding data ({dataSource}).
You can modify this value if needed.
</Typography>
</Alert>
)}
{/* Examples */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Examples
</Typography>
<List dense>
{examples.map((example, index) => (
<ListItem key={index} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={example}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
))}
</List>
</Box>
<Divider />
{/* Best Practices */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Best Practices
</Typography>
<List dense>
{bestPractices.map((practice, index) => (
<ListItem key={index} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<LightbulbIcon color="primary" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={practice}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
))}
</List>
</Box>
{/* Field Importance */}
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" gutterBottom>
Why This Matters
</Typography>
<Typography variant="body2" color="text.secondary">
This information helps create a more targeted and effective content strategy.
The more accurate and detailed your inputs, the better our AI can generate
personalized recommendations for your specific situation.
</Typography>
</CardContent>
</Card>
{/* Confidence Level */}
{tooltipData.confidence_level && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Data Confidence
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<LinearProgress
variant="determinate"
value={tooltipData.confidence_level * 100}
sx={{ flexGrow: 1, height: 8, borderRadius: 4 }}
/>
<Typography variant="body2" color="text.secondary">
{Math.round(tooltipData.confidence_level * 100)}%
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
Confidence level based on data quality and source reliability
</Typography>
</Box>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} startIcon={<CloseIcon />}>
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default EnhancedTooltip;

View File

@@ -0,0 +1,85 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Chip,
Typography,
CircularProgress
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Warning as WarningIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const HealthCheck: React.FC = () => {
const [healthStatus, setHealthStatus] = useState<{
api: boolean;
database: boolean;
loading: boolean;
}>({
api: false,
database: false,
loading: true
});
const { checkHealth, checkDatabaseHealth } = useContentPlanningStore();
useEffect(() => {
const checkBackendHealth = async () => {
try {
const [apiHealthy, dbHealthy] = await Promise.all([
checkHealth(),
checkDatabaseHealth()
]);
setHealthStatus({
api: apiHealthy,
database: dbHealthy,
loading: false
});
} catch (error) {
console.error('Health check failed:', error);
setHealthStatus({
api: false,
database: false,
loading: false
});
}
};
checkBackendHealth();
}, [checkHealth, checkDatabaseHealth]);
if (healthStatus.loading) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="caption">Checking backend...</Typography>
</Box>
);
}
const allHealthy = healthStatus.api && healthStatus.database;
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
icon={allHealthy ? <CheckCircleIcon /> : <WarningIcon />}
label={allHealthy ? 'Connected' : 'Disconnected'}
color={allHealthy ? 'success' : 'warning'}
size="small"
variant="outlined"
/>
{!allHealthy && (
<Typography variant="caption" color="text.secondary">
{!healthStatus.api && !healthStatus.database && 'API & DB'}
{!healthStatus.api && healthStatus.database && 'API'}
{healthStatus.api && !healthStatus.database && 'DB'}
</Typography>
)}
</Box>
);
};
export default HealthCheck;

View File

@@ -0,0 +1,251 @@
import React from 'react';
import {
Box,
Paper,
Typography,
LinearProgress,
Chip,
IconButton,
Collapse,
List,
ListItem,
ListItemIcon,
ListItemText,
Alert,
Button
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Psychology as StrategyIcon,
Search as SearchIcon,
Analytics as AnalyticsIcon,
CalendarToday as CalendarIcon,
HealthAndSafety as HealthIcon
} from '@mui/icons-material';
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
interface ProgressIndicatorProps {
serviceStatuses: ServiceStatus[];
onRefreshService: (serviceName: string) => void;
expanded?: boolean;
onToggleExpanded?: () => void;
}
const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({
serviceStatuses,
onRefreshService,
expanded = false,
onToggleExpanded
}) => {
const getServiceIcon = (serviceName: string) => {
switch (serviceName) {
case 'Content Strategies':
return <StrategyIcon />;
case 'Gap Analysis':
return <SearchIcon />;
case 'AI Analytics':
return <AnalyticsIcon />;
case 'Calendar Events':
return <CalendarIcon />;
case 'System Health':
return <HealthIcon />;
default:
return <AnalyticsIcon />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'success':
return 'success';
case 'error':
return 'error';
case 'loading':
return 'primary';
default:
return 'primary';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'success':
return <CheckCircleIcon color="success" />;
case 'error':
return <ErrorIcon color="error" />;
case 'loading':
return <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />;
default:
return null;
}
};
const isLoading = serviceStatuses.some(status => status.status === 'loading');
const hasErrors = serviceStatuses.some(status => status.status === 'error');
const allComplete = serviceStatuses.every(status => status.status === 'success');
const overallProgress = serviceStatuses.reduce((acc, status) => acc + status.progress, 0) / serviceStatuses.length;
return (
<Paper
elevation={2}
sx={{
p: 2,
mb: 2,
border: hasErrors ? '1px solid #f44336' : '1px solid transparent',
backgroundColor: hasErrors ? 'rgba(244, 67, 54, 0.05)' : 'background.paper',
'@keyframes spin': {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' }
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isLoading && <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />}
Content Planning Progress
{allComplete && <CheckCircleIcon color="success" />}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`${Math.round(overallProgress)}%`}
color={allComplete ? 'success' : isLoading ? 'primary' : 'default'}
size="small"
/>
{onToggleExpanded && (
<IconButton size="small" onClick={onToggleExpanded}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
)}
</Box>
</Box>
{/* Overall Progress Bar */}
<Box sx={{ mb: 2 }}>
<LinearProgress
variant="determinate"
value={overallProgress}
color={allComplete ? 'success' : isLoading ? 'primary' : 'inherit'}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Status Messages */}
{isLoading && (
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
Loading content planning data... This may take a few moments as we analyze your content strategy.
</Typography>
</Alert>
)}
{hasErrors && (
<Alert severity="error" sx={{ mb: 2 }}>
<Typography variant="body2">
Some services encountered errors. You can refresh individual services below.
</Typography>
</Alert>
)}
{allComplete && (
<Alert severity="success" sx={{ mb: 2 }}>
<Typography variant="body2">
All content planning services are ready! Your dashboard is fully loaded.
</Typography>
</Alert>
)}
{/* Detailed Service Status */}
<Collapse in={expanded}>
<List dense>
{serviceStatuses.map((status, index) => (
<ListItem
key={index}
sx={{
border: '1px solid',
borderColor: getStatusColor(status.status) === 'error' ? 'error.main' : 'divider',
borderRadius: 1,
mb: 1,
backgroundColor: getStatusColor(status.status) === 'error' ? 'rgba(244, 67, 54, 0.05)' : 'transparent'
}}
>
<ListItemIcon>
{getServiceIcon(status.name)}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2" fontWeight="medium">
{status.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(status.status)}
<Chip
label={`${status.progress}%`}
size="small"
color={getStatusColor(status.status)}
variant="outlined"
/>
</Box>
</Box>
}
secondary={
<Box sx={{ mt: 1 }}>
<Typography variant="caption" color="text.secondary">
{status.message}
</Typography>
{status.error && (
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5 }}>
Error: {status.error}
</Typography>
)}
<Box sx={{ mt: 1 }}>
<LinearProgress
variant="determinate"
value={status.progress}
color={getStatusColor(status.status)}
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
</Box>
}
/>
{status.status === 'error' && (
<IconButton
size="small"
onClick={() => onRefreshService(status.name.toLowerCase().replace(' ', ''))}
color="primary"
>
<RefreshIcon />
</IconButton>
)}
</ListItem>
))}
</List>
</Collapse>
{/* Quick Actions */}
{hasErrors && (
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={() => serviceStatuses.forEach(status => {
if (status.status === 'error') {
onRefreshService(status.name.toLowerCase().replace(' ', ''));
}
})}
>
Refresh All Failed Services
</Button>
</Box>
)}
</Paper>
);
};
export default ProgressIndicator;

View File

@@ -0,0 +1,137 @@
import React from 'react';
import {
Box,
Paper,
Typography,
LinearProgress,
IconButton,
Chip,
Collapse,
Alert
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Warning as WarningIcon
} from '@mui/icons-material';
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
interface ServiceStatusPanelProps {
serviceStatuses: ServiceStatus[];
onRefreshService: (serviceName: string) => void;
expanded: boolean;
onToggleExpanded: () => void;
}
const ServiceStatusPanel: React.FC<ServiceStatusPanelProps> = ({
serviceStatuses,
onRefreshService,
expanded,
onToggleExpanded
}) => {
const getStatusColor = (status: string) => {
switch (status) {
case 'success': return 'success';
case 'error': return 'error';
case 'loading': return 'primary';
default: return 'primary';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'success': return <CheckCircleIcon fontSize="small" />;
case 'error': return <ErrorIcon fontSize="small" />;
case 'loading': return <WarningIcon fontSize="small" />;
default: return null;
}
};
const getOverallStatus = () => {
const hasErrors = serviceStatuses.some(s => s.status === 'error');
const hasLoading = serviceStatuses.some(s => s.status === 'loading');
const allSuccess = serviceStatuses.every(s => s.status === 'success');
if (hasErrors) return { status: 'error', text: 'Some services failed' };
if (hasLoading) return { status: 'loading', text: 'Services loading' };
if (allSuccess) return { status: 'success', text: 'All services operational' };
return { status: 'idle', text: 'Services idle' };
};
const overallStatus = getOverallStatus();
return (
<Paper sx={{ mb: 2 }}>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(overallStatus.status)}
<Typography variant="subtitle2">
System Status: {overallStatus.text}
</Typography>
<Chip
label={`${serviceStatuses.filter(s => s.status === 'success').length}/${serviceStatuses.length}`}
size="small"
color={getStatusColor(overallStatus.status)}
variant="outlined"
/>
</Box>
<IconButton size="small" onClick={onToggleExpanded}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
</Box>
<Collapse in={expanded}>
<Box sx={{ p: 2 }}>
{serviceStatuses.map((service) => (
<Box key={service.name} sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(service.status)}
<Typography variant="body2" fontWeight="medium">
{service.name}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary">
{service.progress}%
</Typography>
<IconButton
size="small"
onClick={() => onRefreshService(service.name.toLowerCase().replace(/\s+/g, ''))}
disabled={service.status === 'loading'}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={service.progress}
color={getStatusColor(service.status)}
sx={{ mb: 1 }}
/>
<Typography variant="caption" color="text.secondary">
{service.message}
</Typography>
{service.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{service.error}
</Alert>
)}
</Box>
))}
</Box>
</Collapse>
</Paper>
);
};
export default ServiceStatusPanel;

View File

@@ -0,0 +1,524 @@
import React, { useState } from 'react';
import {
Box,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Switch,
Chip,
IconButton,
Tooltip,
Typography,
Alert,
Autocomplete,
InputAdornment
} from '@mui/material';
import {
Help as HelpIcon,
AutoAwesome as AutoAwesomeIcon,
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
Edit as EditIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore } from '../../../stores/enhancedStrategyStore';
interface StrategicInputFieldProps {
fieldId: string;
value: any;
error?: string;
autoPopulated?: boolean;
dataSource?: string;
onChange: (value: any) => void;
onValidate: () => boolean;
onShowTooltip: () => void;
}
// Define proper types for field configurations
interface BaseFieldConfig {
type: string;
label: string;
required: boolean;
}
interface TextFieldConfig extends BaseFieldConfig {
type: 'text' | 'number' | 'json';
placeholder: string;
}
interface SelectFieldConfig extends BaseFieldConfig {
type: 'select';
options: string[];
}
interface MultiSelectFieldConfig extends BaseFieldConfig {
type: 'multiselect';
options: string[];
placeholder?: string;
}
interface BooleanFieldConfig extends BaseFieldConfig {
type: 'boolean';
}
type FieldConfig = TextFieldConfig | SelectFieldConfig | MultiSelectFieldConfig | BooleanFieldConfig;
const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
fieldId,
value,
error,
autoPopulated = false,
dataSource,
onChange,
onValidate,
onShowTooltip
}) => {
const { getTooltipData } = useEnhancedStrategyStore();
const [isEditing, setIsEditing] = useState(false);
// Get field configuration from store with proper null checking
const tooltipData = getTooltipData(fieldId);
// Field configuration mapping (this would come from the store)
const fieldConfig: Record<string, FieldConfig> = {
business_objectives: {
type: 'json',
label: 'Business Objectives',
placeholder: 'Enter your primary and secondary business goals',
required: true
},
target_metrics: {
type: 'json',
label: 'Target Metrics',
placeholder: 'Define your KPIs and success metrics',
required: true
},
content_budget: {
type: 'number',
label: 'Content Budget',
placeholder: 'Enter your content budget',
required: false
},
team_size: {
type: 'number',
label: 'Team Size',
placeholder: 'Enter team size',
required: false
},
implementation_timeline: {
type: 'select',
label: 'Implementation Timeline',
options: ['3 months', '6 months', '1 year', '2 years', 'Ongoing'],
required: false
},
market_share: {
type: 'text',
label: 'Market Share',
placeholder: 'Enter market share percentage',
required: false
},
competitive_position: {
type: 'select',
label: 'Competitive Position',
options: ['Leader', 'Challenger', 'Niche', 'Emerging'],
required: false
},
performance_metrics: {
type: 'json',
label: 'Current Performance Metrics',
placeholder: 'Enter current performance data',
required: false
},
content_preferences: {
type: 'json',
label: 'Content Preferences',
placeholder: 'Define content preferences',
required: true
},
consumption_patterns: {
type: 'json',
label: 'Consumption Patterns',
placeholder: 'Describe consumption patterns',
required: false
},
audience_pain_points: {
type: 'json',
label: 'Audience Pain Points',
placeholder: 'List audience pain points',
required: false
},
buying_journey: {
type: 'json',
label: 'Buying Journey',
placeholder: 'Define buying journey stages',
required: false
},
seasonal_trends: {
type: 'json',
label: 'Seasonal Trends',
placeholder: 'Describe seasonal content patterns',
required: false
},
engagement_metrics: {
type: 'json',
label: 'Engagement Metrics',
placeholder: 'Define engagement tracking metrics',
required: false
},
top_competitors: {
type: 'json',
label: 'Top Competitors',
placeholder: 'List your main competitors',
required: false
},
competitor_content_strategies: {
type: 'json',
label: 'Competitor Content Strategies',
placeholder: 'Analyze competitor content approaches',
required: false
},
market_gaps: {
type: 'json',
label: 'Market Gaps',
placeholder: 'Identify content gaps in the market',
required: false
},
industry_trends: {
type: 'json',
label: 'Industry Trends',
placeholder: 'Describe relevant industry trends',
required: false
},
emerging_trends: {
type: 'json',
label: 'Emerging Trends',
placeholder: 'Identify emerging content trends',
required: false
},
preferred_formats: {
type: 'json',
label: 'Preferred Formats',
placeholder: 'Define preferred content formats',
required: false
},
content_mix: {
type: 'json',
label: 'Content Mix',
placeholder: 'Define your content mix strategy',
required: false
},
content_frequency: {
type: 'select',
label: 'Content Frequency',
options: ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly'],
required: false
},
optimal_timing: {
type: 'json',
label: 'Optimal Timing',
placeholder: 'Define optimal posting times',
required: false
},
quality_metrics: {
type: 'json',
label: 'Quality Metrics',
placeholder: 'Define content quality standards',
required: false
},
editorial_guidelines: {
type: 'json',
label: 'Editorial Guidelines',
placeholder: 'Define editorial guidelines',
required: false
},
brand_voice: {
type: 'json',
label: 'Brand Voice',
placeholder: 'Define your brand voice',
required: false
},
traffic_sources: {
type: 'json',
label: 'Traffic Sources',
placeholder: 'Define your traffic sources',
required: false
},
conversion_rates: {
type: 'json',
label: 'Conversion Rates',
placeholder: 'Define target conversion rates',
required: false
},
content_roi_targets: {
type: 'json',
label: 'Content ROI Targets',
placeholder: 'Define ROI targets for content',
required: false
},
ab_testing_capabilities: {
type: 'boolean',
label: 'A/B Testing Capabilities',
required: false
}
};
// Get the field configuration with fallback
const config = fieldConfig[fieldId] || {
type: 'text',
label: fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
placeholder: `Enter ${fieldId.replace(/_/g, ' ')}`,
required: false
};
const handleChange = (newValue: any) => {
onChange(newValue);
if (autoPopulated && !isEditing) {
setIsEditing(true);
}
};
const renderInput = () => {
// Safety check for config
if (!config) {
return (
<TextField
fullWidth
label={fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={`Enter ${fieldId.replace(/_/g, ' ')}`}
error={!!error}
helperText={error}
required={false}
/>
);
}
switch (config.type) {
case 'text':
return (
<TextField
fullWidth
label={config.label || fieldId}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
case 'number':
return (
<TextField
fullWidth
type="number"
label={config.label || fieldId}
value={value || ''}
onChange={(e) => handleChange(Number(e.target.value))}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
case 'select':
const selectConfig = config as SelectFieldConfig;
return (
<FormControl fullWidth error={!!error} required={config.required || false}>
<InputLabel>{config.label || fieldId}</InputLabel>
<Select
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
label={config.label || fieldId}
endAdornment={
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
}
>
{(selectConfig.options || []).map((option: string) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
);
case 'multiselect':
const multiSelectConfig = config as MultiSelectFieldConfig;
return (
<Autocomplete
multiple
options={multiSelectConfig.options || []}
value={Array.isArray(value) ? value : []}
onChange={(_, newValue) => handleChange(newValue)}
renderInput={(params) => (
<TextField
{...params}
label={config.label || fieldId}
placeholder={multiSelectConfig.placeholder || `Select ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
...params.InputProps,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
)}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
label={option}
{...getTagProps({ index })}
key={option}
/>
))
}
/>
);
case 'boolean':
return (
<FormControlLabel
control={
<Switch
checked={!!value}
onChange={(e) => handleChange(e.target.checked)}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{config.label || fieldId}
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</Box>
}
/>
);
case 'json':
return (
<TextField
fullWidth
multiline
rows={3}
label={config.label || fieldId}
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
handleChange(parsed);
} catch {
handleChange(e.target.value);
}
}}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId} as JSON`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
default:
return (
<TextField
fullWidth
label={(config as any).label || fieldId}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={`Enter ${fieldId}`}
error={!!error}
helperText={error}
required={(config as any).required || false}
/>
);
}
};
return (
<Box sx={{ position: 'relative' }}>
{/* Auto-population indicator */}
{autoPopulated && (
<Box sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
icon={<AutoAwesomeIcon />}
label={`Auto-populated from ${dataSource}`}
color="info"
size="small"
variant="outlined"
/>
{!isEditing && (
<Tooltip title="Edit auto-populated value">
<IconButton size="small" onClick={() => setIsEditing(true)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
)}
{/* Field input */}
{renderInput()}
{/* Validation status */}
{value && !error && (
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleIcon color="success" fontSize="small" />
<Typography variant="caption" color="success.main">
Valid
</Typography>
</Box>
)}
{/* Error display */}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
<Typography variant="body2">{error}</Typography>
</Alert>
)}
</Box>
);
};
export default StrategicInputField;

View File

@@ -0,0 +1,390 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Card,
CardContent,
Chip,
Divider,
Alert,
CircularProgress,
LinearProgress
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
Analytics as AnalyticsIcon,
ShowChart as ShowChartIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AnalyticsTab: React.FC = () => {
const {
performanceMetrics,
aiInsights,
loading,
error,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [analyticsData, setAnalyticsData] = useState<any>(null);
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadAnalyticsData();
}, []);
const loadAnalyticsData = async () => {
try {
setDataLoading(true);
console.log('Loading analytics data...');
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
// Load analytics data from backend
const response = await contentPlanningApi.getAIAnalyticsSafe();
console.log('Analytics Response:', response);
if (response) {
const analyticsData = {
performance_trends: response.performance_trends || {},
content_evolution: response.content_evolution || {},
engagement_patterns: response.engagement_patterns || {},
recommendations: response.recommendations || [],
insights: response.insights || []
};
console.log('Analytics Data:', analyticsData);
setAnalyticsData(analyticsData);
}
} catch (error) {
console.error('Error loading analytics data:', error);
} finally {
setDataLoading(false);
}
};
const getPerformanceColor = (value: number) => {
if (value >= 80) return 'success';
if (value >= 60) return 'warning';
return 'error';
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Performance Analytics
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{/* Performance Overview */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Overview
</Typography>
<Divider sx={{ mb: 2 }} />
{performanceMetrics ? (
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement Rate
</Typography>
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.engagement)}>
{performanceMetrics.engagement}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Reach
</Typography>
<Typography variant="h4" color="primary">
{performanceMetrics.reach.toLocaleString()}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Conversion Rate
</Typography>
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.conversion)}>
{performanceMetrics.conversion}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
ROI
</Typography>
<Typography variant="h4" color="success.main">
${performanceMetrics.roi.toLocaleString()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary">
No performance data available
</Typography>
)}
</Paper>
</Grid>
{/* AI Insights */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
AI Insights
</Typography>
<Divider sx={{ mb: 2 }} />
{aiInsights && aiInsights.length > 0 ? (
<Box>
{aiInsights.slice(0, 3).map((insight, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{insight.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{insight.description}
</Typography>
<Chip
label={insight.priority}
color={insight.priority === 'high' ? 'error' : insight.priority === 'medium' ? 'warning' : 'success'}
size="small"
/>
</Box>
))}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No AI insights available
</Typography>
)}
</Paper>
</Grid>
{/* Content Evolution */}
{analyticsData && analyticsData.content_evolution && (
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<ShowChartIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Evolution
</Typography>
<Divider sx={{ mb: 2 }} />
{analyticsData.content_evolution.content_types ? (
<Box>
{analyticsData.content_evolution.content_types.map((contentType: string, index: number) => {
const performance = analyticsData.content_evolution.performance_by_type?.[contentType];
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1" sx={{ textTransform: 'capitalize' }}>
{contentType.replace('_', ' ')}
</Typography>
{performance && (
<Grid container spacing={1}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Growth
</Typography>
<Typography variant="h6" color="success.main">
+{performance.growth}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement
</Typography>
<Typography variant="h6">
{performance.engagement}%
</Typography>
</Grid>
</Grid>
)}
</Box>
);
})}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No content evolution data available
</Typography>
)}
</Paper>
</Grid>
)}
{/* Performance Trends */}
{analyticsData && analyticsData.performance_trends && (
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Trends
</Typography>
<Divider sx={{ mb: 2 }} />
{analyticsData.performance_trends.engagement_trend ? (
<Box>
<Typography variant="subtitle2" gutterBottom>
Engagement Trend (Last 5 periods)
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{analyticsData.performance_trends.engagement_trend.map((value: number, index: number) => (
<Box key={index} sx={{ flex: 1, textAlign: 'center' }}>
<Typography variant="h6" color="primary">
{value}%
</Typography>
<Typography variant="caption" color="text.secondary">
Period {index + 1}
</Typography>
</Box>
))}
</Box>
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
)}
</Paper>
</Grid>
)}
{/* Engagement Patterns */}
{analyticsData && analyticsData.engagement_patterns && (
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Engagement Patterns
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={3}>
{analyticsData.engagement_patterns.peak_times && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Peak Engagement Times
</Typography>
{analyticsData.engagement_patterns.peak_times.map((time: string, index: number) => (
<Chip
key={index}
label={time}
color="primary"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
{analyticsData.engagement_patterns.best_days && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Best Performing Days
</Typography>
{analyticsData.engagement_patterns.best_days.map((day: string, index: number) => (
<Chip
key={index}
label={day}
color="success"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
{analyticsData.engagement_patterns.audience_segments && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Top Audience Segments
</Typography>
{analyticsData.engagement_patterns.audience_segments.map((segment: string, index: number) => (
<Chip
key={index}
label={segment.replace('_', ' ')}
color="secondary"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
</Grid>
</Paper>
</Grid>
)}
{/* Recommendations */}
{analyticsData && analyticsData.recommendations && analyticsData.recommendations.length > 0 && (
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
AI Recommendations
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{analyticsData.recommendations.map((recommendation: any, index: number) => (
<Grid item xs={12} md={6} key={index}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle1" gutterBottom>
{recommendation.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{recommendation.description}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={recommendation.type}
color="primary"
size="small"
/>
<Chip
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
color="success"
size="small"
/>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
</Grid>
)}
</Grid>
)}
</Box>
);
};
export default AnalyticsTab;

View File

@@ -0,0 +1,597 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
CardActions,
Chip,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
CircularProgress,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
List,
ListItem,
ListItemText,
ListItemIcon,
Divider,
LinearProgress,
Tooltip,
Badge
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
CalendarToday as CalendarIcon,
Event as EventIcon,
Refresh as RefreshIcon,
AutoAwesome as AIIcon,
TrendingUp as TrendingIcon,
ContentCopy as RepurposeIcon,
Analytics as AnalyticsIcon,
ExpandMore as ExpandMoreIcon,
Schedule as ScheduleIcon,
Psychology as PsychologyIcon,
Business as BusinessIcon,
Group as GroupIcon,
Timeline as TimelineIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Info as InfoIcon,
DataUsage as DataUsageIcon,
Insights as InsightsIcon,
Assessment as AssessmentIcon,
Campaign as CampaignIcon,
Speed as SpeedIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`calendar-tabpanel-${index}`}
aria-labelledby={`calendar-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
const CalendarTab: React.FC = () => {
const {
calendarEvents,
createEvent,
updateEvent,
deleteEvent,
loading,
error,
loadCalendarEvents,
updateCalendarEvents,
// New calendar generation state
generatedCalendar,
contentOptimization,
performancePrediction,
contentRepurposing,
trendingTopics,
aiInsights,
calendarGenerationError,
dataLoading
} = useContentPlanningStore();
const [tabValue, setTabValue] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const [eventForm, setEventForm] = useState({
title: '',
description: '',
content_type: '',
platform: '',
scheduled_date: '',
status: 'draft' as 'draft' | 'scheduled' | 'published'
});
// Enhanced state for data transparency
const [userData, setUserData] = useState<any>({
onboardingData: {},
gapAnalysis: {},
strategyData: {},
recommendationsData: [],
performanceData: {},
aiAnalysisResults: []
});
const [calendarGenerationMode, setCalendarGenerationMode] = useState<'transparency' | 'wizard'>('transparency');
useEffect(() => {
loadCalendarData();
}, []);
const loadCalendarData = async () => {
try {
// Load comprehensive user data for calendar generation
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1); // Pass user ID
setUserData(comprehensiveData.data); // Extract the data from the response
// Load existing calendar events
await loadCalendarEvents();
} catch (error) {
console.error('Error loading calendar data:', error);
}
};
const handleOpenDialog = (event?: any) => {
if (event) {
setSelectedEvent(event);
setEventForm({
title: event.title,
description: event.description,
content_type: event.content_type,
platform: event.platform,
scheduled_date: event.scheduled_date || event.date,
status: event.status as 'draft' | 'scheduled' | 'published'
});
} else {
setSelectedEvent(null);
setEventForm({
title: '',
description: '',
content_type: '',
platform: '',
scheduled_date: '',
status: 'draft' as 'draft' | 'scheduled' | 'published'
});
}
setDialogOpen(true);
};
const handleCloseDialog = () => {
setDialogOpen(false);
setSelectedEvent(null);
};
const handleSaveEvent = async () => {
try {
const eventData = {
title: eventForm.title,
description: eventForm.description,
content_type: eventForm.content_type,
platform: eventForm.platform,
date: eventForm.scheduled_date, // Map scheduled_date to date for API compatibility
status: eventForm.status as 'draft' | 'scheduled' | 'published'
};
if (selectedEvent) {
await updateEvent(selectedEvent.id, eventData);
} else {
await createEvent(eventData);
}
handleCloseDialog();
} catch (error) {
console.error('Error saving event:', error);
}
};
const handleDeleteEvent = async (eventId: string) => {
try {
await deleteEvent(eventId);
} catch (error) {
console.error('Error deleting event:', error);
}
};
const handleRefreshData = async () => {
await loadCalendarData();
};
const handleGenerateAICalendar = async () => {
try {
// This will now use the comprehensive data from the transparency dashboard
const calendarConfig = {
userData,
calendarType: 'monthly',
industry: userData.onboardingData?.industry || 'technology',
businessSize: 'sme'
};
await contentPlanningApi.generateComprehensiveCalendar(calendarConfig);
} catch (error) {
console.error('Error generating AI calendar:', error);
}
};
const handleDataUpdate = (updatedData: any) => {
setUserData((prev: any) => ({ ...prev, ...updatedData }));
};
const handleGenerateCalendar = async (calendarConfig: any) => {
try {
await contentPlanningApi.generateComprehensiveCalendar({
...calendarConfig,
userData
});
} catch (error) {
console.error('Error generating calendar:', error);
}
};
const handleOptimizeContent = async (contentData: any) => {
try {
await contentPlanningApi.optimizeContent(contentData);
} catch (error) {
console.error('Error optimizing content:', error);
}
};
const handlePredictPerformance = async (contentData: any) => {
try {
await contentPlanningApi.predictPerformance(contentData);
} catch (error) {
console.error('Error predicting performance:', error);
}
};
const handleGetTrendingTopics = async () => {
try {
await contentPlanningApi.getTrendingTopics({ user_id: 1, industry: 'technology' });
} catch (error) {
console.error('Error getting trending topics:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'draft': return 'default';
case 'scheduled': return 'warning';
case 'published': return 'success';
default: return 'default';
}
};
// Ensure calendarEvents is always an array
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4">
Content Calendar
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRefreshData}
disabled={dataLoading}
>
Refresh
</Button>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenDialog()}
>
Add Event
</Button>
</Box>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{calendarGenerationError && (
<Alert severity="error" sx={{ mb: 2 }}>
{calendarGenerationError}
</Alert>
)}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
<Tab label="Calendar Wizard" icon={<AIIcon />} iconPosition="start" />
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
{/* Calendar Events Tab */}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<CalendarIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Scheduled Events
</Typography>
{safeCalendarEvents.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<EventIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No events scheduled
</Typography>
<Typography variant="body2" color="text.secondary">
Create your first content event to get started
</Typography>
</Box>
) : (
<Grid container spacing={2}>
{safeCalendarEvents.map((event) => (
<Grid item xs={12} md={6} lg={4} key={event.id}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Typography variant="h6" component="div">
{event.title}
</Typography>
<Box>
<IconButton
size="small"
onClick={() => handleOpenDialog(event)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => handleDeleteEvent(event.id)}
>
<DeleteIcon />
</IconButton>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{event.description}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
<Chip
label={event.platform}
size="small"
variant="outlined"
/>
<Chip
label={event.content_type}
size="small"
variant="outlined"
/>
<Chip
label={event.status}
size="small"
color={getStatusColor(event.status)}
/>
</Box>
<Typography variant="caption" color="text.secondary">
Scheduled: {new Date(event.scheduled_date || event.date || '').toLocaleDateString()}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
</Grid>
</Grid>
)}
</TabPanel>
<TabPanel value={tabValue} index={1}>
{/* Calendar Generation Wizard with Data Transparency */}
<CalendarGenerationWizard
userData={userData}
onGenerateCalendar={handleGenerateCalendar}
loading={loading}
/>
</TabPanel>
<TabPanel value={tabValue} index={2}>
{/* Content Optimizer Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Optimization
</Typography>
{contentOptimization ? (
<Box>
<Typography variant="body1" gutterBottom>
Optimization Recommendations
</Typography>
<List>
{contentOptimization.recommendations?.map((rec: any, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<LightbulbIcon color="primary" />
</ListItemIcon>
<ListItemText
primary={rec.title}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No optimization data
</Typography>
<Typography variant="body2" color="text.secondary">
Generate content optimization recommendations
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={tabValue} index={3}>
{/* Trending Topics Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Trending Topics
</Typography>
{trendingTopics ? (
<Box>
<Typography variant="body1" gutterBottom>
Current Trending Topics
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
<Chip
key={index}
label={topic.name || topic.keyword}
color="primary"
variant="outlined"
/>
))}
</Box>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No trending topics
</Typography>
<Typography variant="body2" color="text.secondary">
Get trending topics for your industry
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
{/* Event Dialog */}
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{selectedEvent ? 'Edit Event' : 'Add New Event'}
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<TextField
label="Title"
value={eventForm.title}
onChange={(e) => setEventForm({ ...eventForm, title: e.target.value })}
fullWidth
/>
<TextField
label="Description"
value={eventForm.description}
onChange={(e) => setEventForm({ ...eventForm, description: e.target.value })}
multiline
rows={3}
fullWidth
/>
<FormControl fullWidth>
<InputLabel>Content Type</InputLabel>
<Select
value={eventForm.content_type}
onChange={(e) => setEventForm({ ...eventForm, content_type: e.target.value })}
label="Content Type"
>
<MenuItem value="blog_post">Blog Post</MenuItem>
<MenuItem value="video">Video</MenuItem>
<MenuItem value="social_post">Social Post</MenuItem>
<MenuItem value="case_study">Case Study</MenuItem>
<MenuItem value="whitepaper">Whitepaper</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Platform</InputLabel>
<Select
value={eventForm.platform}
onChange={(e) => setEventForm({ ...eventForm, platform: e.target.value })}
label="Platform"
>
<MenuItem value="website">Website</MenuItem>
<MenuItem value="linkedin">LinkedIn</MenuItem>
<MenuItem value="twitter">Twitter</MenuItem>
<MenuItem value="instagram">Instagram</MenuItem>
<MenuItem value="youtube">YouTube</MenuItem>
</Select>
</FormControl>
<TextField
label="Scheduled Date"
type="datetime-local"
value={eventForm.scheduled_date}
onChange={(e) => setEventForm({ ...eventForm, scheduled_date: e.target.value })}
fullWidth
InputLabelProps={{ shrink: true }}
/>
<FormControl fullWidth>
<InputLabel>Status</InputLabel>
<Select
value={eventForm.status}
onChange={(e) => setEventForm({ ...eventForm, status: e.target.value as 'draft' | 'scheduled' | 'published' })}
label="Status"
>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="scheduled">Scheduled</MenuItem>
<MenuItem value="published">Published</MenuItem>
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Cancel</Button>
<Button onClick={handleSaveEvent} variant="contained">
{selectedEvent ? 'Update' : 'Create'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default CalendarTab;

View File

@@ -0,0 +1,953 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
CardActions,
Chip,
Divider,
Alert,
List,
ListItem,
ListItemText,
ListItemIcon,
LinearProgress,
CircularProgress,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
IconButton,
Tooltip,
Badge
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
Business as BusinessIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Search as SearchIcon,
Analytics as AnalyticsIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
ExpandMore as ExpandMoreIcon,
Refresh as RefreshIcon,
Add as AddIcon,
Edit as EditIcon,
Visibility as VisibilityIcon,
BarChart as BarChartIcon,
PieChart as PieChartIcon,
ShowChart as ShowChartIcon,
AutoAwesome as AutoAwesomeIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import EnhancedStrategyBuilder from '../components/EnhancedStrategyBuilder';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`strategy-tabpanel-${index}`}
aria-labelledby={`strategy-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
const ContentStrategyTab: React.FC = () => {
const {
strategies,
currentStrategy,
aiInsights,
aiRecommendations,
performanceMetrics,
loading,
error,
loadStrategies,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [tabValue, setTabValue] = useState(0);
const [strategyForm, setStrategyForm] = useState({
name: '',
description: '',
industry: '',
target_audience: '',
content_pillars: []
});
// Real data states
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
const [keywordResearch, setKeywordResearch] = useState<any>(null);
const [contentPillars, setContentPillars] = useState<any[]>([]);
const [dataLoading, setDataLoading] = useState({
strategies: false,
insights: false,
recommendations: false,
strategicIntelligence: false,
keywordResearch: false,
pillars: false
});
// Load data on component mount
useEffect(() => {
loadInitialData();
}, []);
const loadInitialData = async () => {
try {
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true, keywordResearch: true, pillars: true });
// Load strategies
await loadStrategies();
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
// Load strategic intelligence
await loadStrategicIntelligence();
// Load keyword research
await loadKeywordResearch();
// Load content pillars
await loadContentPillars();
} catch (error) {
console.error('Error loading initial data:', error);
} finally {
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false, keywordResearch: false, pillars: false });
}
};
const loadStrategicIntelligence = async () => {
try {
setDataLoading(prev => ({ ...prev, strategicIntelligence: true }));
// Use streaming endpoint for real-time updates
const eventSource = await contentPlanningApi.streamStrategicIntelligence(1);
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Strategic Intelligence SSE Data:', data);
if (data.type === 'status') {
// Update loading message
console.log('Status:', data.message);
} else if (data.type === 'progress') {
// Update progress (could be used for progress bar)
console.log('Progress:', data.progress, '%');
} else if (data.type === 'result' && data.status === 'success') {
// Set the strategic intelligence data
setStrategicIntelligence(data.data);
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
} else if (data.type === 'error') {
console.error('Strategic Intelligence Error:', data.message);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
},
(error) => {
console.error('Strategic Intelligence SSE Error:', error);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
);
} catch (error) {
console.error('Error loading strategic intelligence:', error);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
};
const loadKeywordResearch = async () => {
try {
setDataLoading(prev => ({ ...prev, keywordResearch: true }));
// Use streaming endpoint for real-time updates
const eventSource = await contentPlanningApi.streamKeywordResearch(1);
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Keyword Research SSE Data:', data);
if (data.type === 'status') {
// Update loading message
console.log('Status:', data.message);
} else if (data.type === 'progress') {
// Update progress (could be used for progress bar)
console.log('Progress:', data.progress, '%');
} else if (data.type === 'result' && data.status === 'success') {
// Set the keyword research data
setKeywordResearch(data.data);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
} else if (data.type === 'error') {
console.error('Keyword Research Error:', data.message);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
},
(error) => {
console.error('Keyword Research SSE Error:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
);
} catch (error) {
console.error('Error loading keyword research:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
};
const loadContentPillars = async () => {
try {
setDataLoading(prev => ({ ...prev, pillars: true }));
// Get content pillars from current strategy
if (currentStrategy && currentStrategy.content_pillars) {
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
name: pillar.name || `Pillar ${index + 1}`,
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
}));
setContentPillars(pillars);
} else {
// Default pillars if no strategy exists
setContentPillars([
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
]);
}
} catch (error) {
console.error('Error loading content pillars:', error);
} finally {
setDataLoading(prev => ({ ...prev, pillars: false }));
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleStrategyFormChange = (field: string, value: string) => {
setStrategyForm(prev => ({
...prev,
[field]: value
}));
};
const handleCreateStrategy = async () => {
if (!strategyForm.name || !strategyForm.description) {
return;
}
try {
// Call backend API to create strategy
await contentPlanningApi.createStrategy({
name: strategyForm.name,
description: strategyForm.description,
industry: strategyForm.industry,
target_audience: strategyForm.target_audience,
content_pillars: strategyForm.content_pillars
});
// Reload data after creating strategy
await loadInitialData();
// Reset form
setStrategyForm({
name: '',
description: '',
industry: '',
target_audience: '',
content_pillars: []
});
} catch (error) {
console.error('Error creating strategy:', error);
}
};
const handleRefreshData = async () => {
await loadInitialData();
};
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" gutterBottom>
Content Strategy Builder
</Typography>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRefreshData}
disabled={loading}
>
Refresh Data
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* Strategy Builder Tabs */}
<Paper sx={{ width: '100%', mb: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy builder tabs">
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AutoAwesomeIcon />
Enhanced Strategy Builder
</Box>
}
/>
<Tab label="Legacy Strategy Builder" />
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
<Tab label="Keyword Research" icon={<SearchIcon />} />
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
<Tab label="Content Pillars" icon={<PieChartIcon />} />
</Tabs>
</Box>
{/* Enhanced Strategy Builder Tab */}
<TabPanel value={tabValue} index={0}>
<EnhancedStrategyBuilder />
</TabPanel>
{/* Legacy Strategy Builder Tab */}
<TabPanel value={tabValue} index={1}>
<Grid container spacing={3}>
{/* Strategy Overview */}
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Strategy Overview
</Typography>
<Divider sx={{ mb: 2 }} />
<TextField
fullWidth
label="Strategy Name"
value={strategyForm.name}
onChange={(e) => handleStrategyFormChange('name', e.target.value)}
placeholder="Enter strategy name"
sx={{ mb: 2 }}
/>
<TextField
fullWidth
multiline
rows={3}
label="Strategy Description"
value={strategyForm.description}
onChange={(e) => handleStrategyFormChange('description', e.target.value)}
placeholder="Describe your content strategy"
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Industry"
value={strategyForm.industry}
onChange={(e) => handleStrategyFormChange('industry', e.target.value)}
placeholder="e.g., Technology, Healthcare, Finance"
sx={{ mb: 2 }}
/>
<Button
variant="contained"
fullWidth
startIcon={<AddIcon />}
disabled={loading}
onClick={handleCreateStrategy}
>
{loading ? 'Creating...' : 'Create Strategy'}
</Button>
</Paper>
{/* Performance Metrics */}
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Metrics
</Typography>
<Divider sx={{ mb: 2 }} />
{performanceMetrics ? (
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement Rate
</Typography>
<Typography variant="h6" color="primary">
{performanceMetrics.engagement || 75.2}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Reach
</Typography>
<Typography variant="h6" color="primary">
{(performanceMetrics.reach || 12500).toLocaleString()}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Conversion Rate
</Typography>
<Typography variant="h6" color="success.main">
{performanceMetrics.conversion || 3.8}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
ROI
</Typography>
<Typography variant="h6" color="success.main">
${(performanceMetrics.roi || 14200).toLocaleString()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary">
No performance data available
</Typography>
)}
</Paper>
</Grid>
{/* Main Content Area */}
<Grid item xs={12} md={8}>
<Paper sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy tabs">
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
<Tab label="Keyword Research" icon={<SearchIcon />} />
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
<Tab label="Content Pillars" icon={<PieChartIcon />} />
</Tabs>
</Box>
{/* Strategic Intelligence Tab */}
<TabPanel value={tabValue} index={2}>
{dataLoading.strategicIntelligence ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : strategicIntelligence && strategicIntelligence.market_positioning ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Market Positioning
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CircularProgress
variant="determinate"
value={strategicIntelligence.market_positioning.score || 0}
size={60}
color="primary"
/>
<Typography variant="h4" sx={{ ml: 2 }}>
{strategicIntelligence.market_positioning.score || 0}/100
</Typography>
</Box>
<Typography variant="subtitle2" gutterBottom>
Strengths:
</Typography>
<List dense>
{(strategicIntelligence.market_positioning.strengths || []).map((strength: string, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary={strength} />
</ListItem>
))}
</List>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Competitive Advantages
</Typography>
{(strategicIntelligence.competitive_advantages || []).map((advantage: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{advantage.advantage}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
<Chip
label={advantage.impact}
color={advantage.impact === 'High' ? 'success' : 'primary'}
size="small"
/>
<Chip
label={advantage.implementation}
variant="outlined"
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Strategic Risks
</Typography>
{(strategicIntelligence.strategic_risks || []).map((risk: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{risk.risk}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
<Chip
label={`Probability: ${risk.probability}`}
color={risk.probability === 'High' ? 'error' : 'warning'}
size="small"
/>
<Chip
label={`Impact: ${risk.impact}`}
color={risk.impact === 'High' ? 'error' : 'warning'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No strategic intelligence data available
</Typography>
)}
</TabPanel>
{/* Keyword Research Tab */}
<TabPanel value={tabValue} index={3}>
{dataLoading.keywordResearch ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : keywordResearch && keywordResearch.trend_analysis ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
High Volume Keywords
</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Volume</TableCell>
<TableCell>Difficulty</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
<TableRow key={index}>
<TableCell>{keyword.keyword}</TableCell>
<TableCell>{keyword.volume}</TableCell>
<TableCell>
<Chip
label={keyword.difficulty}
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Trending Keywords
</Typography>
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{keyword.keyword}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={keyword.growth}
color="success"
size="small"
/>
<Chip
label={keyword.opportunity}
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Keyword Opportunities
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Search Volume</TableCell>
<TableCell>Competition</TableCell>
<TableCell>CPC</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
<TableRow key={index}>
<TableCell>{opportunity.keyword}</TableCell>
<TableCell>{opportunity.search_volume}</TableCell>
<TableCell>
<Chip
label={opportunity.competition}
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
<TableCell>${opportunity.cpc}</TableCell>
<TableCell>
<Button size="small" variant="outlined">
Add to Strategy
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No keyword research data available
</Typography>
)}
</TabPanel>
{/* Performance Analytics Tab */}
<TabPanel value={tabValue} index={4}>
{performanceMetrics ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Performance by Type
</Typography>
<Typography variant="body2" color="text.secondary">
No content performance data available
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Growth Trends
</Typography>
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No performance analytics data available
</Typography>
)}
</TabPanel>
{/* Content Pillars Tab */}
<TabPanel value={tabValue} index={5}>
{dataLoading.pillars ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : contentPillars.length > 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Content Pillars Overview
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
</Typography>
</Grid>
{contentPillars.map((pillar, index) => (
<Grid item xs={12} md={6} key={index}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{pillar.name}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Content Count
</Typography>
<Typography variant="h6">
{pillar.content_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Avg. Engagement
</Typography>
<Typography variant="h6">
{pillar.avg_engagement}%
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
Performance Score
</Typography>
<Typography variant="h6" color="success.main">
{pillar.performance_score}/100
</Typography>
</Box>
</CardContent>
<CardActions>
<Button size="small">View Content</Button>
<Button size="small">Optimize</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No content pillars data available
</Typography>
)}
</TabPanel>
</Paper>
</Grid>
</Grid>
</TabPanel>
{/* Strategic Intelligence Tab */}
<TabPanel value={tabValue} index={2}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Keyword Research Tab */}
<TabPanel value={tabValue} index={3}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Performance Analytics Tab */}
<TabPanel value={tabValue} index={4}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Content Pillars Tab */}
<TabPanel value={tabValue} index={5}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
</Paper>
</Box>
);
};
export default ContentStrategyTab;

View File

@@ -0,0 +1,406 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
Chip,
Divider,
Alert,
CircularProgress,
List,
ListItem,
ListItemText,
ListItemIcon
} from '@mui/material';
import {
Search as SearchIcon,
Add as AddIcon,
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
TrendingUp as TrendingUpIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const GapAnalysisTab: React.FC = () => {
const {
gapAnalyses,
loading,
error,
loadGapAnalyses,
analyzeContentGaps,
updateGapAnalyses
} = useContentPlanningStore();
const [analysisForm, setAnalysisForm] = useState({
website_url: '',
competitors: [] as string[],
keywords: [] as string[]
});
const [newCompetitor, setNewCompetitor] = useState('');
const [newKeyword, setNewKeyword] = useState('');
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadGapAnalysisData();
}, []);
const loadGapAnalysisData = async () => {
try {
setDataLoading(true);
const response = await contentPlanningApi.getGapAnalysesSafe();
console.log('Gap Analysis Response:', response);
// Transform the backend response to match frontend expectations
if (response && response.gap_analyses) {
const transformedAnalyses = response.gap_analyses.map((analysis: any, index: number) => ({
id: analysis.id || `analysis_${index}`,
website_url: analysis.website_url || 'example.com',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
gaps: analysis.gaps || [],
recommendations: analysis.recommendations || [],
created_at: analysis.created_at || new Date().toISOString()
}));
console.log('Transformed Analyses:', transformedAnalyses);
// Update the store with transformed data
updateGapAnalyses(transformedAnalyses);
} else {
console.log('No gap analyses found in response');
updateGapAnalyses([]);
}
} catch (error) {
console.error('Error loading gap analysis data:', error);
updateGapAnalyses([]);
} finally {
setDataLoading(false);
}
};
const handleAddCompetitor = () => {
if (newCompetitor.trim() && !analysisForm.competitors.includes(newCompetitor.trim())) {
setAnalysisForm(prev => ({
...prev,
competitors: [...prev.competitors, newCompetitor.trim()]
}));
setNewCompetitor('');
}
};
const handleRemoveCompetitor = (competitorToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
competitors: prev.competitors.filter(comp => comp !== competitorToRemove)
}));
};
const handleAddKeyword = () => {
if (newKeyword.trim() && !analysisForm.keywords.includes(newKeyword.trim())) {
setAnalysisForm(prev => ({
...prev,
keywords: [...prev.keywords, newKeyword.trim()]
}));
setNewKeyword('');
}
};
const handleRemoveKeyword = (keywordToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
keywords: prev.keywords.filter(keyword => keyword !== keywordToRemove)
}));
};
const handleRunAnalysis = async () => {
if (!analysisForm.website_url) {
return;
}
try {
setDataLoading(true);
await analyzeContentGaps({
website_url: analysisForm.website_url,
competitors: analysisForm.competitors,
keywords: analysisForm.keywords
});
// Reload data after analysis
await loadGapAnalyses();
// Reset form
setAnalysisForm({
website_url: '',
competitors: [],
keywords: []
});
} catch (error) {
console.error('Error running gap analysis:', error);
} finally {
setDataLoading(false);
}
};
// Ensure gapAnalyses is always an array and transform the data structure
const safeGapAnalyses = Array.isArray(gapAnalyses) ? gapAnalyses : [];
// Transform backend data structure to frontend expected structure
const transformedGapAnalyses = safeGapAnalyses.map((analysis, index) => {
// Handle the actual backend structure: { recommendations: [...] }
const recommendations = analysis.recommendations || [];
return {
id: analysis.id || `analysis-${index}`,
website_url: analysis.website_url || 'Unknown Website',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
recommendations: recommendations,
created_at: analysis.created_at || new Date().toISOString(),
// Extract gaps from recommendations if available
gaps: recommendations.length > 0 ?
recommendations.filter((rec: any) => rec.type === 'gap').map((rec: any) => rec.title || rec.description || 'Content gap identified') :
[]
};
});
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Content Gap Analysis
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Grid container spacing={3}>
{/* Analysis Setup */}
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<SearchIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Analysis Setup
</Typography>
<Divider sx={{ mb: 2 }} />
<TextField
fullWidth
label="Website URL"
value={analysisForm.website_url}
onChange={(e) => setAnalysisForm(prev => ({ ...prev, website_url: e.target.value }))}
placeholder="https://example.com"
sx={{ mb: 2 }}
/>
<Typography variant="subtitle2" gutterBottom>
Competitors
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Competitor"
value={newCompetitor}
onChange={(e) => setNewCompetitor(e.target.value)}
placeholder="competitor.com"
onKeyPress={(e) => e.key === 'Enter' && handleAddCompetitor()}
/>
<Button
variant="outlined"
onClick={handleAddCompetitor}
disabled={!newCompetitor.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.competitors.map((competitor, index) => (
<Chip
key={index}
label={competitor}
onDelete={() => handleRemoveCompetitor(competitor)}
color="primary"
variant="outlined"
/>
))}
</Box>
<Typography variant="subtitle2" gutterBottom>
Keywords
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Keyword"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
placeholder="target keyword"
onKeyPress={(e) => e.key === 'Enter' && handleAddKeyword()}
/>
<Button
variant="outlined"
onClick={handleAddKeyword}
disabled={!newKeyword.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.keywords.map((keyword, index) => (
<Chip
key={index}
label={keyword}
onDelete={() => handleRemoveKeyword(keyword)}
color="secondary"
variant="outlined"
/>
))}
</Box>
<Button
variant="contained"
fullWidth
onClick={handleRunAnalysis}
disabled={loading || dataLoading || !analysisForm.website_url}
startIcon={<AssessmentIcon />}
>
{loading || dataLoading ? 'Running Analysis...' : 'Run Gap Analysis'}
</Button>
</Paper>
</Grid>
{/* Content Gaps */}
<Grid item xs={12} md={8}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<WarningIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Gaps
</Typography>
<Divider sx={{ mb: 2 }} />
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : transformedGapAnalyses.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
No previous analyses found. Run your first analysis to see results here.
</Typography>
) : (
<Grid container spacing={2}>
{transformedGapAnalyses.map((analysis) => (
<Grid item xs={12} md={6} lg={4} key={analysis.id}>
<Card>
<CardContent>
<Typography variant="h6" component="div">
{analysis.website_url}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{new Date(analysis.created_at).toLocaleDateString()}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Chip
label={`${analysis.competitors?.length || 0} competitors`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.keywords?.length || 0} keywords`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.gaps?.length || 0} gaps found`}
size="small"
color="warning"
/>
<Chip
label={`${analysis.recommendations?.length || 0} recommendations`}
size="small"
color="success"
/>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
{/* Detailed Analysis Results */}
{transformedGapAnalyses.length > 0 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Detailed Analysis Results
</Typography>
<Divider sx={{ mb: 2 }} />
{transformedGapAnalyses.map((analysis, index) => (
<Box key={index} sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom>
Analysis for {analysis.website_url}
</Typography>
{analysis.gaps && analysis.gaps.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Identified Content Gaps:
</Typography>
<List dense>
{analysis.gaps.map((gap, gapIndex) => (
<ListItem key={gapIndex}>
<ListItemIcon>
<WarningIcon color="warning" />
</ListItemIcon>
<ListItemText primary={gap} />
</ListItem>
))}
</List>
</Box>
)}
{analysis.recommendations && analysis.recommendations.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Recommendations:
</Typography>
<List dense>
{analysis.recommendations.map((rec, recIndex) => (
<ListItem key={recIndex}>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText
primary={rec.title || rec.description || 'Recommendation'}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
)}
</Box>
))}
</Paper>
)}
</Grid>
</Grid>
</Box>
);
};
export default GapAnalysisTab;

View File

@@ -0,0 +1,148 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import {
Box,
Typography,
Button,
Paper,
Alert,
AlertTitle
} from '@mui/material';
import { Refresh, BugReport, Home } from '@mui/icons-material';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
errorInfo?: ErrorInfo;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.setState({ error, errorInfo });
// Log error to monitoring service (e.g., Sentry)
// logErrorToService(error, errorInfo);
}
handleRetry = () => {
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
p: 3,
}}
>
<Paper
elevation={24}
sx={{
maxWidth: 500,
p: 4,
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.2)',
}}
>
<Alert severity="error" sx={{ mb: 3 }}>
<AlertTitle>Something went wrong</AlertTitle>
We encountered an unexpected error. Please try again.
</Alert>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600 }}>
Oops! Something went wrong
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
We're sorry, but something unexpected happened. Our team has been notified and is working to fix this issue.
</Typography>
{process.env.NODE_ENV === 'development' && this.state.error && (
<Box sx={{ mb: 3, p: 2, bgcolor: 'rgba(0,0,0,0.05)', borderRadius: 1 }}>
<Typography variant="body2" fontFamily="monospace" sx={{ fontSize: '0.75rem' }}>
{this.state.error.toString()}
</Typography>
</Box>
)}
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button
variant="contained"
onClick={this.handleRetry}
startIcon={<Refresh />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
}
}}
>
Try Again
</Button>
<Button
variant="outlined"
onClick={this.handleGoHome}
startIcon={<Home />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
}}
>
Go Home
</Button>
<Button
variant="text"
startIcon={<BugReport />}
onClick={() => {
// Open support ticket or contact form
window.open('mailto:support@alwrity.com?subject=Error Report', '_blank');
}}
sx={{
textTransform: 'none',
fontWeight: 600,
}}
>
Report Issue
</Button>
</Box>
</Paper>
</Box>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Box, Typography, Paper, Button } from '@mui/material';
const MainApp: React.FC = () => {
return (
<Box sx={{ p: 3 }}>
<Paper elevation={3} sx={{ p: 4, maxWidth: 800, margin: 'auto' }}>
<Typography variant="h4" align="center" gutterBottom>
Welcome to Alwrity! 🚀
</Typography>
<Typography variant="body1" align="center" sx={{ mb: 3 }}>
Your onboarding is complete. The main application is ready to use.
</Typography>
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
Available Features:
</Typography>
<Typography variant="body2" component="ul" sx={{ pl: 2 }}>
<li>AI Content Writers (Blog, Social Media, Email, etc.)</li>
<li>SEO Tools and Analytics</li>
<li>Website Analysis</li>
<li>Content Calendar</li>
<li>Research Tools</li>
<li>And much more...</li>
</Typography>
</Box>
<Box sx={{ mt: 4, textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary">
This is where the main Alwrity application will be implemented.
All existing functionality will be migrated here.
</Typography>
</Box>
</Paper>
</Box>
);
};
export default MainApp;

View File

@@ -0,0 +1,227 @@
import React from 'react';
import {
Box,
Container,
Grid,
Alert,
Snackbar,
useTheme,
useMediaQuery
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
// Shared components
import DashboardHeader from '../shared/DashboardHeader';
import SearchFilter from '../shared/SearchFilter';
import ToolCard from '../shared/ToolCard';
import CategoryHeader from '../shared/CategoryHeader';
import LoadingSkeleton from '../shared/LoadingSkeleton';
import ErrorDisplay from '../shared/ErrorDisplay';
import EmptyState from '../shared/EmptyState';
// Shared types and utilities
import { Tool, Category } from '../shared/types';
import { getFilteredCategories, getToolsForCategory } from '../shared/utils';
// Zustand store
import { useDashboardStore } from '../../stores/dashboardStore';
// Data
import { toolCategories } from '../../data/toolCategories';
// Main dashboard component
const MainDashboard: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Zustand store hooks
const {
loading,
error,
searchQuery,
selectedCategory,
selectedSubCategory,
favorites,
snackbar,
toggleFavorite,
setSearchQuery,
setSelectedCategory,
setSelectedSubCategory,
setError,
setLoading,
showSnackbar,
hideSnackbar,
clearFilters,
} = useDashboardStore();
const handleToolClick = (tool: Tool) => {
console.log('Navigating to tool:', tool.path);
// Handle SEO Dashboard navigation
if (tool.path === '/seo-dashboard') {
window.location.href = '/seo-dashboard';
return;
}
// Handle Content Planning Dashboard navigation
if (tool.path === '/content-planning') {
window.location.href = '/content-planning';
return;
}
showSnackbar(`Launching ${tool.name}...`, 'info');
};
const filteredCategories = getFilteredCategories(
toolCategories,
selectedCategory,
searchQuery
);
if (loading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorDisplay error={error} />;
}
return (
<Box
sx={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
padding: theme.spacing(4),
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'url("data:image/svg+xml,%3Csvg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.03"%3E%3Ccircle cx="40" cy="40" r="3"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")',
pointerEvents: 'none',
},
'&::after': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
width: '600px',
height: '600px',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: 0,
},
}}
>
<Container maxWidth="xl" sx={{ position: 'relative', zIndex: 1 }}>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Dashboard Header */}
<DashboardHeader
title="🚀 Alwrity Content Hub"
subtitle="Your AI-powered content creation suite"
statusChips={[
{
label: 'Active',
color: '#4CAF50',
icon: <span></span>,
},
{
label: 'Premium',
color: '#FFD700',
icon: <span></span>,
},
]}
/>
{/* Search and Filter */}
<SearchFilter
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
onClearSearch={() => setSearchQuery('')}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
selectedSubCategory={selectedSubCategory}
onSubCategoryChange={setSelectedSubCategory}
toolCategories={toolCategories}
theme={theme}
/>
{/* Enhanced Tools Grid */}
<Box sx={{ mb: 4 }}>
{Object.entries(filteredCategories).map(([categoryName, category], categoryIndex) => (
<motion.div
key={categoryName}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
>
<Box sx={{ mb: 5 }}>
{/* Category Header */}
<CategoryHeader
categoryName={categoryName}
category={category}
theme={theme}
/>
<Grid container spacing={3}>
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={tool.name}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: (categoryIndex * 0.1) + (toolIndex * 0.05) }}
>
<ToolCard
tool={tool}
onToolClick={handleToolClick}
isFavorite={favorites.includes(tool.name)}
onToggleFavorite={toggleFavorite}
/>
</motion.div>
</Grid>
))}
</Grid>
</Box>
</motion.div>
))}
</Box>
{/* Empty State */}
{Object.keys(filteredCategories).length === 0 && (
<EmptyState
icon={<span>🔍</span>}
title="No tools found matching your criteria"
message="Try adjusting your search or category filter"
onClearFilters={clearFilters}
clearButtonText="Clear Filters"
/>
)}
</motion.div>
</AnimatePresence>
{/* Enhanced Snackbar for notifications */}
<Snackbar
open={snackbar.open}
autoHideDuration={3000}
onClose={hideSnackbar}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<Alert onClose={hideSnackbar} severity={snackbar.severity} sx={{ width: '100%' }}>
{snackbar.message}
</Alert>
</Snackbar>
</Container>
</Box>
);
};
export default MainDashboard;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { Box, Typography, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import {
Accessibility,
Keyboard,
Visibility,
Hearing,
TouchApp
} from '@mui/icons-material';
const AccessibilityGuide: React.FC = () => {
return (
<Box sx={{ p: 3, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Accessibility />
Accessibility Features
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<Keyboard />
</ListItemIcon>
<ListItemText
primary="Keyboard Navigation"
secondary="Use Tab, Enter, and Arrow keys to navigate"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<Visibility />
</ListItemIcon>
<ListItemText
primary="High Contrast"
secondary="All text meets WCAG contrast requirements"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<Hearing />
</ListItemIcon>
<ListItemText
primary="Screen Reader Support"
secondary="ARIA labels and semantic HTML structure"
/>
</ListItem>
<ListItem>
<ListItemIcon>
<TouchApp />
</ListItemIcon>
<ListItemText
primary="Touch Friendly"
secondary="Large touch targets for mobile devices"
/>
</ListItem>
</List>
</Box>
);
};
export default AccessibilityGuide;

View File

@@ -0,0 +1,741 @@
import React, { useEffect, useState } from 'react';
import {
Box,
TextField,
Typography,
Alert,
Card,
CardContent,
Fade,
Zoom,
Chip,
IconButton,
Collapse,
Divider,
Link,
Container,
Paper,
Grid,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
List,
ListItem,
ListItemIcon,
ListItemText
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error,
Info,
Key,
Security,
HelpOutline,
Warning,
Star,
VerifiedUser,
Lock,
Launch,
Info as InfoIcon
} from '@mui/icons-material';
import { getApiKeys, saveApiKey } from '../../api/onboarding';
import { useOnboardingStyles } from './common/useOnboardingStyles';
import {
validateApiKey,
getKeyStatus,
isFormValid,
debounce,
formatErrorMessage
} from './common/onboardingUtils';
import OnboardingButton from './common/OnboardingButton';
interface ApiKeyStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const ApiKeyStep: React.FC<ApiKeyStepProps> = ({ onContinue, updateHeaderContent }) => {
const [openaiKey, setOpenaiKey] = useState('');
const [geminiKey, setGeminiKey] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
const [showGeminiKey, setShowGeminiKey] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [savedKeys, setSavedKeys] = useState<Record<string, string>>({});
const [benefitsModalOpen, setBenefitsModalOpen] = useState(false);
const [selectedProvider, setSelectedProvider] = useState<any>(null);
const [keysLoaded, setKeysLoaded] = useState(false);
const styles = useOnboardingStyles();
useEffect(() => {
if (!keysLoaded) {
loadExistingKeys();
}
// Update header content when component mounts
updateHeaderContent({
title: 'Connect Your AI Services',
description: 'Alwrity uses AI to generate high-quality, personalized content for your brand. Connect at least one AI service to enable intelligent content creation, style analysis, and automated writing assistance.'
});
}, [updateHeaderContent, keysLoaded]);
const loadExistingKeys = async () => {
if (keysLoaded) return; // Prevent multiple calls
try {
console.log('ApiKeyStep: Loading API keys...');
const keys = await getApiKeys();
setSavedKeys(keys);
if (keys.openai) setOpenaiKey(keys.openai);
if (keys.gemini) setGeminiKey(keys.gemini);
setKeysLoaded(true);
console.log('ApiKeyStep: API keys loaded successfully');
} catch (error) {
console.error('ApiKeyStep: Error loading API keys:', error);
setKeysLoaded(true); // Set to true even on error to prevent infinite retries
}
};
const handleContinue = async () => {
setLoading(true);
setError(null);
setSuccess(null);
try {
const promises = [];
if (openaiKey.trim()) {
promises.push(saveApiKey('openai', openaiKey.trim()));
}
if (geminiKey.trim()) {
promises.push(saveApiKey('gemini', geminiKey.trim()));
}
await Promise.all(promises);
setSuccess('API keys saved successfully!');
await loadExistingKeys();
// Auto-continue after a short delay
setTimeout(() => {
onContinue();
}, 1500);
} catch (err) {
setError(formatErrorMessage(err));
console.error('Error saving API keys:', err);
} finally {
setLoading(false);
}
};
const aiProviders = [
{
name: 'OpenAI',
description: 'Advanced language model for content generation',
benefits: ['High-quality text generation', 'Creative content creation', 'Natural language processing'],
key: openaiKey,
setKey: setOpenaiKey,
showKey: showOpenaiKey,
setShowKey: setShowOpenaiKey,
placeholder: 'sk-...',
status: getKeyStatus(openaiKey, 'openai'),
link: 'https://platform.openai.com/api-keys',
free: false,
recommended: true
},
{
name: 'Google Gemini',
description: 'Google\'s latest AI model for content creation',
benefits: ['Multimodal capabilities', 'Real-time information', 'Google\'s latest technology'],
key: geminiKey,
setKey: setGeminiKey,
showKey: showGeminiKey,
setShowKey: setShowGeminiKey,
placeholder: 'AIza...',
status: getKeyStatus(geminiKey, 'gemini'),
link: 'https://makersuite.google.com/app/apikey',
free: true,
recommended: true
}
];
const hasAtLeastOneKey = openaiKey.trim() || geminiKey.trim();
const isValid = hasAtLeastOneKey;
const handleBenefitsClick = (provider: any) => {
setSelectedProvider(provider);
setBenefitsModalOpen(true);
};
const handleCloseBenefitsModal = () => {
setBenefitsModalOpen(false);
setSelectedProvider(null);
};
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* AI Providers */}
<Box sx={{ mb: 4 }}>
<Grid container spacing={3}>
{aiProviders.map((provider, index) => (
<Grid item xs={12} md={6} key={provider.name}>
<Zoom in={true} timeout={700 + index * 100}>
<Card
sx={{
border: `1px solid ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.2)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.2)'
: 'rgba(0,0,0,0.08)'
}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04)',
transform: 'translateY(-1px)',
borderColor: provider.status === 'valid'
? 'rgba(16, 185, 129, 0.4)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.4)'
: 'rgba(0,0,0,0.12)'
},
position: 'relative',
overflow: 'hidden',
background: 'rgba(255, 255, 255, 0.8)',
backdropFilter: 'blur(10px)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 2,
background: provider.status === 'valid'
? 'linear-gradient(90deg, rgba(16, 185, 129, 0.6) 0%, rgba(5, 150, 105, 0.6) 100%)'
: provider.status === 'invalid'
? 'linear-gradient(90deg, rgba(239, 68, 68, 0.6) 0%, rgba(220, 38, 38, 0.6) 100%)'
: 'linear-gradient(90deg, rgba(107, 114, 128, 0.3) 0%, rgba(75, 85, 99, 0.3) 100%)',
}
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}>
<Key sx={{ color: 'white', fontSize: 20 }} />
</Box>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{
fontWeight: 600,
mb: 0.5,
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '1.125rem'
}}>
{provider.name}
</Typography>
{provider.recommended && (
<Chip
label="Recommended"
color="success"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 20
}}
/>
)}
{provider.free && (
<Chip
label="Free Tier"
color="primary"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 20
}}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
{provider.description}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Benefits Button - Inline with Get Help */}
<Button
variant="text"
onClick={() => handleBenefitsClick(provider)}
startIcon={<InfoIcon />}
sx={{
color: 'primary.main',
fontWeight: 600,
fontSize: '0.75rem',
fontFamily: 'Inter, system-ui, sans-serif',
textTransform: 'none',
padding: '2px 6px',
borderRadius: 1,
minWidth: 'auto',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
transform: 'translateY(-1px)'
}
}}
>
Benefits ({provider.benefits.length})
</Button>
{provider.status === 'valid' && (
<Chip
icon={<CheckCircle />}
label="Valid"
color="success"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24
}}
/>
)}
{provider.status === 'invalid' && (
<Chip
icon={<Error />}
label="Invalid"
color="error"
size="small"
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 24
}}
/>
)}
</Box>
</Box>
{/* Enhanced API Key Input */}
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<Lock sx={{ color: 'text.secondary', mr: 1, fontSize: 16 }} />
),
endAdornment: (
<IconButton
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
size="small"
sx={{
color: 'text.secondary',
'&:hover': {
color: 'primary.main',
background: 'rgba(102, 126, 234, 0.08)'
}
}}
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
border: '1px solid rgba(0,0,0,0.12)',
background: 'rgba(255, 255, 255, 0.8)',
'&:hover': {
borderColor: 'rgba(0,0,0,0.24)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
},
'&.Mui-focused': {
borderColor: provider.status === 'valid'
? 'rgba(16, 185, 129, 0.6)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.6)'
: 'rgba(102, 126, 234, 0.6)',
boxShadow: `0 0 0 2px ${
provider.status === 'valid'
? 'rgba(16, 185, 129, 0.1)'
: provider.status === 'invalid'
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(102, 126, 234, 0.1)'
}, 0 2px 8px rgba(0, 0, 0, 0.08)`,
'& .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
},
'& .MuiInputBase-input': {
padding: '12px 14px',
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem'
}
}}
/>
{/* Enhanced Link with Icon */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Link
href={provider.link}
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
fontWeight: 600,
fontSize: '0.9rem',
color: 'primary.main',
textDecoration: 'none',
fontFamily: 'Inter, system-ui, sans-serif',
padding: '4px 8px',
borderRadius: 1,
transition: 'all 0.2s ease',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
textDecoration: 'none',
transform: 'translateY(-1px)'
}
}}
>
Get API Key
<Launch sx={{ fontSize: 16 }} />
</Link>
</Box>
{savedKeys[provider.name.toLowerCase()] && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="caption" color="success.main" sx={{
fontWeight: 500,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Key already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
</Grid>
))}
</Grid>
</Box>
{/* Description moved below cards */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary" sx={{
mb: 2,
lineHeight: 1.6,
maxWidth: 800,
mx: 'auto',
fontWeight: 500,
opacity: 0.8,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Alwrity uses AI to generate high-quality, personalized content for your brand. Connect at least one AI service to enable intelligent content creation, style analysis, and automated writing assistance.
</Typography>
{/* Get Help Link moved to description area */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<OnboardingButton
variant="text"
onClick={() => setShowHelp(!showHelp)}
icon={<HelpOutline />}
size="small"
>
{showHelp ? 'Hide Help' : 'Get Help'}
</OnboardingButton>
</Box>
</Box>
{/* Benefits Modal */}
<Dialog
open={benefitsModalOpen}
onClose={handleCloseBenefitsModal}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)'
}
}}
>
<DialogTitle sx={{
pb: 1,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600
}}>
{selectedProvider?.name} Benefits
</DialogTitle>
<DialogContent sx={{ pt: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{
mb: 2,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Discover what {selectedProvider?.name} can do for your content creation:
</Typography>
<List sx={{ pt: 0 }}>
{selectedProvider?.benefits.map((benefit: string, index: number) => (
<ListItem key={index} sx={{ px: 0, py: 1 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'primary.main',
flexShrink: 0
}} />
</ListItemIcon>
<ListItemText
primary={benefit}
sx={{
'& .MuiListItemText-primary': {
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 500,
fontSize: '0.875rem'
}
}}
/>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1 }}>
<Button
onClick={handleCloseBenefitsModal}
variant="contained"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}
>
Got it
</Button>
</DialogActions>
</Dialog>
{/* Help Section */}
<Collapse in={showHelp}>
<Zoom in={showHelp} timeout={1600}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 3,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
}}>
<Typography variant="h6" gutterBottom sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 3,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 600
}}>
<HelpOutline color="primary" />
How to Get Your AI API Keys
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
<Star sx={{ color: 'warning.main', fontSize: 20 }} />
Recommended Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
OpenAI
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Visit{' '}
<Link href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
}}>
platform.openai.com
</Link>
, sign up, and create an API key in your account settings.
</Typography>
</Box>
<Box>
<Typography variant="subtitle2" sx={{
fontWeight: 600,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
Google Gemini
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
Visit{' '}
<Link href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener noreferrer" sx={{
fontWeight: 600,
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline'
}
}}>
makersuite.google.com
</Link>
, create an account, and generate an API key.
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<Typography variant="subtitle1" gutterBottom sx={{
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 1,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
<Info sx={{ color: 'info.main', fontSize: 20 }} />
Why AI Services Matter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Content Generation:</strong> Create high-quality, engaging content for your brand.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Style Analysis:</strong> Analyze your brand's voice and tone for consistency.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Automated Writing:</strong> Generate blog posts, social media content, and more.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<strong>Personalization:</strong> Tailor content to your specific audience and goals.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
</Collapse>
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert severity="error" sx={{
mb: 2,
borderRadius: 2,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
{error}
</Alert>
</Fade>
)}
{success && (
<Fade in={true}>
<Alert severity="success" sx={{
mb: 2,
borderRadius: 2,
fontFamily: 'Inter, system-ui, sans-serif'
}}>
{success}
</Alert>
</Fade>
)}
</Box>
{/* Security Notice */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: 400
}}>
<Lock sx={{ fontSize: 14 }} />
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
</Container>
</Fade>
);
};
export default ApiKeyStep;

View File

@@ -0,0 +1,660 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Typography,
Alert,
Paper,
Container,
Fade,
Zoom,
Grid,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Accordion,
AccordionSummary,
AccordionDetails,
Card,
CardContent,
IconButton,
Tooltip,
CircularProgress
} from '@mui/material';
import {
CheckCircle,
Rocket,
Star,
TrendingUp,
Security,
ExpandMore,
Visibility,
VisibilityOff,
Lock,
LockOpen,
Settings,
Web,
Psychology,
Business,
ContentCopy
} from '@mui/icons-material';
import OnboardingButton from './common/OnboardingButton';
import { getApiKeys, completeOnboarding, getOnboardingSummary, getWebsiteAnalysisData, getResearchPreferencesData, setCurrentStep } from '../../api/onboarding';
interface FinalStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
interface OnboardingData {
apiKeys: Record<string, string>;
websiteUrl?: string;
researchPreferences?: any;
personalizationSettings?: any;
integrations?: any;
styleAnalysis?: any;
}
interface Capability {
id: string;
title: string;
description: string;
icon: React.ReactElement;
unlocked: boolean;
required?: string[];
}
const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }) => {
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [onboardingData, setOnboardingData] = useState<OnboardingData>({
apiKeys: {}
});
const [showApiKeys, setShowApiKeys] = useState(false);
const [expandedSection, setExpandedSection] = useState<string | null>('summary');
useEffect(() => {
updateHeaderContent({
title: 'Review & Launch Alwrity 🚀',
description: 'Review your configuration and confirm all settings before launching your AI-powered content creation workspace.'
});
loadOnboardingData();
}, [updateHeaderContent]);
const loadOnboardingData = async () => {
setDataLoading(true);
try {
// Load comprehensive onboarding summary
const summary = await getOnboardingSummary();
// Load individual data sources for detailed information
const websiteAnalysis = await getWebsiteAnalysisData();
const researchPreferences = await getResearchPreferencesData();
setOnboardingData({
apiKeys: summary.api_keys || {},
websiteUrl: websiteAnalysis?.website_url || summary.website_url,
researchPreferences: researchPreferences || summary.research_preferences,
personalizationSettings: summary.personalization_settings,
integrations: summary.integrations || {},
styleAnalysis: websiteAnalysis?.style_analysis || summary.style_analysis
});
} catch (error) {
console.error('Error loading onboarding data:', error);
// Fallback to just API keys if other endpoints fail
try {
const apiKeys = await getApiKeys();
setOnboardingData({
apiKeys,
websiteUrl: undefined,
researchPreferences: undefined,
personalizationSettings: undefined,
integrations: undefined,
styleAnalysis: undefined
});
} catch (fallbackError) {
console.error('Error loading API keys as fallback:', fallbackError);
}
} finally {
setDataLoading(false);
}
};
const handleLaunch = async () => {
setLoading(true);
setError(null);
try {
console.log('FinalStep: Starting onboarding completion...');
// First, complete step 6 (Final Step) to mark it as completed
console.log('FinalStep: Completing step 6...');
await setCurrentStep(6);
console.log('FinalStep: Step 6 completed successfully');
// Then complete the entire onboarding process
console.log('FinalStep: Completing onboarding...');
await completeOnboarding();
console.log('FinalStep: Onboarding completed successfully');
// Navigate directly to dashboard without calling onContinue
// This bypasses the wizard flow and goes straight to the dashboard
console.log('FinalStep: Navigating to dashboard...');
window.location.href = '/dashboard';
} catch (e: any) {
console.error('FinalStep: Error completing onboarding:', e);
// Provide more specific error messages
let errorMessage = 'Failed to complete onboarding. Please try again.';
if (e.response?.data?.detail) {
errorMessage = e.response.data.detail;
} else if (e.message) {
errorMessage = e.message;
}
setError(errorMessage);
}
setLoading(false);
};
const capabilities: Capability[] = [
{
id: 'ai-content',
title: 'AI Content Generation',
description: 'Generate high-quality, personalized content using advanced AI models',
icon: <ContentCopy />,
unlocked: Object.keys(onboardingData.apiKeys).length > 0,
required: ['API Keys']
},
{
id: 'style-analysis',
title: 'Style Analysis',
description: 'Analyze and match your brand\'s writing style and tone',
icon: <Psychology />,
unlocked: !!onboardingData.websiteUrl,
required: ['Website URL']
},
{
id: 'research-tools',
title: 'AI Research Tools',
description: 'Automated research and fact-checking capabilities',
icon: <TrendingUp />,
unlocked: !!onboardingData.researchPreferences,
required: ['Research Configuration']
},
{
id: 'personalization',
title: 'Content Personalization',
description: 'Tailored content based on your brand voice and preferences',
icon: <Settings />,
unlocked: !!onboardingData.personalizationSettings,
required: ['Personalization Settings']
},
{
id: 'integrations',
title: 'Third-party Integrations',
description: 'Connect with external tools and platforms',
icon: <Business />,
unlocked: !!onboardingData.integrations,
required: ['Integration Setup']
}
];
const getConfiguredProviders = () => {
return Object.keys(onboardingData.apiKeys).map(provider => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
configured: true
}));
};
const getMissingRequirements = () => {
const missing = [];
if (Object.keys(onboardingData.apiKeys).length === 0) {
missing.push('At least one AI provider API key');
}
if (!onboardingData.websiteUrl) {
missing.push('Website URL for style analysis');
}
return missing;
};
const unlockedCapabilities = capabilities.filter(cap => cap.unlocked);
const missingRequirements = getMissingRequirements();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Loading State */}
{dataLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
<Box sx={{ textAlign: 'center' }}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6" sx={{ mb: 1 }}>
Loading your configuration...
</Typography>
<Typography variant="body2" color="text.secondary">
Retrieving your onboarding data and settings
</Typography>
</Box>
</Box>
)}
{/* Content - Only show when data is loaded */}
{!dataLoading && (
<React.Fragment>
{/* Summary Section */}
<Zoom in={true} timeout={800}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 3
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 32 }} />
<Typography variant="h4" color="success.main" sx={{ fontWeight: 600 }}>
Setup Summary
</Typography>
</Box>
<Chip
label={`${unlockedCapabilities.length}/${capabilities.length} Capabilities Unlocked`}
color="success"
variant="filled"
icon={<LockOpen />}
/>
</Box>
<Grid container spacing={3}>
{/* Configured Providers */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ background: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<Security sx={{ color: 'primary.main' }} />
AI Providers
</Typography>
<List dense>
{getConfiguredProviders().map((provider, index) => (
<ListItem key={index} sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
</ListItemIcon>
<ListItemText
primary={provider.name}
secondary="API key configured"
/>
</ListItem>
))}
</List>
</CardContent>
</Card>
</Grid>
{/* Quick Stats */}
<Grid item xs={12} md={6}>
<Card elevation={0} sx={{ background: 'rgba(255, 255, 255, 0.7)', borderRadius: 2 }}>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendingUp sx={{ color: 'primary.main' }} />
Quick Stats
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">AI Providers:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{Object.keys(onboardingData.apiKeys).length} configured
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">Capabilities:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{unlockedCapabilities.length} unlocked
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2">Missing:</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: missingRequirements.length > 0 ? 'warning.main' : 'success.main' }}>
{missingRequirements.length} requirements
</Typography>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Detailed Configuration Review */}
<Zoom in={true} timeout={1000}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Settings sx={{ color: 'primary.main' }} />
Configuration Details
</Typography>
<Grid container spacing={3}>
{/* API Keys Section */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'api-keys'}
onChange={() => setExpandedSection(expandedSection === 'api-keys' ? null : 'api-keys')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Security sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
API Keys ({Object.keys(onboardingData.apiKeys).length} configured)
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(onboardingData.apiKeys).map(([provider, key]) => (
<Box key={provider} sx={{
p: 2,
border: '1px solid rgba(0,0,0,0.1)',
borderRadius: 1,
background: 'rgba(255,255,255,0.5)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, textTransform: 'capitalize' }}>
{provider}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title={showApiKeys ? 'Hide key' : 'Show key'}>
<IconButton
size="small"
onClick={() => setShowApiKeys(!showApiKeys)}
>
{showApiKeys ? <VisibilityOff /> : <Visibility />}
</IconButton>
</Tooltip>
</Box>
</Box>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{showApiKeys ? key : '••••••••••••••••••••••••••••••••'}
</Typography>
</Box>
))}
</Box>
</AccordionDetails>
</Accordion>
</Grid>
{/* Website Configuration */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'website'}
onChange={() => setExpandedSection(expandedSection === 'website' ? null : 'website')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Web sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Website Analysis
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.websiteUrl ? (
<Box>
<Typography variant="body2" sx={{ mb: 2 }}>
<strong>URL:</strong> {onboardingData.websiteUrl}
</Typography>
{onboardingData.styleAnalysis && (
<Typography variant="body2" color="success.main">
Style analysis completed
</Typography>
)}
</Box>
) : (
<Typography variant="body2" color="warning.main">
No website URL configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
{/* Research Preferences */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'research'}
onChange={() => setExpandedSection(expandedSection === 'research' ? null : 'research')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<TrendingUp sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Research Configuration
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.researchPreferences ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Depth:</strong> {onboardingData.researchPreferences.research_depth}
</Typography>
<Typography variant="body2">
<strong>Content Types:</strong> {onboardingData.researchPreferences.content_types?.join(', ')}
</Typography>
<Typography variant="body2">
<strong>Auto Research:</strong> {onboardingData.researchPreferences.auto_research ? 'Enabled' : 'Disabled'}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Research preferences not configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
{/* Personalization Settings */}
<Grid item xs={12} md={6}>
<Accordion
expanded={expandedSection === 'personalization'}
onChange={() => setExpandedSection(expandedSection === 'personalization' ? null : 'personalization')}
sx={{ background: 'rgba(255, 255, 255, 0.8)', borderRadius: 2 }}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Psychology sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Personalization
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
{onboardingData.personalizationSettings ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2">
<strong>Style:</strong> {onboardingData.personalizationSettings.writing_style}
</Typography>
<Typography variant="body2">
<strong>Tone:</strong> {onboardingData.personalizationSettings.tone}
</Typography>
<Typography variant="body2">
<strong>Brand Voice:</strong> {onboardingData.personalizationSettings.brand_voice}
</Typography>
</Box>
) : (
<Typography variant="body2" color="warning.main">
Personalization not configured
</Typography>
)}
</AccordionDetails>
</Accordion>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Capabilities Overview */}
<Zoom in={true} timeout={1200}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Star sx={{ color: 'warning.main' }} />
Capabilities Overview
</Typography>
<Grid container spacing={2}>
{capabilities.map((capability) => (
<Grid item xs={12} sm={6} md={4} key={capability.id}>
<Card elevation={0} sx={{
background: capability.unlocked ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.05)',
border: `1px solid ${capability.unlocked ? 'rgba(16, 185, 129, 0.3)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: 2,
opacity: capability.unlocked ? 1 : 0.6
}}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: capability.unlocked
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #6b7280 0%, #4b5563 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
{React.cloneElement(capability.icon, {
sx: { color: 'white', fontSize: 20 }
})}
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
{capability.title}
{capability.unlocked ? (
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
) : (
<Lock sx={{ color: 'text.secondary', fontSize: 16 }} />
)}
</Typography>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{capability.description}
</Typography>
{!capability.unlocked && capability.required && (
<Box>
<Typography variant="caption" color="text.secondary">
Requires: {capability.required.join(', ')}
</Typography>
</Box>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
</Zoom>
{/* Missing Requirements Warning */}
{missingRequirements.length > 0 && (
<Zoom in={true} timeout={1400}>
<Alert
severity="warning"
sx={{ mb: 4, borderRadius: 2 }}
action={
<Button color="inherit" size="small">
Configure Now
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Missing Requirements
</Typography>
<Typography variant="body2">
The following items are recommended for optimal experience: {missingRequirements.join(', ')}
</Typography>
</Alert>
</Zoom>
)}
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert
severity="error"
sx={{ mb: 2, borderRadius: 2 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setError(null)}
>
Dismiss
</Button>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Setup Incomplete
</Typography>
<Typography variant="body2">
{error}
</Typography>
</Alert>
</Fade>
)}
</Box>
{/* Action Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<OnboardingButton
variant="primary"
onClick={handleLaunch}
loading={loading}
size="large"
icon={<Rocket />}
disabled={Object.keys(onboardingData.apiKeys).length === 0}
>
Launch Alwrity & Complete Setup
</OnboardingButton>
</Box>
{/* Help Text */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
This will complete your onboarding and launch Alwrity with your configured settings.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Star sx={{ fontSize: 16 }} />
Ready to create amazing content with AI-powered assistance
</Typography>
</Box>
</React.Fragment>
)}
</Container>
</Fade>
);
};
export default FinalStep;

View File

@@ -0,0 +1,752 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
CircularProgress,
Card,
CardContent,
Grid,
Tabs,
Tab,
Chip,
Divider,
FormControlLabel,
Switch,
Accordion,
AccordionSummary,
AccordionDetails,
IconButton,
Tooltip,
Fade,
Zoom,
Paper,
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemSecondaryAction
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
CheckCircle as CheckIcon,
Error as ErrorIcon,
Info as InfoIcon,
Add as AddIcon,
Settings as SettingsIcon,
Link as LinkIcon,
Launch as LaunchIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
// Social Media Icons
Facebook as FacebookIcon,
Twitter as TwitterIcon,
Instagram as InstagramIcon,
LinkedIn as LinkedInIcon,
YouTube as YouTubeIcon,
VideoLibrary as TikTokIcon, // Using VideoLibrary as alternative for TikTok
Pinterest as PinterestIcon,
// Platform Icons
Web as WordPressIcon, // Using Web as alternative for WordPress
Web as WebIcon,
// AI and Analytics Icons
Analytics as AnalyticsIcon,
AutoAwesome as AutoAwesomeIcon,
TrendingUp as TrendingUpIcon,
Schedule as ScheduleIcon,
ContentPaste as ContentPasteIcon,
SmartToy as SmartToyIcon,
// Status Icons
Warning as WarningIcon,
HelpOutline as HelpOutlineIcon,
Verified as VerifiedIcon,
Close as CloseIcon
} from '@mui/icons-material';
interface IntegrationsStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
interface IntegrationConfig {
id: string;
name: string;
description: string;
icon: React.ReactNode;
category: 'social' | 'platform' | 'analytics';
apiKeyField: string;
apiKeyPlaceholder: string;
setupUrl: string;
features: string[];
isConnected: boolean;
apiKey: string;
showApiKey: boolean;
isEnabled: boolean;
status: 'connected' | 'disconnected' | 'error' | 'pending';
}
const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateHeaderContent }) => {
const [activeTab, setActiveTab] = useState(0);
const [integrations, setIntegrations] = useState<IntegrationConfig[]>([
// Social Media Platforms
{
id: 'facebook',
name: 'Facebook',
description: 'Connect your Facebook page for AI-powered content creation and automated posting',
icon: <FacebookIcon />,
category: 'social',
apiKeyField: 'facebook_access_token',
apiKeyPlaceholder: 'EAA...',
setupUrl: 'https://developers.facebook.com/apps/',
features: ['AI Content Generation', 'Automated Posting', 'Trend Analysis', 'Engagement Tracking'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'twitter',
name: 'Twitter/X',
description: 'Connect your Twitter account for AI-powered tweets and trend analysis',
icon: <TwitterIcon />,
category: 'social',
apiKeyField: 'twitter_bearer_token',
apiKeyPlaceholder: 'AAAA...',
setupUrl: 'https://developer.twitter.com/en/portal/dashboard',
features: ['AI Tweet Generation', 'Trend Analysis', 'Automated Posting', 'Hashtag Optimization'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'instagram',
name: 'Instagram',
description: 'Connect your Instagram account for AI-powered content and caption generation',
icon: <InstagramIcon />,
category: 'social',
apiKeyField: 'instagram_access_token',
apiKeyPlaceholder: 'IGQ...',
setupUrl: 'https://developers.facebook.com/apps/',
features: ['AI Caption Generation', 'Hashtag Optimization', 'Content Scheduling', 'Engagement Analytics'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'linkedin',
name: 'LinkedIn',
description: 'Connect your LinkedIn profile for professional content creation and networking',
icon: <LinkedInIcon />,
category: 'social',
apiKeyField: 'linkedin_access_token',
apiKeyPlaceholder: 'AQV...',
setupUrl: 'https://www.linkedin.com/developers/',
features: ['Professional Content', 'Network Analysis', 'Industry Insights', 'Thought Leadership'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'youtube',
name: 'YouTube',
description: 'Connect your YouTube channel for AI-powered video descriptions and SEO optimization',
icon: <YouTubeIcon />,
category: 'social',
apiKeyField: 'youtube_api_key',
apiKeyPlaceholder: 'AIza...',
setupUrl: 'https://console.developers.google.com/',
features: ['Video Description AI', 'SEO Optimization', 'Trend Analysis', 'Content Strategy'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'tiktok',
name: 'TikTok',
description: 'Connect your TikTok account for AI-powered video captions and trend analysis',
icon: <TikTokIcon />,
category: 'social',
apiKeyField: 'tiktok_access_token',
apiKeyPlaceholder: 'TikTok...',
setupUrl: 'https://developers.tiktok.com/',
features: ['Video Caption AI', 'Trend Analysis', 'Hashtag Optimization', 'Viral Content'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'pinterest',
name: 'Pinterest',
description: 'Connect your Pinterest account for AI-powered pin descriptions and board optimization',
icon: <PinterestIcon />,
category: 'social',
apiKeyField: 'pinterest_access_token',
apiKeyPlaceholder: 'Pinterest...',
setupUrl: 'https://developers.pinterest.com/',
features: ['Pin Description AI', 'Board Optimization', 'Visual Content Strategy', 'SEO Enhancement'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
// Website Platforms
{
id: 'wordpress',
name: 'WordPress',
description: 'Connect your WordPress site for AI-powered content management and SEO optimization',
icon: <WordPressIcon />,
category: 'platform',
apiKeyField: 'wordpress_api_key',
apiKeyPlaceholder: 'wp_...',
setupUrl: 'https://wordpress.org/plugins/rest-api/',
features: ['AI Content Creation', 'SEO Optimization', 'Automated Publishing', 'Performance Analytics'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
},
{
id: 'wix',
name: 'Wix',
description: 'Connect your Wix website for AI-powered content management and optimization',
icon: <WebIcon />,
category: 'platform',
apiKeyField: 'wix_api_key',
apiKeyPlaceholder: 'wix_...',
setupUrl: 'https://developers.wix.com/',
features: ['AI Content Creation', 'SEO Optimization', 'Automated Updates', 'Performance Tracking'],
isConnected: false,
apiKey: '',
showApiKey: false,
isEnabled: false,
status: 'disconnected'
}
]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
updateHeaderContent({
title: 'Connect Your Platforms',
description: 'Integrate your social media accounts and websites to enable AI-powered content creation, automated posting, and comprehensive analytics across all your platforms.'
});
}, [updateHeaderContent]);
useEffect(() => {
// Prefill integrations on mount
const fetchIntegrations = async () => {
try {
const res = await fetch('/api/onboarding/integrations');
const data = await res.json();
if (data.success && Array.isArray(data.integrations)) {
setIntegrations(prev => prev.map(intg => {
const found = data.integrations.find((i: any) => i.id === intg.id);
if (found) {
return {
...intg,
apiKey: found.apiKey || '',
isConnected: !!found.isConnected,
isEnabled: typeof found.isEnabled === 'boolean' ? found.isEnabled : intg.isEnabled,
status: found.status || intg.status,
};
}
return intg;
}));
}
} catch (err) {
console.error('IntegrationsStep: Error pre-filling integrations', err);
}
};
fetchIntegrations();
}, []);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
const handleApiKeyChange = (integrationId: string, value: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, apiKey: value }
: integration
));
};
const handleToggleApiKeyVisibility = (integrationId: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, showApiKey: !integration.showApiKey }
: integration
));
};
const handleToggleIntegration = (integrationId: string) => {
setIntegrations(prev => prev.map(integration =>
integration.id === integrationId
? { ...integration, isEnabled: !integration.isEnabled }
: integration
));
};
const handleConnectIntegration = async (integrationId: string) => {
const integration = integrations.find(i => i.id === integrationId);
if (!integration) return;
setLoading(true);
setError(null);
try {
// Simulate API call to connect integration
await new Promise(resolve => setTimeout(resolve, 2000));
setIntegrations(prev => prev.map(i =>
i.id === integrationId
? { ...i, isConnected: true, status: 'connected' }
: i
));
setSuccess(`${integration.name} connected successfully!`);
} catch (err) {
setError(`Failed to connect ${integration.name}. Please check your API key and try again.`);
setIntegrations(prev => prev.map(i =>
i.id === integrationId
? { ...i, status: 'error' }
: i
));
} finally {
setLoading(false);
}
};
const handleContinue = async () => {
const connectedIntegrations = integrations.filter(i => i.isConnected);
if (connectedIntegrations.length === 0) {
setError('Please connect at least one platform to continue.');
return;
}
console.log('IntegrationsStep: handleContinue called');
console.log('IntegrationsStep: Connected integrations:', connectedIntegrations.length);
console.log('IntegrationsStep: Current step should be 5 (IntegrationsStep)');
console.log('IntegrationsStep: Calling onContinue()');
try {
// Add a small delay to see the logs
await new Promise(resolve => setTimeout(resolve, 100));
onContinue();
} catch (error) {
console.error('IntegrationsStep: Error in onContinue:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'connected': return 'success';
case 'error': return 'error';
case 'pending': return 'warning';
default: return 'default';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <CheckIcon color="success" />;
case 'error': return <ErrorIcon color="error" />;
case 'pending': return <CircularProgress size={16} />;
default: return <InfoIcon color="action" />;
}
};
const renderIntegrationCard = (integration: IntegrationConfig) => (
<Zoom in timeout={300}>
<Card
sx={{
mb: 2,
border: integration.isConnected ? '2px solid success.main' : '1px solid rgba(0,0,0,0.12)',
background: integration.isConnected ? 'success.50' : 'background.paper',
transition: 'all 0.3s ease'
}}
>
<CardContent sx={{ p: 3 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Box display="flex" alignItems="center" gap={2}>
<Box sx={{
color: integration.isConnected ? 'success.main' : 'primary.main',
fontSize: 32
}}>
{integration.icon}
</Box>
<Box>
<Typography variant="h6" fontWeight={600}>
{integration.name}
</Typography>
<Typography variant="body2" color="textSecondary">
{integration.description}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
{getStatusIcon(integration.status)}
<Chip
label={integration.status}
color={getStatusColor(integration.status) as any}
size="small"
/>
</Box>
</Box>
<Grid container spacing={2} mb={2}>
<Grid item xs={12} md={8}>
<TextField
label={`${integration.name} API Key`}
type={integration.showApiKey ? 'text' : 'password'}
value={integration.apiKey}
onChange={(e) => handleApiKeyChange(integration.id, e.target.value)}
placeholder={integration.apiKeyPlaceholder}
fullWidth
size="small"
disabled={integration.isConnected}
InputProps={{
endAdornment: (
<IconButton
onClick={() => handleToggleApiKeyVisibility(integration.id)}
edge="end"
>
{integration.showApiKey ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
),
}}
/>
</Grid>
<Grid item xs={12} md={4}>
<Box display="flex" gap={1}>
<Button
variant="outlined"
size="small"
startIcon={<LaunchIcon />}
onClick={() => window.open(integration.setupUrl, '_blank')}
fullWidth
>
Setup Guide
</Button>
{!integration.isConnected && (
<Button
variant="contained"
size="small"
startIcon={<LinkIcon />}
onClick={() => handleConnectIntegration(integration.id)}
disabled={!integration.apiKey || loading}
fullWidth
>
Connect
</Button>
)}
</Box>
</Grid>
</Grid>
<Box mb={2}>
<Typography variant="subtitle2" color="primary" gutterBottom>
Features:
</Typography>
<Box display="flex" flexWrap="wrap" gap={1}>
{integration.features.map((feature, index) => (
<Chip
key={index}
label={feature}
size="small"
variant="outlined"
icon={<AutoAwesomeIcon />}
/>
))}
</Box>
</Box>
<FormControlLabel
control={
<Switch
checked={integration.isEnabled}
onChange={() => handleToggleIntegration(integration.id)}
disabled={!integration.isConnected}
/>
}
label="Enable AI-powered features for this platform"
/>
</CardContent>
</Card>
</Zoom>
);
const renderTabContent = (category: 'social' | 'platform' | 'analytics') => {
const categoryIntegrations = integrations.filter(i => i.category === category);
return (
<Box>
{categoryIntegrations.map(integration => renderIntegrationCard(integration))}
</Box>
);
};
const connectedCount = integrations.filter(i => i.isConnected).length;
const enabledCount = integrations.filter(i => i.isEnabled).length;
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', p: 3 }}>
{/* Header Section */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700} gutterBottom>
Connect Your Platforms
</Typography>
<Typography variant="body1" color="textSecondary" sx={{ mb: 3, maxWidth: 800, mx: 'auto' }}>
Integrate your social media accounts and websites to enable AI-powered content creation,
automated posting, and comprehensive analytics across all your platforms.
</Typography>
{/* Stats Cards */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="primary" fontWeight={700}>
{integrations.length}
</Typography>
<Typography variant="body2" color="textSecondary">
Available Platforms
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="success.main" fontWeight={700}>
{connectedCount}
</Typography>
<Typography variant="body2" color="textSecondary">
Connected Platforms
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper elevation={2} sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="info.main" fontWeight={700}>
{enabledCount}
</Typography>
<Typography variant="body2" color="textSecondary">
AI Features Enabled
</Typography>
</Paper>
</Grid>
</Grid>
</Box>
{/* Info Alert */}
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body2">
<strong>How it works:</strong> Connect your platforms using their API keys. Once connected,
ALwrity can generate AI-powered content, analyze trends, and automatically post to your platforms.
Your API keys are securely stored and never shared.
</Typography>
</Alert>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
{success}
</Alert>
)}
{/* Tabs for Different Categories */}
<Paper elevation={2} sx={{ mb: 3 }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTab-root': {
textTransform: 'none',
fontWeight: 600,
fontSize: '1rem'
}
}}
>
<Tab
label={
<Box display="flex" alignItems="center" gap={1}>
<AutoAwesomeIcon />
Social Media ({integrations.filter(i => i.category === 'social').length})
</Box>
}
/>
<Tab
label={
<Box display="flex" alignItems="center" gap={1}>
<WebIcon />
Website Platforms ({integrations.filter(i => i.category === 'platform').length})
</Box>
}
/>
</Tabs>
</Paper>
{/* Tab Content */}
<Box sx={{ mb: 4 }}>
{activeTab === 0 && renderTabContent('social')}
{activeTab === 1 && renderTabContent('platform')}
</Box>
{/* Features Preview */}
{connectedCount > 0 && (
<Accordion sx={{ mb: 3 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
<SmartToyIcon color="primary" />
<Typography variant="h6">AI Features Preview</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<ContentPasteIcon color="primary" />
<Typography variant="h6">Content Creation</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="AI-powered content generation" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Platform-specific optimization" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Hashtag and SEO optimization" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<ScheduleIcon color="primary" />
<Typography variant="h6">Automation</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Automated posting schedules" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Cross-platform content distribution" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Smart timing optimization" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<AnalyticsIcon color="primary" />
<Typography variant="h6">Analytics</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Performance tracking" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Trend analysis" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Engagement insights" />
</ListItem>
</List>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ p: 2 }}>
<Box display="flex" alignItems="center" gap={1} mb={2}>
<TrendingUpIcon color="primary" />
<Typography variant="h6">Optimization</Typography>
</Box>
<List dense>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Content performance optimization" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="Audience targeting" />
</ListItem>
<ListItem>
<ListItemIcon><CheckIcon color="success" /></ListItemIcon>
<ListItemText primary="ROI tracking" />
</ListItem>
</List>
</Card>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
)}
{/* Continue Button */}
<Box display="flex" justifyContent="center" mt={4}>
<Button
variant="contained"
size="large"
onClick={handleContinue}
disabled={connectedCount === 0}
startIcon={connectedCount > 0 ? <CheckIcon /> : <WarningIcon />}
sx={{
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
}
}}
>
{connectedCount === 0
? 'Connect at least one platform to continue'
: `Continue with ${connectedCount} connected platform${connectedCount > 1 ? 's' : ''}`
}
</Button>
</Box>
</Box>
);
};
export default IntegrationsStep;

View File

@@ -0,0 +1,362 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
MenuItem,
FormControl,
InputLabel,
Select,
Chip,
OutlinedInput,
FormHelperText,
Switch,
FormControlLabel,
Accordion,
AccordionSummary,
AccordionDetails,
Divider
} from '@mui/material';
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
import {
validateContentStyle,
configureBrandVoice,
processPersonalizationSettings,
getPersonalizationConfigurationOptions,
generateContentGuidelines,
ContentStyleRequest,
BrandVoiceRequest,
AdvancedSettingsRequest,
PersonalizationSettingsRequest
} from '../../api/componentLogic';
interface PersonalizationStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const PersonalizationStep: React.FC<PersonalizationStepProps> = ({ onContinue, updateHeaderContent }) => {
// Content Style State
const [writingStyle, setWritingStyle] = useState('Professional');
const [tone, setTone] = useState('Neutral');
const [contentLength, setContentLength] = useState('Standard');
// Brand Voice State
const [personalityTraits, setPersonalityTraits] = useState<string[]>(['Professional']);
const [voiceDescription, setVoiceDescription] = useState('');
const [keywords, setKeywords] = useState('');
// Advanced Settings State
const [seoOptimization, setSeoOptimization] = useState(false);
const [readabilityLevel, setReadabilityLevel] = useState('Standard');
const [contentStructure, setContentStructure] = useState<string[]>(['Introduction', 'Key Points', 'Conclusion']);
// UI State
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [configurationOptions, setConfigurationOptions] = useState<any>(null);
useEffect(() => {
async function loadConfigurationOptions() {
try {
const options = await getPersonalizationConfigurationOptions();
setConfigurationOptions(options.options);
} catch (e) {
console.error('Failed to load configuration options:', e);
}
}
loadConfigurationOptions();
// Update header content when component mounts
updateHeaderContent({
title: 'Customize Your Experience',
description: 'Personalize Alwrity to match your brand voice, content style, and writing preferences. Configure how AI generates content to ensure it aligns with your brand identity and resonates with your audience.'
});
}, [updateHeaderContent]);
const handleContinue = async () => {
setError(null);
setSuccess(null);
setLoading(true);
try {
// Validate content style
const contentStyleRequest: ContentStyleRequest = {
writing_style: writingStyle,
tone: tone,
content_length: contentLength
};
const contentStyleValidation = await validateContentStyle(contentStyleRequest);
if (!contentStyleValidation.valid) {
setError(`Content style validation failed: ${contentStyleValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Configure brand voice
const brandVoiceRequest: BrandVoiceRequest = {
personality_traits: personalityTraits,
voice_description: voiceDescription,
keywords: keywords
};
const brandVoiceValidation = await configureBrandVoice(brandVoiceRequest);
if (!brandVoiceValidation.valid) {
setError(`Brand voice validation failed: ${brandVoiceValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Process complete settings
const advancedSettingsRequest: AdvancedSettingsRequest = {
seo_optimization: seoOptimization,
readability_level: readabilityLevel,
content_structure: contentStructure
};
const completeSettingsRequest: PersonalizationSettingsRequest = {
content_style: contentStyleRequest,
brand_voice: brandVoiceRequest,
advanced_settings: advancedSettingsRequest
};
const settingsValidation = await processPersonalizationSettings(completeSettingsRequest);
if (!settingsValidation.valid) {
setError(`Settings validation failed: ${settingsValidation.errors.join(', ')}`);
setLoading(false);
return;
}
// Generate content guidelines
const guidelines = await generateContentGuidelines(settingsValidation.settings);
if (guidelines.success) {
setSuccess('Personalization settings saved successfully! Content guidelines generated.');
// TODO: Store guidelines for later use
onContinue();
} else {
setError('Failed to generate content guidelines.');
}
} catch (e) {
setError('Failed to save personalization settings. Please try again.');
console.error('Personalization error:', e);
} finally {
setLoading(false);
}
};
const handlePersonalityTraitsChange = (event: any) => {
const value = event.target.value;
setPersonalityTraits(typeof value === 'string' ? value.split(',') : value);
};
const handleContentStructureChange = (event: any) => {
const value = event.target.value;
setContentStructure(typeof value === 'string' ? value.split(',') : value);
};
if (!configurationOptions) {
return (
<Box>
<Typography variant="h6" gutterBottom>
Personalize Your Experience
</Typography>
<Alert severity="info">Loading configuration options...</Alert>
</Box>
);
}
return (
<Box>
{/* Enhanced Explanatory Text */}
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary" sx={{
mb: 3,
lineHeight: 1.6,
maxWidth: 800,
mx: 'auto',
fontWeight: 500,
opacity: 0.8
}}>
Configure your content style, brand voice, and advanced settings to tailor the AI experience to your needs.
This ensures that all generated content aligns with your brand identity and resonates with your target audience.
</Typography>
</Box>
{/* Content Style Section */}
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Content Style</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl fullWidth>
<InputLabel>Writing Style</InputLabel>
<Select
value={writingStyle}
onChange={(e) => setWritingStyle(e.target.value)}
label="Writing Style"
>
{configurationOptions.writing_styles?.map((style: string) => (
<MenuItem key={style} value={style}>{style}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Tone</InputLabel>
<Select
value={tone}
onChange={(e) => setTone(e.target.value)}
label="Tone"
>
{configurationOptions.tones?.map((toneOption: string) => (
<MenuItem key={toneOption} value={toneOption}>{toneOption}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Content Length</InputLabel>
<Select
value={contentLength}
onChange={(e) => setContentLength(e.target.value)}
label="Content Length"
>
{configurationOptions.content_lengths?.map((length: string) => (
<MenuItem key={length} value={length}>{length}</MenuItem>
))}
</Select>
</FormControl>
</Box>
</AccordionDetails>
</Accordion>
{/* Brand Voice Section */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Brand Voice</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl fullWidth>
<InputLabel>Personality Traits</InputLabel>
<Select
multiple
value={personalityTraits}
onChange={handlePersonalityTraitsChange}
input={<OutlinedInput label="Personality Traits" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{configurationOptions.personality_traits?.map((trait: string) => (
<MenuItem key={trait} value={trait}>{trait}</MenuItem>
))}
</Select>
<FormHelperText>Select traits that best describe your brand</FormHelperText>
</FormControl>
<TextField
label="Brand Voice Description"
value={voiceDescription}
onChange={(e) => setVoiceDescription(e.target.value)}
fullWidth
multiline
rows={3}
helperText="Describe how your brand should sound in content (optional)"
/>
<TextField
label="Brand Keywords"
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
fullWidth
helperText="Enter key terms that should be used in your content (optional)"
/>
</Box>
</AccordionDetails>
</Accordion>
{/* Advanced Settings Section */}
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">Advanced Settings</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={seoOptimization}
onChange={(e) => setSeoOptimization(e.target.checked)}
/>
}
label="Enable SEO Optimization"
/>
<FormControl fullWidth>
<InputLabel>Readability Level</InputLabel>
<Select
value={readabilityLevel}
onChange={(e) => setReadabilityLevel(e.target.value)}
label="Readability Level"
>
{configurationOptions.readability_levels?.map((level: string) => (
<MenuItem key={level} value={level}>{level}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Content Structure</InputLabel>
<Select
multiple
value={contentStructure}
onChange={handleContentStructureChange}
input={<OutlinedInput label="Content Structure" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{configurationOptions.content_structures?.map((structure: string) => (
<MenuItem key={structure} value={structure}>{structure}</MenuItem>
))}
</Select>
<FormHelperText>Select required content sections</FormHelperText>
</FormControl>
</Box>
</AccordionDetails>
</Accordion>
<Divider sx={{ my: 2 }} />
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ mt: 2 }}>{success}</Alert>}
<Button
variant="contained"
color="primary"
onClick={handleContinue}
sx={{ mt: 2 }}
disabled={loading}
>
{loading ? 'Saving Settings...' : 'Continue'}
</Button>
</Box>
);
};
export default PersonalizationStep;

View File

@@ -0,0 +1,914 @@
import React, { useEffect, useState } from 'react';
import {
Box,
TextField,
Typography,
Alert,
Card,
CardContent,
Fade,
Zoom,
Chip,
IconButton,
Collapse,
Divider,
Link,
Container,
Paper,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
OutlinedInput,
FormHelperText,
Switch,
FormControlLabel,
Button,
CircularProgress,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Error as ErrorIcon,
Info,
Search,
HelpOutline,
Warning,
Star,
VerifiedUser,
Lock,
Science,
TrendingUp,
Security,
AutoAwesome,
School,
Link as LinkIcon,
Launch,
Close
} from '@mui/icons-material';
import { getApiKeys, saveApiKey } from '../../api/onboarding';
import { configureResearchPreferences } from '../../api/componentLogic';
import { useOnboardingStyles } from './common/useOnboardingStyles';
import {
validateApiKey,
getKeyStatus,
isFormValid,
debounce,
formatErrorMessage
} from './common/onboardingUtils';
import OnboardingButton from './common/OnboardingButton';
import OnboardingCard from './common/OnboardingCard';
interface ResearchStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
}
const ResearchStep: React.FC<ResearchStepProps> = ({ onContinue, updateHeaderContent }) => {
console.log('ResearchStep: Component rendered');
// API Keys State
const [tavilyKey, setTavilyKey] = useState('');
const [serperKey, setSerperKey] = useState('');
const [exaKey, setExaKey] = useState('');
const [firecrawlKey, setFirecrawlKey] = useState('');
// User Information State
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [company, setCompany] = useState('');
const [role, setRole] = useState('Content Creator');
// Research Preferences State
const [researchDepth, setResearchDepth] = useState('Comprehensive');
const [contentTypes, setContentTypes] = useState<string[]>(['Blog Posts', 'Social Media', 'Articles']);
const [autoResearch, setAutoResearch] = useState(true);
const [factualContent, setFactualContent] = useState(true);
// UI State
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [showTavilyKey, setShowTavilyKey] = useState(false);
const [showSerperKey, setShowSerperKey] = useState(false);
const [showExaKey, setShowExaKey] = useState(false);
const [showFirecrawlKey, setShowFirecrawlKey] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [savedKeys, setSavedKeys] = useState<Record<string, string>>({});
const [benefitsDialog, setBenefitsDialog] = useState<{ open: boolean; provider: any }>({ open: false, provider: null });
const [keysLoaded, setKeysLoaded] = useState(false);
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
const styles = useOnboardingStyles();
useEffect(() => {
console.log('ResearchStep: useEffect triggered', { keysLoaded });
if (!keysLoaded) {
console.log('ResearchStep: Calling debouncedLoadKeys');
debouncedLoadKeys();
} else {
console.log('ResearchStep: Keys already loaded, skipping debouncedLoadKeys');
}
loadWebsiteDefaults();
}, [keysLoaded]); // Removed updateHeaderContent from dependencies
useEffect(() => {
updateHeaderContent({
title: "Configure AI Research",
description: "Set up research APIs and preferences for intelligent content generation"
});
}, [updateHeaderContent]);
useEffect(() => {
// Prefill research preferences on mount
const fetchPreferences = async () => {
if (preferencesLoaded) {
console.log('ResearchStep: Preferences already loaded, skipping API call');
return;
}
try {
console.log('ResearchStep: Loading research preferences...');
const res = await import('../../api/componentLogic');
const { getResearchPreferences } = res;
const data = await getResearchPreferences();
if (data && data.preferences) {
if (data.preferences.research_depth) setResearchDepth(data.preferences.research_depth);
if (data.preferences.content_types) setContentTypes(data.preferences.content_types);
if (typeof data.preferences.auto_research === 'boolean') setAutoResearch(data.preferences.auto_research);
if (typeof data.preferences.factual_content === 'boolean') setFactualContent(data.preferences.factual_content);
}
setPreferencesLoaded(true);
console.log('ResearchStep: Research preferences loaded successfully');
} catch (err) {
console.error('ResearchStep: Error pre-filling research preferences', err);
setPreferencesLoaded(true); // Set to true even on error to prevent infinite retries
}
};
fetchPreferences();
}, []); // Empty dependency array to run only once on mount
const loadExistingKeys = async () => {
if (keysLoaded) {
console.log('ResearchStep: Keys already loaded, skipping API call');
return; // Prevent multiple calls
}
console.log('ResearchStep: Starting to load API keys...');
try {
const keys = await getApiKeys();
console.log('ResearchStep: API keys loaded successfully:', Object.keys(keys));
setSavedKeys(keys);
if (keys.tavily) setTavilyKey(keys.tavily);
if (keys.serperapi) setSerperKey(keys.serperapi);
if (keys.exa) setExaKey(keys.exa);
if (keys.firecrawl) setFirecrawlKey(keys.firecrawl);
setKeysLoaded(true); // Set keysLoaded to true after keys are loaded
console.log('ResearchStep: Keys loaded and state updated');
} catch (error: any) {
console.error('ResearchStep: Error loading API keys:', error);
// Don't show error for rate limiting - it will retry automatically
if (error.response?.status !== 429) {
setError(`Failed to load API keys: ${error.message || 'Unknown error'}`);
}
setKeysLoaded(true); // Set to true even on error to prevent infinite retries
console.log('ResearchStep: Set keysLoaded to true after error');
}
};
// Debounced version to prevent rapid calls
const debouncedLoadKeys = debounce(() => {
console.log('ResearchStep: debouncedLoadKeys called');
loadExistingKeys();
}, 1000);
const loadWebsiteDefaults = async () => {
try {
// TODO: Load website analysis data and populate intelligent defaults
// This would be based on the website URL from step 2
// For now, we'll use sensible defaults
setCompany('Your Company');
setRole('Content Creator');
setResearchDepth('Comprehensive');
setContentTypes(['Blog Posts', 'Social Media', 'Articles']);
} catch (error) {
console.error('Error loading website defaults:', error);
}
};
const handleSave = async () => {
setLoading(true);
setError(null);
setSuccess(null);
try {
const promises = [];
// Save API keys
if (tavilyKey.trim()) {
promises.push(saveApiKey('tavily', tavilyKey.trim()));
}
if (serperKey.trim()) {
promises.push(saveApiKey('serperapi', serperKey.trim()));
}
if (exaKey.trim()) {
promises.push(saveApiKey('exa', exaKey.trim()));
}
if (firecrawlKey.trim()) {
promises.push(saveApiKey('firecrawl', firecrawlKey.trim()));
}
// Save research preferences to database
const researchPreferences = {
research_depth: researchDepth,
content_types: contentTypes,
auto_research: autoResearch,
factual_content: factualContent
};
const preferencesResponse = await configureResearchPreferences(researchPreferences);
if (!preferencesResponse.valid) {
const errorMessage = preferencesResponse.errors?.join(', ') || 'Unknown error';
const error = `Failed to save research preferences: ${errorMessage}`;
throw error;
}
await Promise.all(promises);
setSuccess('Research configuration and preferences saved successfully!');
// Auto-continue after a short delay
setTimeout(() => {
onContinue();
}, 1500);
} catch (err) {
setError(formatErrorMessage(err));
console.error('Error saving research configuration:', err);
} finally {
setLoading(false);
}
};
const researchProviders = [
{
name: 'Tavily AI',
description: 'Intelligent web research and content analysis',
benefits: ['Factual content generation', 'Real-time information', 'Comprehensive research'],
key: tavilyKey,
setKey: setTavilyKey,
showKey: showTavilyKey,
setShowKey: setShowTavilyKey,
placeholder: 'tvly-...',
status: getKeyStatus(tavilyKey, 'tavily'),
link: 'https://tavily.com/',
free: true,
recommended: true
},
{
name: 'Exa',
description: 'Advanced web search and content discovery',
benefits: ['High-quality search results', 'Content verification', 'Source credibility'],
key: exaKey,
setKey: setExaKey,
showKey: showExaKey,
setShowKey: setShowExaKey,
placeholder: 'exa-...',
status: getKeyStatus(exaKey, 'exa'),
link: 'https://exa.ai/',
free: true,
recommended: true
},
{
name: 'Serper API',
description: 'Google search results and web data',
benefits: ['Google search integration', 'Real-time data', 'Comprehensive coverage'],
key: serperKey,
setKey: setSerperKey,
showKey: showSerperKey,
setShowKey: setShowSerperKey,
placeholder: 'serper-...',
status: getKeyStatus(serperKey, 'serperapi'),
link: 'https://serper.dev/',
free: true,
recommended: false
},
{
name: 'Firecrawl',
description: 'Web content extraction and processing',
benefits: ['Content extraction', 'Data processing', 'Structured information'],
key: firecrawlKey,
setKey: setFirecrawlKey,
showKey: showFirecrawlKey,
setShowKey: setShowFirecrawlKey,
placeholder: 'firecrawl-...',
status: getKeyStatus(firecrawlKey, 'firecrawl'),
link: 'https://firecrawl.dev/',
free: true,
recommended: false
}
];
const hasAtLeastOneKey = tavilyKey.trim() || exaKey.trim() || serperKey.trim() || firecrawlKey.trim();
const isValid = fullName.trim() && email.trim() && company.trim();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Importance Notice */}
<Paper elevation={0} sx={{
p: 3,
mb: 4,
textAlign: 'left',
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 2
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
<AutoAwesome sx={{ color: 'warning.main', fontSize: 24 }} />
<Typography variant="h6" color="warning.dark" sx={{ fontWeight: 600 }}>
Why Research APIs Matter
</Typography>
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Factual Content
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Generate content based on real, verified information instead of AI hallucinations.
</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<TrendingUp sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Real-time Data
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Access current information, trends, and latest developments in your industry.
</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Security sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Source Verification
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
Verify facts and cite reliable sources to build trust with your audience.
</Typography>
</Grid>
</Grid>
</Paper>
{/* Research Providers */}
<Box sx={{ mb: 4 }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<Search sx={{ color: 'primary.main' }} />
Research API Providers
</Typography>
<Grid container spacing={3}>
{researchProviders.map((provider, index) => (
<Grid item xs={12} md={6} key={provider.name}>
<Zoom in={true} timeout={700 + index * 100}>
<Card
sx={{
background: provider.status === 'valid'
? 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)'
: provider.status === 'invalid'
? 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)'
: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
border: `2px solid ${
provider.status === 'valid'
? '#10b981'
: provider.status === 'invalid'
? '#ef4444'
: 'rgba(0,0,0,0.08)'
}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: provider.status === 'valid'
? '0 8px 25px rgba(16, 185, 129, 0.25), 0 0 0 1px rgba(16, 185, 129, 0.1)'
: provider.status === 'invalid'
? '0 8px 25px rgba(239, 68, 68, 0.25), 0 0 0 1px rgba(239, 68, 68, 0.1)'
: '0 8px 25px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',
transform: 'translateY(-2px)'
},
position: 'relative',
overflow: 'hidden',
borderRadius: 3,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: provider.status === 'valid'
? 'linear-gradient(90deg, #10b981 0%, #059669 100%)'
: provider.status === 'invalid'
? 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(90deg, #6b7280 0%, #4b5563 100%)',
},
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: provider.status === 'valid'
? 'radial-gradient(circle at top right, rgba(16, 185, 129, 0.1) 0%, transparent 70%)'
: provider.status === 'invalid'
? 'radial-gradient(circle at top right, rgba(239, 68, 68, 0.1) 0%, transparent 70%)'
: 'radial-gradient(circle at top right, rgba(107, 114, 128, 0.1) 0%, transparent 70%)',
pointerEvents: 'none'
}
}}
>
<CardContent sx={{ p: 2.5, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{
width: 36,
height: 36,
borderRadius: '50%',
background: provider.recommended
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
}}>
<Search sx={{ color: 'white', fontSize: 18 }} />
</Box>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0 }}>
{provider.name}
</Typography>
{provider.recommended && (
<Chip
label="Recommended"
color="success"
size="small"
sx={{ fontWeight: 600, height: 20 }}
/>
)}
{provider.free && (
<Chip
label="Free Tier"
color="primary"
size="small"
sx={{ fontWeight: 600, height: 20 }}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.875rem' }}>
{provider.description}
</Typography>
</Box>
</Box>
{provider.status === 'valid' && (
<Chip
icon={<CheckCircle />}
label="Valid"
color="success"
size="small"
sx={{ fontWeight: 600, height: 24 }}
/>
)}
{provider.status === 'invalid' && (
<Chip
icon={<ErrorIcon />}
label="Invalid"
color="error"
size="small"
sx={{ fontWeight: 600, height: 24 }}
/>
)}
</Box>
<Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Benefits:
</Typography>
<Tooltip title="View all benefits">
<IconButton
size="small"
onClick={() => setBenefitsDialog({ open: true, provider })}
sx={{
color: 'primary.main',
'&:hover': {
background: 'rgba(59, 130, 246, 0.1)'
}
}}
>
<HelpOutline sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
<TextField
fullWidth
type={provider.showKey ? 'text' : 'password'}
value={provider.key}
onChange={(e) => provider.setKey(e.target.value)}
placeholder={provider.placeholder}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<Lock sx={{ color: 'text.secondary', mr: 1, fontSize: 16 }} />
),
endAdornment: (
<IconButton
onClick={() => provider.setShowKey(!provider.showKey)}
edge="end"
size="small"
>
{provider.showKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
background: 'rgba(255, 255, 255, 0.9)',
backdropFilter: 'blur(10px)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.8)',
border: '1px solid rgba(0, 0, 0, 0.08)',
transition: 'all 0.2s ease-in-out',
'&:hover': {
background: 'rgba(255, 255, 255, 0.95)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(0, 0, 0, 0.12)'
},
'&.Mui-focused': {
background: 'rgba(255, 255, 255, 0.98)',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 12px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(59, 130, 246, 0.3)'
}
}
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<LinkIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
<Link
href={provider.link}
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
fontWeight: 600,
fontSize: '0.875rem'
}}
>
Get API Key
<Launch sx={{ fontSize: 14 }} />
</Link>
</Box>
{savedKeys[provider.name.toLowerCase()] && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
<CheckCircle sx={{ color: 'success.main', fontSize: 16 }} />
<Typography variant="caption" color="success.main" sx={{ fontWeight: 500 }}>
Key already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
</Grid>
))}
</Grid>
</Box>
{/* Research Preferences */}
<Zoom in={true} timeout={1400}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 3
}}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<School sx={{ color: 'success.main' }} />
Research Preferences
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Research Depth</InputLabel>
<Select
value={researchDepth}
onChange={(e) => setResearchDepth(e.target.value)}
label="Research Depth"
size="medium"
>
<MenuItem value="Basic">Basic - Quick overview</MenuItem>
<MenuItem value="Standard">Standard - Balanced depth</MenuItem>
<MenuItem value="Comprehensive">Comprehensive - Detailed analysis</MenuItem>
<MenuItem value="Expert">Expert - In-depth research</MenuItem>
</Select>
<FormHelperText>Choose how detailed you want the AI research to be</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Content Types</InputLabel>
<Select
multiple
value={contentTypes}
onChange={(e) => setContentTypes(typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value)}
input={<OutlinedInput label="Content Types" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
size="medium"
>
<MenuItem value="Blog Posts">Blog Posts</MenuItem>
<MenuItem value="Social Media">Social Media</MenuItem>
<MenuItem value="Articles">Articles</MenuItem>
<MenuItem value="Email Newsletters">Email Newsletters</MenuItem>
<MenuItem value="Product Descriptions">Product Descriptions</MenuItem>
<MenuItem value="Landing Pages">Landing Pages</MenuItem>
</Select>
<FormHelperText>Choose what types of content you want to research</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={autoResearch}
onChange={(e) => setAutoResearch(e.target.checked)}
color="primary"
/>
}
label="Enable Automated Research"
/>
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
Automatically start research when content topics are added
</Typography>
<FormControlLabel
control={
<Switch
checked={factualContent}
onChange={(e) => setFactualContent(e.target.checked)}
color="primary"
/>
}
label="Prioritize Factual Content"
/>
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
Focus on generating content based on verified facts and sources
</Typography>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
{/* Help Section */}
<Collapse in={showHelp}>
<Zoom in={showHelp} timeout={1600}>
<Paper elevation={0} sx={{
p: 4,
mb: 4,
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 3
}}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<HelpOutline color="primary" />
How to Get Your Research API Keys
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<Star sx={{ color: 'warning.main', fontSize: 20 }} />
Recommended Providers
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Tavily AI
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Visit{' '}
<Link href="https://tavily.com/" target="_blank" rel="noopener noreferrer" sx={{ fontWeight: 600 }}>
tavily.com
</Link>
, sign up for free, and get your API key from the dashboard.
</Typography>
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Exa
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Visit{' '}
<Link href="https://exa.ai/" target="_blank" rel="noopener noreferrer" sx={{ fontWeight: 600 }}>
exa.ai
</Link>
, create an account, and access your API key in the settings.
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<Info sx={{ color: 'info.main', fontSize: 20 }} />
Why These APIs Matter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="body2" color="text.secondary">
<strong>Factual Content:</strong> Generate content based on real, verified information instead of AI hallucinations.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Real-time Data:</strong> Access current information, trends, and latest developments in your industry.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Source Verification:</strong> Verify facts and cite reliable sources to build trust with your audience.
</Typography>
<Typography variant="body2" color="text.secondary">
<strong>Free Tiers:</strong> Most providers offer generous free tiers to get you started.
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</Paper>
</Zoom>
</Collapse>
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
<Fade in={true}>
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
{error}
</Alert>
</Fade>
)}
{success && (
<Fade in={true}>
<Alert severity="success" sx={{ mb: 2, borderRadius: 2 }}>
{success}
</Alert>
</Fade>
)}
</Box>
{/* Action Buttons */}
<Box sx={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', mt: 4 }}>
<OnboardingButton
variant="text"
onClick={() => setShowHelp(!showHelp)}
icon={<HelpOutline />}
>
{showHelp ? 'Hide Help' : 'Get Help'}
</OnboardingButton>
</Box>
{/* Security Notice */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
<Lock sx={{ fontSize: 14 }} />
Your API keys are encrypted and stored securely on your device
</Typography>
</Box>
{/* Benefits Dialog */}
<Dialog
open={benefitsDialog.open}
onClose={() => setBenefitsDialog({ open: false, provider: null })}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
borderRadius: '12px 12px 0 0'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Search sx={{ fontSize: 24 }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{benefitsDialog.provider?.name} Benefits
</Typography>
</Box>
<IconButton
onClick={() => setBenefitsDialog({ open: false, provider: null })}
sx={{ color: 'white' }}
>
<Close />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 3 }}>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{benefitsDialog.provider?.description}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{benefitsDialog.provider?.benefits.map((benefit: string, index: number) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{
width: 32,
height: 32,
borderRadius: '50%',
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<CheckCircle sx={{ color: 'white', fontSize: 18 }} />
</Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{benefit}
</Typography>
</Box>
))}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
variant="outlined"
onClick={() => setBenefitsDialog({ open: false, provider: null })}
sx={{ borderRadius: 2 }}
>
Close
</Button>
<Button
variant="contained"
onClick={() => {
if (benefitsDialog.provider?.link) {
window.open(benefitsDialog.provider.link, '_blank');
}
}}
sx={{
borderRadius: 2,
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #059669 0%, #047857 100%)'
}
}}
>
Get API Key
</Button>
</DialogActions>
</Dialog>
</Container>
</Fade>
);
};
export default ResearchStep;

View File

@@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
TextField,
Typography,
Alert,
CircularProgress,
Card,
CardContent,
CardActions,
Grid,
Chip,
Divider
} from '@mui/material';
import { getApiKeys } from '../../api/onboarding';
import {
processResearchTopic,
processResearchResults,
validateResearchRequest,
getResearchProvidersInfo,
generateResearchReport,
ResearchTopicRequest
} from '../../api/componentLogic';
const ResearchTestStep: React.FC<{ onContinue: () => void }> = ({ onContinue }) => {
const [topic, setTopic] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [researchResults, setResearchResults] = useState<any>(null);
const [providersInfo, setProvidersInfo] = useState<any>(null);
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
useEffect(() => {
async function loadData() {
try {
// Load API keys
const keys = await getApiKeys();
setApiKeys(keys);
// Load providers info
const providers = await getResearchProvidersInfo();
setProvidersInfo(providers.providers_info);
} catch (e) {
console.error('Failed to load research data:', e);
}
}
loadData();
}, []);
const handleResearch = async () => {
if (!topic.trim()) {
setError('Please enter a research topic.');
return;
}
setLoading(true);
setError(null);
setSuccess(null);
setResearchResults(null);
try {
// Validate research request
const validation = await validateResearchRequest(topic, apiKeys);
if (!validation.valid) {
setError(`Research validation failed: ${validation.errors.join(', ')}`);
if (validation.warnings.length > 0) {
console.warn('Research warnings:', validation.warnings);
}
setLoading(false);
return;
}
// Process research topic
const request: ResearchTopicRequest = {
topic: topic.trim(),
api_keys: apiKeys
};
const results = await processResearchTopic(request);
if (!results.success) {
setError(`Research failed: ${results.error}`);
setLoading(false);
return;
}
// Process research results
const processedResults = await processResearchResults(results);
if (processedResults.success) {
setResearchResults(processedResults.processed_results);
setSuccess('Research completed successfully!');
} else {
setError('Failed to process research results.');
}
} catch (e) {
setError('Research failed. Please try again.');
console.error('Research error:', e);
} finally {
setLoading(false);
}
};
const handleGenerateReport = async () => {
if (!researchResults) {
setError('No research results available to generate report.');
return;
}
setLoading(true);
try {
const report = await generateResearchReport({ processed_results: researchResults });
if (report.success) {
setSuccess('Research report generated successfully!');
console.log('Generated report:', report.report);
} else {
setError('Failed to generate research report.');
}
} catch (e) {
setError('Failed to generate research report.');
console.error('Report generation error:', e);
} finally {
setLoading(false);
}
};
const availableProviders = providersInfo ? Object.keys(providersInfo.providers).filter(
provider => apiKeys[providersInfo.providers[provider].api_key_name]
) : [];
return (
<Box>
<Typography variant="h6" gutterBottom>
Test Research Functionality
</Typography>
<Typography variant="body2" color="textSecondary" gutterBottom>
Test the AI research capabilities with your configured settings and API keys.
</Typography>
{/* Research Input */}
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
Research Topic
</Typography>
<TextField
label="Enter a topic to research"
value={topic}
onChange={(e) => setTopic(e.target.value)}
fullWidth
multiline
rows={2}
placeholder="e.g., 'Latest trends in artificial intelligence'"
disabled={loading}
/>
{availableProviders.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" color="textSecondary">
Available providers: {availableProviders.map(provider => (
<Chip key={provider} label={provider} size="small" sx={{ mr: 0.5 }} />
))}
</Typography>
</Box>
)}
</CardContent>
<CardActions>
<Button
variant="contained"
onClick={handleResearch}
disabled={loading || !topic.trim()}
>
{loading ? 'Researching...' : 'Start Research'}
</Button>
</CardActions>
</Card>
{/* Research Results */}
{researchResults && (
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography variant="subtitle1" gutterBottom>
Research Results
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
<strong>Topic:</strong> {researchResults.topic}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
<strong>Summary:</strong>
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
{researchResults.summary}
</Typography>
</Grid>
{researchResults.key_insights && researchResults.key_insights.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="textSecondary">
<strong>Key Insights:</strong>
</Typography>
<Box sx={{ mt: 1 }}>
{researchResults.key_insights.map((insight: string, index: number) => (
<Chip
key={index}
label={insight}
size="small"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
</Grid>
)}
{researchResults.trends && researchResults.trends.length > 0 && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="textSecondary">
<strong>Trends:</strong>
</Typography>
<Box sx={{ mt: 1 }}>
{researchResults.trends.map((trend: string, index: number) => (
<Chip
key={index}
label={trend}
size="small"
variant="outlined"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
</Grid>
)}
{researchResults.metadata && (
<Grid item xs={12}>
<Divider sx={{ my: 1 }} />
<Typography variant="caption" color="textSecondary">
<strong>Research Details:</strong>
Confidence: {Math.round((researchResults.metadata.confidence_score || 0) * 100)}% |
Depth: {researchResults.metadata.research_depth} |
Providers: {researchResults.metadata.providers_used?.join(', ')}
</Typography>
</Grid>
)}
</Grid>
</CardContent>
<CardActions>
<Button
variant="outlined"
onClick={handleGenerateReport}
disabled={loading}
>
Generate Report
</Button>
</CardActions>
</Card>
)}
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ mt: 2 }}>{success}</Alert>}
<Button
variant="contained"
color="primary"
onClick={onContinue}
sx={{ mt: 2 }}
>
Continue to Next Step
</Button>
</Box>
);
};
export default ResearchTestStep;

View File

@@ -0,0 +1,330 @@
import React, { useState } from 'react';
import {
Box,
Typography,
TextField,
Button,
Alert,
CircularProgress,
Card,
CardContent,
Grid,
Chip,
Accordion,
AccordionSummary,
AccordionDetails,
Divider,
IconButton,
Tooltip
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ContentCopy as CopyIcon,
CheckCircle as CheckIcon,
Error as ErrorIcon,
Info as InfoIcon
} from '@mui/icons-material';
import { useOnboardingStyles } from './common/useOnboardingStyles';
interface StyleDetectionStepProps {
onContinue: () => void;
}
interface StyleAnalysis {
writing_style?: {
tone: string;
voice: string;
complexity: string;
engagement_level: string;
};
content_characteristics?: {
sentence_structure: string;
vocabulary_level: string;
paragraph_organization: string;
content_flow: string;
};
target_audience?: {
demographics: string[];
expertise_level: string;
industry_focus: string;
geographic_focus: string;
};
recommended_settings?: {
writing_tone: string;
target_audience: string;
content_type: string;
creativity_level: string;
geographic_location: string;
};
}
const StyleDetectionStep: React.FC<StyleDetectionStepProps> = ({ onContinue }) => {
const classes = useOnboardingStyles();
const [url, setUrl] = useState('');
const [textSample, setTextSample] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [analysis, setAnalysis] = useState<StyleAnalysis | null>(null);
const [activeTab, setActiveTab] = useState<'url' | 'text'>('url');
const handleAnalyze = async () => {
setError(null);
setSuccess(null);
setLoading(true);
try {
// Validate and fix URL format if using URL tab
let requestUrl = url;
if (activeTab === 'url') {
const fixedUrl = fixUrlFormat(url);
if (!fixedUrl) {
setError('Please enter a valid website URL (starting with http:// or https://)');
setLoading(false);
return;
}
requestUrl = fixedUrl;
}
const requestData = {
url: activeTab === 'url' ? requestUrl : undefined,
text_sample: activeTab === 'text' ? textSample : undefined,
include_patterns: true,
include_guidelines: true
};
const response = await fetch('/api/onboarding/style-detection/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
});
const result = await response.json();
if (result.success) {
setAnalysis(result.style_analysis);
setSuccess('Style analysis completed successfully!');
} else {
setError(result.error || 'Analysis failed');
}
} catch (err) {
setError('Failed to analyze content. Please try again.');
} finally {
setLoading(false);
}
};
const fixUrlFormat = (url: string): string | null => {
if (!url) return null;
// Remove leading/trailing whitespace
let fixedUrl = url.trim();
// Check if URL already has a protocol but is missing slashes
if (fixedUrl.startsWith('https:/') && !fixedUrl.startsWith('https://')) {
fixedUrl = fixedUrl.replace('https:/', 'https://');
} else if (fixedUrl.startsWith('http:/') && !fixedUrl.startsWith('http://')) {
fixedUrl = fixedUrl.replace('http:/', 'http://');
}
// Add protocol if missing
if (!fixedUrl.startsWith('http://') && !fixedUrl.startsWith('https://')) {
fixedUrl = 'https://' + fixedUrl;
}
// Fix missing slash after protocol
if (fixedUrl.includes('://') && !fixedUrl.split('://')[1].startsWith('/')) {
fixedUrl = fixedUrl.replace('://', ':///');
}
// Ensure only two slashes after protocol
if (fixedUrl.includes(':///')) {
fixedUrl = fixedUrl.replace(':///', '://');
}
// Basic URL validation
try {
new URL(fixedUrl);
return fixedUrl;
} catch {
return null;
}
};
const handleContinue = () => {
if (analysis) {
onContinue();
} else {
setError('Please complete style analysis before continuing');
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const renderAnalysisSection = (title: string, data: any, icon: React.ReactNode) => (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}>
{icon}
<Typography variant="h6">{title}</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
{Object.entries(data).map(([key, value]) => (
<Grid item xs={12} sm={6} key={key}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Typography>
<Typography variant="body2">
{Array.isArray(value) ? value.join(', ') : String(value)}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
);
return (
<Box sx={classes.container}>
<Typography variant="h4" gutterBottom sx={classes.headerTitle}>
🎨 Style Detection
</Typography>
<Typography variant="body1" color="textSecondary" gutterBottom>
Analyze your writing style to get personalized content generation recommendations.
</Typography>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Source
</Typography>
<Box mb={3}>
<Button
variant={activeTab === 'url' ? 'contained' : 'outlined'}
onClick={() => setActiveTab('url')}
sx={{ mr: 2 }}
>
Website URL
</Button>
<Button
variant={activeTab === 'text' ? 'contained' : 'outlined'}
onClick={() => setActiveTab('text')}
>
Text Sample
</Button>
</Box>
{activeTab === 'url' ? (
<TextField
fullWidth
label="Website URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://yourwebsite.com"
helperText="Enter your website URL to analyze your content style"
margin="normal"
/>
) : (
<TextField
fullWidth
multiline
rows={6}
label="Text Sample"
value={textSample}
onChange={(e) => setTextSample(e.target.value)}
placeholder="Paste your content samples here..."
helperText="Provide 2-3 samples of your best content (min 50 characters)"
margin="normal"
/>
)}
<Box mt={3}>
<Button
variant="contained"
onClick={handleAnalyze}
disabled={loading || (!url && !textSample)}
startIcon={loading ? <CircularProgress size={20} /> : null}
fullWidth
>
{loading ? 'Analyzing...' : 'Analyze Style'}
</Button>
</Box>
</CardContent>
</Card>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
{success}
</Alert>
)}
{analysis && (
<Card sx={{ mt: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Style Analysis Results
</Typography>
{analysis.writing_style && renderAnalysisSection(
'Writing Style',
analysis.writing_style,
<InfoIcon color="primary" />
)}
{analysis.content_characteristics && renderAnalysisSection(
'Content Characteristics',
analysis.content_characteristics,
<InfoIcon color="secondary" />
)}
{analysis.target_audience && renderAnalysisSection(
'Target Audience',
analysis.target_audience,
<InfoIcon color="success" />
)}
{analysis.recommended_settings && renderAnalysisSection(
'Recommended Settings',
analysis.recommended_settings,
<CheckIcon color="primary" />
)}
</CardContent>
</Card>
)}
<Box mt={3} display="flex" justifyContent="space-between">
<Button variant="outlined" disabled>
Previous
</Button>
<Button
variant="contained"
onClick={handleContinue}
disabled={!analysis}
endIcon={<CheckIcon />}
>
Continue
</Button>
</Box>
</Box>
);
};
export default StyleDetectionStep;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,557 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Box,
Stepper,
Step,
StepLabel,
Button,
Typography,
Paper,
LinearProgress,
Fade,
Slide,
useTheme,
useMediaQuery,
IconButton,
Tooltip,
Container
} from '@mui/material';
import {
ArrowBack,
ArrowForward,
CheckCircle,
HelpOutline,
Close
} from '@mui/icons-material';
import { startOnboarding, getCurrentStep, setCurrentStep, getProgress } from '../../api/onboarding';
import ApiKeyStep from './ApiKeyStep';
import WebsiteStep from './WebsiteStep';
import ResearchStep from './ResearchStep';
import PersonalizationStep from './PersonalizationStep';
import IntegrationsStep from './IntegrationsStep';
import FinalStep from './FinalStep';
const steps = [
{ label: 'API Keys', description: 'Connect your AI services', icon: '🔑' },
{ label: 'Website', description: 'Set up your website', icon: '🌐' },
{ label: 'Research', description: 'Configure research tools', icon: '🔍' },
{ label: 'Personalization', description: 'Customize your experience', icon: '⚙️' },
{ label: 'Integrations', description: 'Connect additional services', icon: '🔗' },
{ label: 'Finish', description: 'Complete setup', icon: '✅' }
];
interface WizardProps {
onComplete?: () => void;
}
interface StepHeaderContent {
title: string;
description: string;
}
const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(true);
const [progress, setProgressState] = useState(0);
const [direction, setDirection] = useState<'left' | 'right'>('right');
const [showHelp, setShowHelp] = useState(false);
const [showProgressMessage, setShowProgressMessage] = useState(false);
const [progressMessage, setProgressMessage] = useState('');
const [stepHeaderContent, setStepHeaderContent] = useState<StepHeaderContent>({
title: steps[0].label,
description: steps[0].description
});
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
console.log('Wizard: Component mounted');
const init = async () => {
try {
setLoading(true);
console.log('Wizard: Starting initialization...');
// Check if there's existing progress first
const stepResponse = await getCurrentStep();
console.log('Wizard: Backend returned step:', stepResponse.step);
// Only start onboarding if we're at step 1 (no progress)
if (stepResponse.step === 1) {
console.log('Wizard: No existing progress, starting new onboarding');
await startOnboarding();
} else {
console.log('Wizard: Existing progress found, continuing from step:', stepResponse.step);
}
// Get the current step and progress
const finalStepResponse = await getCurrentStep();
const progressResponse = await getProgress();
console.log('Wizard: Final step:', finalStepResponse.step);
console.log('Wizard: Backend returned progress:', progressResponse.progress);
console.log('Wizard: Setting activeStep to:', finalStepResponse.step - 1);
setActiveStep(finalStepResponse.step - 1);
setProgressState(progressResponse.progress);
console.log('Wizard: Initialization complete');
} catch (error) {
console.error('Error initializing onboarding:', error);
} finally {
setLoading(false);
}
};
init();
}, []);
const handleNext = async () => {
console.log('Wizard: handleNext called');
console.log('Wizard: Current activeStep:', activeStep);
console.log('Wizard: Steps length:', steps.length);
setDirection('right');
const nextStep = activeStep + 1;
console.log('Wizard: Next step will be:', nextStep);
// Show progress message
const newProgress = ((nextStep + 1) / steps.length) * 100;
setProgressMessage(`Your data is saved, moving to the next step. Progress is ${Math.round(newProgress)}%`);
setShowProgressMessage(true);
// Hide message after 3 seconds
setTimeout(() => {
setShowProgressMessage(false);
}, 3000);
// Complete the current step (activeStep + 1 because steps are 1-indexed)
const currentStepNumber = activeStep + 1;
console.log('Wizard: Completing current step:', currentStepNumber);
await setCurrentStep(currentStepNumber);
// Check what step the backend thinks we should be on after completion
console.log('Wizard: Checking backend step after completion...');
const stepResponse = await getCurrentStep();
console.log('Wizard: Backend says current step should be:', stepResponse.step);
setActiveStep(nextStep);
console.log('Wizard: Setting activeStep to:', nextStep);
// Update progress
setProgressState(newProgress);
// If this is the final step, call onComplete
if (nextStep === steps.length - 1) {
console.log('Wizard: This is the final step, calling onComplete');
onComplete?.();
} else {
console.log('Wizard: Not the final step, continuing to next step');
}
};
const handleBack = async () => {
setDirection('left');
const prevStep = activeStep - 1;
setActiveStep(prevStep);
await setCurrentStep(prevStep + 1);
// Update progress
const newProgress = ((prevStep + 1) / steps.length) * 100;
setProgressState(newProgress);
};
const handleStepClick = (stepIndex: number) => {
if (stepIndex <= activeStep) {
setDirection(stepIndex > activeStep ? 'right' : 'left');
setActiveStep(stepIndex);
setCurrentStep(stepIndex + 1);
}
};
const updateHeaderContent = useCallback((content: StepHeaderContent) => {
setStepHeaderContent(content);
}, []);
const handleComplete = async () => {
console.log('Wizard: handleComplete called - completing onboarding');
try {
// Call onComplete to notify parent component
onComplete?.();
} catch (error) {
console.error('Error completing onboarding:', error);
}
};
const renderStepContent = (step: number) => {
const stepComponents = [
<ApiKeyStep key="api-keys" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<WebsiteStep key="website" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<ResearchStep key="research" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<PersonalizationStep key="personalization" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<IntegrationsStep key="integrations" onContinue={handleNext} updateHeaderContent={updateHeaderContent} />,
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
];
return (
<Slide direction={direction} in={true} mountOnEnter unmountOnExit>
<Box sx={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
{stepComponents[step]}
</Box>
</Slide>
);
};
if (loading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<Fade in={true}>
<Paper
elevation={24}
sx={{
p: 4,
borderRadius: 3,
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
maxWidth: 400,
width: '100%',
}}
>
<Typography variant="h5" align="center" gutterBottom sx={{ fontWeight: 600 }}>
Setting up your workspace...
</Typography>
<LinearProgress
sx={{
mt: 3,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
}}
/>
</Paper>
</Fade>
</Box>
);
}
return (
<Box
sx={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: { xs: 2, md: 4 },
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
<Paper
elevation={24}
sx={{
maxWidth: { xs: '100%', md: '1200px' },
width: '100%',
borderRadius: 4,
overflow: 'hidden',
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.3)',
position: 'relative',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
}}
>
{/* Header with Stepper */}
<Box
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
p: { xs: 3, md: 4 },
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
}
}}
>
{/* Progress Message */}
{showProgressMessage && (
<Fade in={showProgressMessage}>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
background: 'rgba(16, 185, 129, 0.9)',
color: 'white',
p: 2,
textAlign: 'center',
zIndex: 10,
backdropFilter: 'blur(10px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{progressMessage}
</Typography>
</Box>
</Fade>
)}
{/* Top Row - Title and Actions */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ flex: 1 }} />
<Box sx={{ flex: 2, textAlign: 'center' }}>
<Typography variant="h4" sx={{ fontWeight: 700, letterSpacing: '-0.025em' }}>
{stepHeaderContent.title}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, flex: 1, justifyContent: 'flex-end' }}>
<Tooltip title="Get Help" arrow>
<IconButton
onClick={() => setShowHelp(!showHelp)}
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<HelpOutline />
</IconButton>
</Tooltip>
<Tooltip title="Skip for now" arrow>
<IconButton
sx={{
color: 'white',
bgcolor: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(10px)',
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.2)',
}
}}
>
<Close />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Progress Bar */}
<Box sx={{ mb: 3, position: 'relative', zIndex: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 500 }}>
Setup Progress
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, fontWeight: 600 }}>
{Math.round(progress)}% Complete
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.2)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}
}}
/>
</Box>
{/* Stepper in Header */}
<Box sx={{ position: 'relative', zIndex: 1 }}>
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
sx={{
'& .MuiStepLabel-root': {
cursor: 'pointer',
},
'& .MuiStepLabel-label': {
fontSize: '0.875rem',
fontWeight: 600,
color: 'white',
},
'& .MuiStepLabel-labelContainer': {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
'& .MuiStepLabel-label.Mui-completed': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiStepLabel-label.Mui-active': {
color: 'white',
},
'& .MuiStepLabel-label.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
}}
>
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel
onClick={() => handleStepClick(index)}
sx={{
cursor: index <= activeStep ? 'pointer' : 'default',
'& .MuiStepLabel-iconContainer': {
background: index <= activeStep
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: '50%',
width: 40,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: index <= activeStep ? 'white' : 'rgba(255, 255, 255, 0.6)',
fontSize: '1.2rem',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: index <= activeStep
? '0 4px 12px rgba(255, 255, 255, 0.2)'
: 'none',
'&:hover': {
transform: index <= activeStep ? 'scale(1.05)' : 'none',
boxShadow: index <= activeStep
? '0 6px 16px rgba(255, 255, 255, 0.3)'
: 'none',
}
},
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
<Typography variant="h6" sx={{ mb: 0.5 }}>
{step.icon}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'center' }}>
{step.label}
</Typography>
</Box>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
</Box>
{/* Content */}
<Box sx={{ p: { xs: 2, md: 3 }, pt: 2 }}>
<Fade in={true} timeout={400}>
<Box>
{renderStepContent(activeStep)}
</Box>
</Fade>
</Box>
{/* Navigation */}
<Box
sx={{
p: { xs: 2, md: 3 },
pt: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderTop: '1px solid rgba(0,0,0,0.08)',
background: 'rgba(0,0,0,0.02)',
}}
>
<Button
variant="outlined"
onClick={handleBack}
disabled={activeStep === 0}
startIcon={<ArrowBack />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
}}
>
Back
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ opacity: 0.7, fontWeight: 500 }}>
Step {activeStep + 1} of {steps.length}
</Typography>
{activeStep === steps.length - 1 && (
<CheckCircle sx={{ color: 'success.main', fontSize: 20 }} />
)}
</Box>
<Button
variant="contained"
onClick={handleNext}
disabled={activeStep === steps.length - 1}
endIcon={activeStep === steps.length - 1 ? <CheckCircle /> : <ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
{activeStep === steps.length - 1 ? 'Complete Setup' : 'Continue'}
</Button>
</Box>
</Paper>
</Box>
);
};
export default Wizard;

View File

@@ -0,0 +1,165 @@
import React from 'react';
import { Button, Box, CircularProgress } from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingButtonProps {
variant?: 'primary' | 'secondary' | 'text';
loading?: boolean;
children: ReactNode;
icon?: ReactNode;
iconPosition?: 'start' | 'end';
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
fullWidth?: boolean;
size?: 'small' | 'medium' | 'large';
[key: string]: any;
}
const OnboardingButton: React.FC<OnboardingButtonProps> = ({
variant = 'primary',
loading = false,
children,
icon,
iconPosition = 'start',
onClick,
disabled,
type = 'button',
fullWidth = false,
size = 'medium',
...props
}) => {
const baseStyles = {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const,
overflow: 'hidden' as const,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
opacity: 0,
transition: 'opacity 0.3s ease',
},
'&:hover::before': {
opacity: 1,
},
};
const getStyles = () => {
const sizeStyles = {
small: { px: 2, py: 1, fontSize: '0.875rem' },
medium: { px: 3, py: 1.5, fontSize: '1rem' },
large: { px: 4, py: 2, fontSize: '1.125rem' },
};
switch (variant) {
case 'primary':
return {
...baseStyles,
...sizeStyles[size],
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
color: 'white',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:active': {
transform: 'translateY(0px)',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
},
};
case 'secondary':
return {
...baseStyles,
...sizeStyles[size],
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
background: 'transparent',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
},
'&:active': {
transform: 'translateY(0px)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
background: 'transparent',
transform: 'none',
boxShadow: 'none',
}
};
case 'text':
return {
...baseStyles,
...sizeStyles[size],
color: 'primary.main',
background: 'transparent',
'&:hover': {
background: 'rgba(102, 126, 234, 0.08)',
transform: 'translateY(-1px)',
},
'&:active': {
transform: 'translateY(0px)',
},
'&:disabled': {
color: 'rgba(0,0,0,0.3)',
background: 'transparent',
transform: 'none',
}
};
default:
return baseStyles;
}
};
const buttonVariant = variant === 'primary' ? 'contained' : variant === 'secondary' ? 'outlined' : 'text';
return (
<Button
variant={buttonVariant}
onClick={onClick}
disabled={loading || disabled}
type={type}
fullWidth={fullWidth}
startIcon={iconPosition === 'start' && icon && !loading ? icon : undefined}
endIcon={iconPosition === 'end' && icon && !loading ? icon : undefined}
sx={getStyles()}
{...props}
>
{loading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress
size={size === 'small' ? 16 : size === 'large' ? 24 : 20}
color="inherit"
thickness={4}
/>
{children}
</Box>
) : (
children
)}
</Button>
);
};
export default OnboardingButton;

View File

@@ -0,0 +1,185 @@
import React from 'react';
import {
Card,
CardContent,
Box,
Typography,
Chip,
Zoom,
useTheme,
Paper
} from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingCardProps {
title: string;
icon: ReactNode;
children: ReactNode;
status?: 'valid' | 'invalid' | 'empty';
statusLabel?: string;
elevation?: number;
delay?: number;
saved?: boolean;
variant?: 'default' | 'info' | 'warning' | 'success';
}
const OnboardingCard: React.FC<OnboardingCardProps> = ({
title,
icon,
children,
status,
statusLabel,
elevation = 2,
delay = 0,
saved = false,
variant = 'default'
}) => {
const theme = useTheme();
const getStatusColor = () => {
switch (status) {
case 'valid':
return '#10b981';
case 'invalid':
return '#ef4444';
default:
return 'transparent';
}
};
const getStatusChip = () => {
if (!status || status === 'empty') return null;
return (
<Chip
icon={status === 'valid' ? <Box component="span"></Box> : <Box component="span"></Box>}
label={statusLabel || (status === 'valid' ? 'Valid' : 'Invalid')}
color={status === 'valid' ? 'success' : 'error'}
size="small"
sx={{ fontWeight: 600, borderRadius: 1 }}
/>
);
};
const getVariantStyles = () => {
switch (variant) {
case 'info':
return {
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
};
case 'warning':
return {
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
};
case 'success':
return {
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
};
default:
return {
background: 'white',
border: `2px solid ${getStatusColor()}`,
};
}
};
return (
<Zoom in={true} timeout={700 + delay}>
<Card
elevation={elevation}
sx={{
...getVariantStyles(),
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden',
'&:hover': {
elevation: 4,
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
},
'&::before': variant === 'default' ? {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: status === 'valid'
? 'linear-gradient(90deg, #10b981 0%, #059669 100%)'
: status === 'invalid'
? 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(90deg, #6b7280 0%, #4b5563 100%)',
} : {},
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1 }}>
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: variant === 'default'
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: variant === 'info'
? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'
: variant === 'warning'
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}>
{React.cloneElement(icon as React.ReactElement, {
sx: { color: 'white', fontSize: 20 }
})}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
{title}
</Typography>
{variant !== 'default' && (
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4 }}>
{children}
</Typography>
)}
</Box>
</Box>
{getStatusChip()}
</Box>
{variant === 'default' && children}
{saved && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1.5 }}>
<Box component="span" sx={{
width: 16,
height: 16,
borderRadius: '50%',
background: '#10b981',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
color: 'white',
fontWeight: 'bold'
}}>
</Box>
<Typography variant="caption" color="success.main" sx={{ fontWeight: 500 }}>
Already saved and secured
</Typography>
</Box>
)}
</CardContent>
</Card>
</Zoom>
);
};
export default OnboardingCard;

View File

@@ -0,0 +1,124 @@
import React from 'react';
import {
Box,
Typography,
Fade,
Zoom,
useTheme,
Container
} from '@mui/material';
import { ReactNode } from 'react';
interface OnboardingStepLayoutProps {
icon: ReactNode;
title: string;
subtitle: string;
children: ReactNode;
maxWidth?: number | string;
showIcon?: boolean;
centered?: boolean;
}
const OnboardingStepLayout: React.FC<OnboardingStepLayoutProps> = ({
icon,
title,
subtitle,
children,
maxWidth = 800,
showIcon = true,
centered = true
}) => {
const theme = useTheme();
return (
<Fade in={true} timeout={500}>
<Container maxWidth="lg" sx={{ py: 2 }}>
{/* Header */}
<Box sx={{
textAlign: centered ? 'center' : 'left',
mb: 4,
maxWidth: maxWidth,
mx: centered ? 'auto' : 0
}}>
<Zoom in={true} timeout={600}>
<Box>
{showIcon && (
<Box sx={{
mb: 3,
display: 'flex',
justifyContent: centered ? 'center' : 'flex-start',
position: 'relative'
}}>
<Box sx={{
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 25px rgba(102, 126, 234, 0.3)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
opacity: 0.3,
zIndex: -1,
}
}}>
{React.cloneElement(icon as React.ReactElement, {
sx: { fontSize: 36, color: 'white' }
})}
</Box>
</Box>
)}
<Typography
variant="h4"
gutterBottom
sx={{
fontWeight: 700,
mb: 2,
letterSpacing: '-0.025em',
color: 'text.primary'
}}
>
{title}
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{
lineHeight: 1.6,
maxWidth: 600,
mx: centered ? 'auto' : 0,
fontSize: '1.1rem'
}}
>
{subtitle}
</Typography>
</Box>
</Zoom>
</Box>
{/* Content */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: 3,
maxWidth: maxWidth,
mx: centered ? 'auto' : 0
}}>
{children}
</Box>
</Container>
</Fade>
);
};
export default OnboardingStepLayout;

View File

@@ -0,0 +1,104 @@
// Validation utilities
export const validateApiKey = (key: string, provider: string): boolean | null => {
if (!key.trim()) return null;
const patterns = {
openai: /^sk-[a-zA-Z0-9]{32,}$/,
gemini: /^AIza[a-zA-Z0-9_-]{35}$/,
anthropic: /^sk-ant-[a-zA-Z0-9]{32,}$/,
mistral: /^[a-zA-Z0-9]{32,}$/,
};
const pattern = patterns[provider as keyof typeof patterns];
return pattern ? pattern.test(key) : true;
};
export const getKeyStatus = (key: string, provider: string): 'valid' | 'invalid' | 'empty' => {
if (!key.trim()) return 'empty';
const isValid = validateApiKey(key, provider);
return isValid ? 'valid' : 'invalid';
};
// Animation utilities
export const getAnimationDelay = (index: number, baseDelay: number = 100): number => {
return baseDelay * index;
};
export const getSlideDirection = (currentStep: number, targetStep: number): 'left' | 'right' => {
return targetStep > currentStep ? 'right' : 'left';
};
// Progress utilities
export const calculateProgress = (currentStep: number, totalSteps: number): number => {
return ((currentStep + 1) / totalSteps) * 100;
};
// Form utilities
export const isFormValid = (values: Record<string, string>): boolean => {
return Object.values(values).some(value => value.trim() !== '');
};
// Status utilities
export const getStatusColor = (status: 'valid' | 'invalid' | 'empty'): string => {
switch (status) {
case 'valid':
return '#4caf50';
case 'invalid':
return '#f44336';
default:
return 'transparent';
}
};
export const getStatusLabel = (status: 'valid' | 'invalid' | 'empty'): string => {
switch (status) {
case 'valid':
return 'Valid';
case 'invalid':
return 'Invalid';
default:
return '';
}
};
// Auto-save utilities
export const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
};
// Error handling utilities
export const formatErrorMessage = (error: any): string => {
if (typeof error === 'string') return error;
if (error?.message) return error.message;
return 'An unexpected error occurred. Please try again.';
};
// URL validation
export const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Text validation
export const validateRequired = (value: string): boolean => {
return value.trim().length > 0;
};
export const validateMinLength = (value: string, minLength: number): boolean => {
return value.trim().length >= minLength;
};
export const validateMaxLength = (value: string, maxLength: number): boolean => {
return value.trim().length <= maxLength;
};

View File

@@ -0,0 +1,242 @@
import { useTheme } from '@mui/material';
export const useOnboardingStyles = () => {
const theme = useTheme();
const styles = {
// Layout styles
container: {
maxWidth: 800,
mx: 'auto',
},
// Header styles
header: {
textAlign: 'center',
mb: 4,
},
headerIcon: {
fontSize: 64,
color: 'primary.main',
mb: 2,
},
headerIconContainer: {
width: 80,
height: 80,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 25px rgba(102, 126, 234, 0.3)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
opacity: 0.3,
zIndex: -1,
}
},
headerTitle: {
fontWeight: 700,
letterSpacing: '-0.025em',
},
headerSubtitle: {
color: 'text.secondary',
lineHeight: 1.6,
maxWidth: 600,
mx: 'auto',
},
// Card styles
card: {
elevation: 2,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
borderRadius: 3,
'&:hover': {
elevation: 4,
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
},
},
cardContent: {
p: 3,
},
cardHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
},
cardTitle: {
display: 'flex',
alignItems: 'center',
gap: 1.5,
},
cardIconContainer: {
width: 40,
height: 40,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
// Button styles
primaryButton: {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
px: 4,
py: 1.5,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
},
},
secondaryButton: {
borderRadius: 2,
textTransform: 'none' as const,
fontWeight: 600,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
},
'&:disabled': {
borderColor: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.3)',
}
},
textButton: {
textTransform: 'none' as const,
fontWeight: 600,
},
// Form styles
textField: {
'& .MuiOutlinedInput-root': {
borderRadius: 2,
},
'& .MuiInputBase-input': {
padding: '12px 16px',
},
},
// Alert styles
alert: {
borderRadius: 2,
'& .MuiAlert-icon': {
fontSize: 20,
},
},
// Paper styles
infoPaper: {
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
border: '1px solid rgba(59, 130, 246, 0.2)',
borderRadius: 2,
},
warningPaper: {
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid rgba(245, 158, 11, 0.2)',
borderRadius: 2,
},
successPaper: {
background: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderRadius: 2,
},
// Progress styles
progressBar: {
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0,0,0,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
}
},
// Chip styles
chip: {
fontWeight: 600,
borderRadius: 1,
},
// Divider styles
divider: {
my: 2,
opacity: 0.6,
},
// Link styles
link: {
fontWeight: 600,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
// Animation styles
fadeIn: {
animation: 'fadeIn 0.5s ease-in-out',
},
slideUp: {
animation: 'slideUp 0.3s ease-out',
},
// Responsive styles
responsiveContainer: {
maxWidth: { xs: '100%', md: 800 },
mx: 'auto',
px: { xs: 2, md: 3 },
},
// Spacing utilities
sectionSpacing: {
mb: 4,
},
cardSpacing: {
gap: 3,
},
buttonSpacing: {
gap: 2,
},
};
return styles;
};

View File

@@ -0,0 +1,257 @@
# 🏗️ Dashboard Components Architecture
## 📋 Overview
This directory contains a modular, reusable architecture for dashboard components following React best practices. The architecture promotes code reusability, maintainability, and type safety.
## 🎯 Architecture Principles
### **1. Modularity**
- **Single Responsibility**: Each component has one clear purpose
- **Composition over Inheritance**: Components are built by combining smaller, focused components
- **Separation of Concerns**: UI, logic, and data are separated
### **2. Reusability**
- **Shared Components**: Common UI elements are extracted into reusable components
- **Shared Utilities**: Common functions are centralized
- **Shared Types**: TypeScript interfaces are shared across components
### **3. Maintainability**
- **Clear Structure**: Organized file structure with logical grouping
- **Type Safety**: Full TypeScript support with proper interfaces
- **Consistent Styling**: Shared styled components for consistent design
## 📁 Directory Structure
```
components/
├── shared/ # Shared components and utilities
│ ├── components/ # Reusable UI components
│ ├── styled.ts # Shared styled components
│ ├── types.ts # Shared TypeScript interfaces
│ ├── utils.ts # Shared utility functions
│ └── index.ts # Barrel exports
├── MainDashboard/ # Main dashboard implementation
│ └── MainDashboard.tsx # Main dashboard component
├── SEODashboard/ # SEO dashboard implementation
│ ├── components/ # SEO-specific components
│ └── SEODashboard.tsx # SEO dashboard component
└── README.md # This documentation
```
## 🔧 Shared Components
### **DashboardHeader**
- **Purpose**: Consistent header across all dashboards
- **Props**: `title`, `subtitle`, `statusChips`
- **Features**: Shimmer animation, gradient text, status indicators
### **SearchFilter**
- **Purpose**: Search and category filtering functionality
- **Props**: Search state, category state, callbacks
- **Features**: Real-time search, category chips, sub-category filtering
### **ToolCard**
- **Purpose**: Display individual tools with consistent styling
- **Props**: Tool data, click handlers, favorite state
- **Features**: Hover animations, pinned indicators, status badges
### **CategoryHeader**
- **Purpose**: Display category information with enhanced styling
- **Props**: Category name, category data, theme
- **Features**: Gradient borders, tool counts, sub-category info
### **LoadingSkeleton**
- **Purpose**: Consistent loading states across dashboards
- **Props**: Item count, heights, customization
- **Features**: Responsive grid, customizable dimensions
### **ErrorDisplay**
- **Purpose**: Consistent error handling and display
- **Props**: Error message, retry callback
- **Features**: Retry functionality, consistent styling
### **EmptyState**
- **Purpose**: Display when no data is available
- **Props**: Title, message, clear filters callback
- **Features**: Clear filters functionality, consistent messaging
## 🎨 Shared Styled Components
### **DashboardContainer**
- Glassmorphic background with gradient
- Animated background patterns
- Responsive padding and positioning
### **GlassCard**
- Backdrop blur effects
- Hover animations and transitions
- Consistent border radius and shadows
### **ShimmerHeader**
- Animated shimmer effect
- Gradient text support
- Status chip integration
### **SearchContainer**
- Glassmorphic search interface
- Responsive design
- Hover effects and transitions
### **CategoryChip**
- Active state styling
- Hover animations
- Consistent typography
## 📊 Shared Types
### **Core Interfaces**
- `Tool`: Individual tool data structure
- `Category`: Category data with tools or sub-categories
- `ToolCategories`: Main categories object
- `DashboardState`: Complete dashboard state management
### **Component Props**
- `ToolCardProps`: Tool card component props
- `SearchFilterProps`: Search and filter component props
- `DashboardHeaderProps`: Header component props
### **State Management**
- `SnackbarState`: Notification state
- `DashboardState`: Complete dashboard state
## 🛠️ Shared Utilities
### **Data Processing**
- `getToolsForCategory()`: Extract tools from categories
- `getFilteredCategories()`: Filter categories based on search
- `getStatusConfig()`: Get status styling configuration
### **Formatting**
- `formatNumber()`: Format large numbers (K, M)
- `capitalizeFirst()`: Capitalize first letter
- `formatPlatformName()`: Format platform names
### **Status Helpers**
- `getStatusColor()`: Get color for status
- `getStatusIcon()`: Get icon for status
## 🎣 Custom Hooks
### **useDashboardState**
- **Purpose**: Centralized dashboard state management
- **Features**:
- Favorites management with localStorage
- Search and filter state
- Snackbar notifications
- Error handling
- Loading states
## 📦 Data Management
### **toolCategories.ts**
- **Purpose**: Centralized tool data management
- **Features**:
- Type-safe tool definitions
- Sub-category organization
- Icon and styling configuration
- Easy to extend and modify
## 🚀 Usage Examples
### **Basic Dashboard Implementation**
```typescript
import {
DashboardHeader,
SearchFilter,
ToolCard,
useDashboardState
} from '../shared';
const MyDashboard = () => {
const { state, toggleFavorite, setSearchQuery } = useDashboardState();
return (
<DashboardContainer>
<DashboardHeader title="My Dashboard" subtitle="Description" />
<SearchFilter {...searchProps} />
{/* Tool cards */}
</DashboardContainer>
);
};
```
### **Custom Component with Shared Styling**
```typescript
import { GlassCard } from '../shared';
const MyComponent = () => (
<GlassCard>
<Box sx={{ p: 3 }}>
{/* Content */}
</Box>
</GlassCard>
);
```
## 🔄 Migration Benefits
### **Before (Monolithic)**
- ❌ Large, hard-to-maintain components
- ❌ Duplicated code across dashboards
- ❌ Inconsistent styling
- ❌ Difficult to test
- ❌ Poor type safety
### **After (Modular)**
- ✅ Small, focused components
- ✅ Shared code and utilities
- ✅ Consistent design system
- ✅ Easy to test individual components
- ✅ Full TypeScript support
- ✅ Better performance through code splitting
## 🎯 Best Practices
### **1. Component Design**
- Keep components small and focused
- Use composition over inheritance
- Implement proper TypeScript interfaces
- Follow consistent naming conventions
### **2. State Management**
- Use custom hooks for complex state
- Centralize shared state logic
- Implement proper error boundaries
- Use localStorage for persistence
### **3. Styling**
- Use shared styled components
- Maintain consistent design tokens
- Implement responsive design
- Use proper animation timing
### **4. Performance**
- Implement proper memoization
- Use code splitting for large components
- Optimize re-renders with React.memo
- Lazy load non-critical components
## 🔮 Future Enhancements
### **Planned Improvements**
- [ ] Add more shared components (charts, tables, forms)
- [ ] Implement theme system for dark/light modes
- [ ] Add accessibility improvements
- [ ] Create component documentation with Storybook
- [ ] Add unit tests for all shared components
### **Extensibility**
- Easy to add new dashboard types
- Simple to extend with new features
- Flexible for different use cases
- Scalable architecture
---
This modular architecture provides a solid foundation for building maintainable, scalable dashboard applications with excellent developer experience and user interface consistency.

View File

@@ -0,0 +1,205 @@
import React, { useEffect } from 'react';
import {
Box,
Container,
Grid,
Typography,
Alert,
Skeleton,
useTheme
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
// Shared components
import { DashboardContainer, GlassCard } from '../shared/styled';
import SEOAnalyzerPanel from './components/SEOAnalyzerPanel';
// Zustand store
import { useSEODashboardStore } from '../../stores/seoDashboardStore';
// API
import { userDataAPI } from '../../api/userData';
// SEO Dashboard component
const SEODashboard: React.FC = () => {
const theme = useTheme();
// Zustand store hooks
const {
loading,
error,
data,
analysisData,
analysisLoading,
analysisError,
setData,
setLoading,
setError,
runSEOAnalysis,
checkAndRunInitialAnalysis,
} = useSEODashboardStore();
useEffect(() => {
// Simulate fetching dashboard data
const fetchData = async () => {
setLoading(true);
try {
// Try to get the website URL from the database
let websiteUrl = null;
try {
websiteUrl = await userDataAPI.getWebsiteURL();
console.log('Fetched website URL from database:', websiteUrl);
} catch (error) {
console.warn('Could not fetch website URL from database:', error);
}
// Mock data for now
const mockData = {
health_score: {
score: 85,
change: 5,
trend: 'up',
label: 'GOOD',
color: '#4CAF50'
},
key_insight: 'Your SEO is performing well with room for improvement',
priority_alert: 'No critical issues detected',
metrics: {
traffic: { value: 12500, change: 12, trend: 'up', description: 'Organic traffic', color: '#4CAF50' },
rankings: { value: 8.5, change: -0.3, trend: 'down', description: 'Average ranking', color: '#2196F3' },
mobile: { value: 92, change: 3, trend: 'up', description: 'Mobile speed', color: '#FF9800' },
keywords: { value: 150, change: 5, trend: 'up', description: 'Keywords tracked', color: '#9C27B0' }
},
platforms: {
google: { status: 'connected', connected: true, last_sync: '2024-01-15T10:30:00Z', data_points: 1250 },
bing: { status: 'connected', connected: true, last_sync: '2024-01-15T09:45:00Z', data_points: 850 },
yandex: { status: 'disconnected', connected: false }
},
ai_insights: [
{
insight: 'Consider adding more internal links to improve page authority',
priority: 'medium',
category: 'content',
action_required: false
},
{
insight: 'Mobile page speed could be optimized further',
priority: 'high',
category: 'performance',
action_required: true,
tool_path: '/seo-dashboard'
}
],
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');
setLoading(false);
}
};
fetchData();
}, [setData, setLoading, setError]);
useEffect(() => {
// Run initial SEO analysis if no data exists
if (!loading && !error && data) {
checkAndRunInitialAnalysis();
}
}, [loading, error, data, checkAndRunInitialAnalysis]);
if (loading) {
return <Skeleton variant="rectangular" height={200} />;
}
if (error || !data) {
return <Alert severity="error">Failed to load dashboard data</Alert>;
}
return (
<DashboardContainer>
<Container maxWidth="xl">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{/* Header */}
<Box sx={{ mb: 4 }}>
<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>
{/* Executive Summary */}
<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>
</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>
{/* SEO Analyzer Panel */}
<SEOAnalyzerPanel
analysisData={analysisData}
onRunAnalysis={runSEOAnalysis}
loading={analysisLoading}
error={analysisError}
/>
</motion.div>
</AnimatePresence>
</Container>
</DashboardContainer>
);
};
export default SEODashboard;

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { Box, Typography, Button, Avatar } from '@mui/material';
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
import { AIInsightsPanel as StyledAIInsightsPanel } from '../../shared/styled';
import { AIInsight } from '../../../api/seoDashboard';
interface AIInsightsPanelProps {
insights: AIInsight[];
}
const AIInsightsPanel: React.FC<AIInsightsPanelProps> = ({ insights }) => {
return (
<StyledAIInsightsPanel>
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<Avatar sx={{
background: 'linear-gradient(135deg, #667eea, #764ba2)',
width: 48,
height: 48
}}>
🤖
</Avatar>
<Box>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
AI SEO Assistant
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Analyzing your data...
</Typography>
</Box>
</Box>
<Typography variant="body1" sx={{
color: 'rgba(255, 255, 255, 0.9)',
mb: 3,
lineHeight: 1.6
}}>
💡 Based on your current performance, here are my recommendations:
</Typography>
<Box sx={{ mb: 3 }}>
{insights.map((insight, index) => (
<Box key={index} sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
mb: 2,
p: 2,
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: 2,
border: '1px solid rgba(255, 255, 255, 0.1)'
}}>
<CheckCircleIcon sx={{
color: '#4CAF50',
fontSize: 20,
mt: 0.5
}} />
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.8)',
flex: 1
}}>
{insight.insight}
</Typography>
</Box>
))}
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="contained"
size="small"
sx={{
background: 'linear-gradient(135deg, #667eea, #764ba2)',
color: 'white',
fontWeight: 600,
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8, #6a4190)',
},
}}
>
Optimize Now
</Button>
<Button
variant="outlined"
size="small"
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.3)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
background: 'rgba(255, 255, 255, 0.1)',
},
}}
>
Learn More
</Button>
</Box>
</Box>
</StyledAIInsightsPanel>
);
};
export default AIInsightsPanel;

View File

@@ -0,0 +1,83 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Grid,
Paper,
List,
ListItem,
ListItemIcon,
ListItemText
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon
} from '@mui/icons-material';
import { AnalysisDetailsDialogProps } from '../../shared/types';
import { getAnalysisDetails } from './seoUtils';
const AnalysisDetailsDialog: React.FC<AnalysisDetailsDialogProps> = ({
open,
onClose
}) => {
const analysisDetails = getAnalysisDetails();
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
>
<DialogTitle sx={{ color: 'white', fontWeight: 600 }}>
📊 SEO Analysis Details
</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 3 }}>
Our comprehensive SEO analyzer performs detailed tests across multiple categories to provide you with actionable insights.
</Typography>
<Grid container spacing={2}>
{analysisDetails.map((detail, index) => (
<Grid item xs={12} md={6} key={index}>
<Paper sx={{ p: 2, background: '#f8f9fa', height: '100%' }}>
<Typography variant="h6" sx={{ color: '#1976d2', mb: 1, fontWeight: 600 }}>
{detail.title}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 2 }}>
{detail.description}
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Tests Performed:
</Typography>
<List dense>
{detail.tests.map((test, testIndex) => (
<ListItem key={testIndex} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 24 }}>
<CheckCircleIcon sx={{ fontSize: 16, color: '#4CAF50' }} />
</ListItemIcon>
<ListItemText
primary={test}
primaryTypographyProps={{ variant: 'body2', fontSize: '0.875rem' }}
/>
</ListItem>
))}
</List>
</Paper>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default AnalysisDetailsDialog;

View File

@@ -0,0 +1,178 @@
import React from 'react';
import {
Box,
Typography,
Tabs,
Tab,
Badge
} from '@mui/material';
import {
ThumbUp as ThumbUpIcon,
ThumbDown as ThumbDownIcon,
Warning as WarningIcon2
} from '@mui/icons-material';
import { AnalysisTabsProps } from '../../shared/types';
import CategoryCard from './CategoryCard';
import TabPanel from './TabPanel';
const AnalysisTabs: React.FC<AnalysisTabsProps> = ({
categorizedData,
expandedCategories,
onToggleCategory,
onIssueClick,
onAIAction
}) => {
const [tabValue, setTabValue] = React.useState(0);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
return (
<Box sx={{ width: '100%' }}>
{/* Styled Tabs */}
<Box sx={{
borderBottom: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
mb: 2,
background: 'rgba(255, 255, 255, 0.03)',
borderRadius: 2,
p: 1
}}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant="fullWidth"
sx={{
'& .MuiTab-root': {
color: 'rgba(255, 255, 255, 0.7)',
fontWeight: 600,
fontSize: '0.875rem',
textTransform: 'none',
minHeight: 48,
'&.Mui-selected': {
color: 'white',
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 1,
},
},
'& .MuiTabs-indicator': {
display: 'none',
},
}}
>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ThumbUpIcon sx={{ color: '#388E3C' }} />
The Good
<Badge
badgeContent={categorizedData.good.length}
color="success"
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
/>
</Box>
}
/>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<WarningIcon2 sx={{ color: '#F57C00' }} />
The Bad
<Badge
badgeContent={categorizedData.bad.length}
color="warning"
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
/>
</Box>
}
/>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ThumbDownIcon sx={{ color: '#D32F2F' }} />
The Ugly
<Badge
badgeContent={categorizedData.ugly.length}
color="error"
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
/>
</Box>
}
/>
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
<Typography variant="h6" sx={{ color: '#388E3C', mb: 2, fontWeight: 600 }}>
Good Performance ({categorizedData.good.length} categories)
</Typography>
{categorizedData.good.length > 0 ? (
categorizedData.good.map(({ category, data }) =>
<CategoryCard
key={category}
category={category}
data={data}
isExpanded={expandedCategories.has(category)}
onToggle={onToggleCategory}
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
)
) : (
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
No excellent performing categories found
</Typography>
)}
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Typography variant="h6" sx={{ color: '#F57C00', mb: 2, fontWeight: 600 }}>
Needs Improvement ({categorizedData.bad.length} categories)
</Typography>
{categorizedData.bad.length > 0 ? (
categorizedData.bad.map(({ category, data }) =>
<CategoryCard
key={category}
category={category}
data={data}
isExpanded={expandedCategories.has(category)}
onToggle={onToggleCategory}
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
)
) : (
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
No categories needing improvement
</Typography>
)}
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Typography variant="h6" sx={{ color: '#D32F2F', mb: 2, fontWeight: 600 }}>
Critical Issues ({categorizedData.ugly.length} categories)
</Typography>
{categorizedData.ugly.length > 0 ? (
categorizedData.ugly.map(({ category, data }) =>
<CategoryCard
key={category}
category={category}
data={data}
isExpanded={expandedCategories.has(category)}
onToggle={onToggleCategory}
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
)
) : (
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
No critical issues found
</Typography>
)}
</TabPanel>
</Box>
);
};
export default AnalysisTabs;

View File

@@ -0,0 +1,136 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Chip,
LinearProgress,
Collapse,
IconButton,
Divider,
Box
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon
} from '@mui/icons-material';
import { CategoryCardProps } from '../../shared/types';
import { getCategoryIcon, getCategoryTitle, getStatusColor } from './seoUtils';
import IssueList from './IssueList';
const CategoryCard: React.FC<CategoryCardProps> = ({
category,
data,
isExpanded,
onToggle,
onIssueClick,
onAIAction
}) => {
const score = data.score;
const status = score >= 80 ? 'excellent' : score >= 60 ? 'good' : score >= 40 ? 'needs_improvement' : 'poor';
return (
<Card
sx={{
background: 'rgba(255, 255, 255, 0.08)',
border: '1px solid rgba(255, 255, 255, 0.15)',
cursor: 'pointer',
transition: 'all 0.3s ease',
mb: 2,
'&:hover': {
background: 'rgba(255, 255, 255, 0.12)',
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(0,0,0,0.3)',
},
}}
onClick={() => onToggle(category)}
>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
{getCategoryIcon(category)}
<Typography variant="subtitle2" sx={{ color: 'white', ml: 1, flex: 1, fontWeight: 600 }}>
{getCategoryTitle(category)}
</Typography>
<Chip
label={score}
size="small"
sx={{
backgroundColor: getStatusColor(status),
color: 'white',
fontWeight: 600,
fontSize: '0.75rem',
}}
/>
</Box>
<LinearProgress
variant="determinate"
value={score}
sx={{
height: 4,
borderRadius: 2,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: getStatusColor(status),
borderRadius: 2,
},
}}
/>
<IconButton
size="small"
sx={{
color: 'rgba(255, 255, 255, 0.7)',
mt: 1,
'&:hover': { color: 'white' }
}}
>
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</CardContent>
<Collapse in={isExpanded}>
<Divider sx={{ borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Box sx={{ p: 2, pt: 1 }}>
<IssueList
issues={data.issues || []}
type="critical"
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
<IssueList
issues={data.warnings || []}
type="warning"
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
<IssueList
issues={data.recommendations || []}
type="recommendation"
onIssueClick={onIssueClick}
onAIAction={onAIAction}
/>
{/* Show key metrics if available */}
{data.load_time && (
<Typography variant="caption" sx={{ color: '#666', display: 'block', mt: 1 }}>
Load Time: {data.load_time.toFixed(2)}s
</Typography>
)}
{data.word_count && (
<Typography variant="caption" sx={{ color: '#666', display: 'block' }}>
Words: {data.word_count}
</Typography>
)}
{data.total_headers !== undefined && (
<Typography variant="caption" sx={{ color: '#666', display: 'block' }}>
Security Headers: {data.total_headers}/6
</Typography>
)}
</Box>
</Collapse>
</Card>
);
};
export default CategoryCard;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import {
Paper,
Typography,
Button
} from '@mui/material';
import {
Build as BuildIcon
} from '@mui/icons-material';
import { CriticalIssueCardProps } from '../../shared/types';
import { formatMessage } from './seoUtils';
const CriticalIssueCard: React.FC<CriticalIssueCardProps> = ({
issue,
index,
onClick,
onAIAction
}) => {
const { title, details } = formatMessage(issue.message);
return (
<Paper sx={{
p: 2,
mb: 1,
background: 'rgba(211, 47, 47, 0.08)',
border: '1px solid rgba(211, 47, 47, 0.2)',
cursor: 'pointer',
'&:hover': { background: 'rgba(211, 47, 47, 0.12)' }
}}
onClick={() => onClick(issue)}
>
<Typography variant="subtitle2" sx={{ color: '#D32F2F', fontWeight: 600, mb: 1 }}>
{title}
</Typography>
{details && (
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.9)',
mb: 1,
fontSize: '0.875rem',
lineHeight: 1.4,
wordBreak: 'break-word'
}}>
{details}
</Typography>
)}
<Typography variant="caption" sx={{
color: 'rgba(255, 255, 255, 0.8)',
display: 'block',
mb: 1,
fontSize: '0.75rem'
}}>
Location: {issue.location}
</Typography>
<Button
size="small"
variant="contained"
startIcon={<BuildIcon />}
sx={{
backgroundColor: '#D32F2F',
'&:hover': { backgroundColor: '#B71C1C' }
}}
onClick={(e) => {
e.stopPropagation();
onAIAction(issue.action, issue);
}}
>
Fix with AI
</Button>
</Paper>
);
};
export default CriticalIssueCard;

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { Box, Typography, Chip, LinearProgress } from '@mui/material';
import { TrendingUp as TrendingUpIcon, TrendingDown as TrendingDownIcon } from '@mui/icons-material';
import { EnhancedGlassCard } from '../../shared/styled';
import { SEOHealthScore } from '../../../api/seoDashboard';
interface HealthScoreProps {
score: SEOHealthScore;
}
const HealthScore: React.FC<HealthScoreProps> = ({ score }) => {
return (
<EnhancedGlassCard>
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
🎯 SEO Health Score
</Typography>
<Chip
label={score.label}
size="small"
sx={{
background: `${score.color}20`,
color: score.color,
border: `1px solid ${score.color}40`,
fontWeight: 600,
}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Typography variant="h2" sx={{
color: 'white',
fontWeight: 800,
fontSize: { xs: '2.5rem', md: '3.5rem' }
}}>
{score.score}/100
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{score.trend === 'up' ? (
<TrendingUpIcon sx={{ color: '#4CAF50', fontSize: 24 }} />
) : (
<TrendingDownIcon sx={{ color: '#F44336', fontSize: 24 }} />
)}
<Typography variant="h6" sx={{
color: score.trend === 'up' ? '#4CAF50' : '#F44336',
fontWeight: 600
}}>
{score.trend === 'up' ? '+' : ''}{score.change} this month
</Typography>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={score.score}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
'& .MuiLinearProgress-bar': {
background: `linear-gradient(90deg, ${score.color}, ${score.color}80)`,
borderRadius: 4,
},
}}
/>
</Box>
</EnhancedGlassCard>
);
};
export default HealthScore;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Paper
} from '@mui/material';
import {
Build as BuildIcon
} from '@mui/icons-material';
import { IssueDetailsDialogProps } from '../../shared/types';
const IssueDetailsDialog: React.FC<IssueDetailsDialogProps> = ({
open,
issue,
onClose,
onAIAction
}) => {
if (!issue) return null;
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
>
<DialogTitle sx={{
color: issue.type === 'critical' ? '#D32F2F' :
issue.type === 'warning' ? '#F57C00' : '#388E3C',
fontWeight: 600
}}>
{issue.message}
</DialogTitle>
<DialogContent>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Location:
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{issue.location}
</Typography>
</Box>
{issue.current_value && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Current Value:
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{issue.current_value}
</Typography>
</Box>
)}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Recommended Fix:
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 1 }}>
{issue.fix}
</Typography>
{issue.code_example && (
<Paper sx={{ p: 2, background: '#f5f5f5', fontFamily: 'monospace', fontSize: '0.875rem' }}>
{issue.code_example}
</Paper>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
Close
</Button>
<Button
variant="contained"
startIcon={<BuildIcon />}
onClick={() => {
onAIAction(issue.action, issue);
onClose();
}}
sx={{
backgroundColor: issue.type === 'critical' ? '#D32F2F' :
issue.type === 'warning' ? '#F57C00' : '#388E3C',
'&:hover': {
backgroundColor: issue.type === 'critical' ? '#B71C1C' :
issue.type === 'warning' ? '#F57C00' : '#388E3C'
}
}}
>
Fix with AI
</Button>
</DialogActions>
</Dialog>
);
};
export default IssueDetailsDialog;

View File

@@ -0,0 +1,123 @@
import React from 'react';
import {
Box,
Typography,
List,
ListItem,
ListItemIcon,
ListItemText,
Button
} from '@mui/material';
import {
Error as ErrorIcon,
Warning as WarningIcon,
Info as InfoIcon,
PlayArrow as PlayArrowIcon
} from '@mui/icons-material';
import { IssueListProps } from '../../shared/types';
const IssueList: React.FC<IssueListProps> = ({
issues,
type,
onIssueClick,
onAIAction
}) => {
if (!issues || issues.length === 0) return null;
const colors = {
critical: '#D32F2F', // Softer red instead of bright #F44336
warning: '#F57C00', // Softer orange instead of bright #FF9800
recommendation: '#388E3C' // Softer green instead of bright #4CAF50
};
const icons = {
critical: <ErrorIcon sx={{ fontSize: 16, color: colors.critical }} />,
warning: <WarningIcon sx={{ fontSize: 16, color: colors.warning }} />,
recommendation: <InfoIcon sx={{ fontSize: 16, color: colors.recommendation }} />
};
const typeLabels = {
critical: 'Critical Issues',
warning: 'Warnings',
recommendation: 'Recommendations'
};
return (
<Box sx={{ mt: 1 }}>
<Typography variant="subtitle2" sx={{
color: colors[type],
fontWeight: 600,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 0.5
}}>
{icons[type]}
{typeLabels[type]} ({issues.length})
</Typography>
<List dense>
{issues.slice(0, 3).map((issue, index) => (
<ListItem
key={index}
sx={{
p: 1,
mb: 0.5,
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: 1,
cursor: 'pointer',
'&:hover': { background: 'rgba(255, 255, 255, 0.1)' }
}}
onClick={() => onIssueClick(issue)}
>
<ListItemIcon sx={{ minWidth: 32 }}>
{icons[type]}
</ListItemIcon>
<ListItemText
primary={issue.message}
secondary={`Location: ${issue.location}`}
primaryTypographyProps={{
variant: 'body2',
color: colors[type],
fontWeight: 500
}}
secondaryTypographyProps={{
variant: 'caption',
color: 'rgba(255, 255, 255, 0.7)'
}}
/>
<Button
size="small"
variant="outlined"
startIcon={<PlayArrowIcon />}
sx={{
color: colors[type],
borderColor: colors[type],
'&:hover': { borderColor: colors[type], backgroundColor: `${colors[type]}20` }
}}
onClick={(e) => {
e.stopPropagation();
onAIAction(issue.action, issue);
}}
>
Fix with AI
</Button>
</ListItem>
))}
{issues.length > 3 && (
<ListItem sx={{ p: 1 }}>
<ListItemText
primary={`... and ${issues.length - 3} more`}
primaryTypographyProps={{
variant: 'body2',
color: colors[type],
fontSize: '0.875rem'
}}
/>
</ListItem>
)}
</List>
</Box>
);
};
export default IssueList;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { TrendingUp as TrendingUpIcon, TrendingDown as TrendingDownIcon } from '@mui/icons-material';
import { GlassCard } from '../../shared/styled';
import { SEOMetric } from '../../../api/seoDashboard';
interface MetricCardProps {
title: string;
metric: SEOMetric;
icon: React.ReactNode;
color?: string;
}
const MetricCard: React.FC<MetricCardProps> = ({
title,
metric,
icon,
color = '#2196F3'
}) => {
return (
<GlassCard>
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Box
sx={{
width: 48,
height: 48,
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `${color}20`,
border: `1px solid ${color}40`,
}}
>
{icon}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{metric.trend === 'up' ? (
<TrendingUpIcon sx={{ color: '#4CAF50', fontSize: 20 }} />
) : (
<TrendingDownIcon sx={{ color: '#F44336', fontSize: 20 }} />
)}
<Typography variant="body2" sx={{
color: metric.trend === 'up' ? '#4CAF50' : '#F44336',
fontWeight: 600
}}>
{metric.change > 0 ? '+' : ''}{metric.change}%
</Typography>
</Box>
</Box>
<Typography variant="h4" sx={{
color: 'white',
fontWeight: 700,
mb: 1
}}>
{metric.value.toLocaleString()}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.8)',
mb: 2
}}>
{title}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontStyle: 'italic'
}}>
{metric.description}
</Typography>
</Box>
</GlassCard>
);
};
export default MetricCard;

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { Box, Typography, Chip, Button } from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Error as ErrorIcon,
Info as InfoIcon
} from '@mui/icons-material';
import { GlassCard } from '../../shared/styled';
import { PlatformStatus as PlatformStatusType } from '../../../api/seoDashboard';
import { getStatusColor, getStatusIcon } from '../../shared/utils';
interface PlatformStatusProps {
platforms: Record<string, PlatformStatusType>;
}
const PlatformStatus: React.FC<PlatformStatusProps> = ({ platforms }) => {
const getStatusIconComponent = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return <CheckCircleIcon />;
case 'good':
return <WarningIcon />;
case 'needs_action':
return <ErrorIcon />;
default:
return <InfoIcon />;
}
};
return (
<GlassCard>
<Box sx={{ p: 3 }}>
<Typography variant="h6" sx={{
color: 'white',
fontWeight: 600,
mb: 3
}}>
🌐 Platform Overview
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{Object.entries(platforms).map(([platform, data]) => (
<Box key={platform} sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 2,
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: 2,
border: '1px solid rgba(255, 255, 255, 0.1)'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{getStatusIconComponent(data.status)}
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.9)',
textTransform: 'capitalize'
}}>
{platform.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ').trim()}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={data.status.replace('_', ' ')}
size="small"
sx={{
background: `${getStatusColor(data.status)}20`,
color: getStatusColor(data.status),
border: `1px solid ${getStatusColor(data.status)}40`,
fontWeight: 600,
}}
/>
{data.connected && (
<Chip
label="Connected"
size="small"
sx={{
background: 'rgba(76, 175, 80, 0.2)',
color: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.4)',
fontWeight: 600,
}}
/>
)}
</Box>
</Box>
))}
</Box>
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
<Button
variant="outlined"
size="small"
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.3)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
background: 'rgba(255, 255, 255, 0.1)',
},
}}
>
View Detailed Analysis
</Button>
<Button
variant="outlined"
size="small"
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.3)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
background: 'rgba(255, 255, 255, 0.1)',
},
}}
>
Compare Platforms
</Button>
</Box>
</Box>
</GlassCard>
);
};
export default PlatformStatus;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import {
Alert,
IconButton
} from '@mui/material';
import {
Close as CloseIcon
} from '@mui/icons-material';
import { SEOAnalysisErrorProps } from '../../shared/types';
const SEOAnalysisError: React.FC<SEOAnalysisErrorProps> = ({
error,
showError,
onCloseError
}) => {
if (!error || !showError) return null;
return (
<Alert
severity="error"
sx={{ mb: 2 }}
action={
<IconButton
color="inherit"
size="small"
onClick={onCloseError}
>
<CloseIcon />
</IconButton>
}
>
{error}
</Alert>
);
};
export default SEOAnalysisError;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import {
Box,
Typography,
LinearProgress
} from '@mui/material';
import { SEOAnalysisLoadingProps } from '../../shared/types';
const SEOAnalysisLoading: React.FC<SEOAnalysisLoadingProps> = ({ loading }) => {
if (!loading) return null;
return (
<Box sx={{ mb: 3 }}>
<Typography variant="body1" sx={{ color: 'white', mb: 2 }}>
🤖 AI is analyzing your website...
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 2 }}>
Identifying specific issues and generating actionable fixes...
</Typography>
<LinearProgress
sx={{
height: 6,
borderRadius: 3,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
'& .MuiLinearProgress-bar': {
background: 'linear-gradient(90deg, #2196F3, #4CAF50)',
borderRadius: 3,
},
}}
/>
</Box>
);
};
export default SEOAnalysisLoading;

View File

@@ -0,0 +1,267 @@
import React, { useState } from 'react';
import {
Box,
Typography,
Button,
Grid,
Chip,
LinearProgress,
IconButton,
Tooltip,
Stack
} from '@mui/material';
import {
Refresh as RefreshIcon,
Language as LanguageIcon,
Help as HelpIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
// Shared styled components
import { GlassCard } from '../../shared/styled';
// Types
import { SEOAnalyzerPanelProps } from '../../shared/types';
// Utilities
import {
getStatusColor,
getStatusIcon,
categorizeAnalysisData
} from './seoUtils';
// Components
import CategoryCard from './CategoryCard';
import CriticalIssueCard from './CriticalIssueCard';
import AnalysisTabs from './AnalysisTabs';
import IssueDetailsDialog from './IssueDetailsDialog';
import AnalysisDetailsDialog from './AnalysisDetailsDialog';
import SEOAnalysisLoading from './SEOAnalysisLoading';
import SEOAnalysisError from './SEOAnalysisError';
const SEOAnalyzerPanel: React.FC<SEOAnalyzerPanelProps> = ({
analysisData,
onRunAnalysis,
loading,
error
}) => {
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
const [showError, setShowError] = useState(true);
const [selectedIssue, setSelectedIssue] = useState<any>(null);
const [showIssueDialog, setShowIssueDialog] = useState(false);
const [showDetailsDialog, setShowDetailsDialog] = useState(false);
// Debug logging
console.log('SEOAnalyzerPanel received data:', {
analysisData,
loading,
error,
hasUrl: analysisData?.url,
hasData: analysisData?.data,
criticalIssues: analysisData?.critical_issues?.length
});
const toggleCategory = (category: string) => {
const newExpanded = new Set(expandedCategories);
if (newExpanded.has(category)) {
newExpanded.delete(category);
} else {
newExpanded.add(category);
}
setExpandedCategories(newExpanded);
};
const handleIssueClick = (issue: any) => {
setSelectedIssue(issue);
setShowIssueDialog(true);
};
const handleAIAction = (action: string, issue: any) => {
// This would integrate with AI to generate specific fixes
console.log(`AI Action: ${action} for issue:`, issue);
// In a real implementation, this would call an AI service
};
const categorizedData = categorizeAnalysisData(analysisData);
return (
<>
<GlassCard sx={{ p: 3, mb: 3 }}>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" sx={{ color: 'white', fontWeight: 600 }}>
🔍 AI-Powered SEO Analysis
</Typography>
<Stack direction="row" spacing={2}>
{/* Index Entire Website Button - Region 1 */}
<Tooltip
title="Pro Feature: Index your entire website with AI-powered analysis. Get comprehensive insights across all pages, blog posts, and content. Coming soon!"
placement="top"
>
<span>
<Button
variant="outlined"
startIcon={<LanguageIcon />}
disabled
sx={{
borderColor: 'rgba(255, 255, 255, 0.3)',
color: 'rgba(255, 255, 255, 0.7)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
backgroundColor: 'rgba(255, 255, 255, 0.05)'
},
'&.Mui-disabled': {
borderColor: 'rgba(255, 255, 255, 0.2)',
color: 'rgba(255, 255, 255, 0.5)'
}
}}
>
Index Entire Website
</Button>
</span>
</Tooltip>
<Button
variant="contained"
startIcon={<RefreshIcon />}
onClick={onRunAnalysis}
disabled={loading}
sx={{
background: 'linear-gradient(45deg, #2196F3, #21CBF3)',
color: 'white',
'&:hover': {
background: 'linear-gradient(45deg, #1976D2, #1E88E5)',
},
}}
>
{loading ? 'Analyzing...' : 'Run Analysis'}
</Button>
</Stack>
</Box>
{/* Error Display */}
<SEOAnalysisError
error={error}
showError={showError}
onCloseError={() => setShowError(false)}
/>
{/* Loading State */}
<SEOAnalysisLoading loading={loading} />
{/* Analysis Results */}
<AnimatePresence>
{analysisData && analysisData.url && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<Grid container spacing={3}>
{/* Left Column - Overall Score & Critical Issues */}
<Grid item xs={12} md={4}>
{/* Overall Score - Region 2 */}
<Box sx={{ mb: 3, p: 2, background: 'rgba(255, 255, 255, 0.05)', borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
{getStatusIcon(analysisData.health_status)}
<Typography variant="h6" sx={{ color: 'white', ml: 1, fontWeight: 600 }}>
Overall Score: {analysisData.overall_score}/100
</Typography>
<Chip
label={analysisData.health_status.replace('_', ' ').toUpperCase()}
sx={{
ml: 2,
backgroundColor: getStatusColor(analysisData.health_status),
color: 'white',
fontWeight: 600,
}}
/>
</Box>
<LinearProgress
variant="determinate"
value={analysisData.overall_score}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: getStatusColor(analysisData.health_status),
borderRadius: 4,
},
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Analyzed: {analysisData.url}
</Typography>
<Tooltip title="View detailed information about all SEO tests performed">
<IconButton
size="small"
onClick={() => setShowDetailsDialog(true)}
sx={{
color: 'rgba(255, 255, 255, 0.7)',
'&:hover': { color: 'white' }
}}
>
<HelpIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Critical Issues Summary - Region 4 */}
{analysisData.critical_issues && analysisData.critical_issues.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ color: '#D32F2F', fontWeight: 600, mb: 2 }}>
🚨 Critical Issues ({analysisData.critical_issues.length})
</Typography>
{analysisData.critical_issues.slice(0, 2).map((issue, index) => (
<CriticalIssueCard
key={index}
issue={issue}
index={index}
onClick={handleIssueClick}
onAIAction={handleAIAction}
/>
))}
</Box>
)}
</Grid>
{/* Right Column - Detailed Analysis Tabs (Area A) */}
<Grid item xs={12} md={8}>
<AnalysisTabs
categorizedData={categorizedData}
expandedCategories={expandedCategories}
onToggleCategory={toggleCategory}
onIssueClick={handleIssueClick}
onAIAction={handleAIAction}
/>
</Grid>
</Grid>
</motion.div>
)}
</AnimatePresence>
</GlassCard>
{/* Dialogs */}
<IssueDetailsDialog
open={showIssueDialog}
issue={selectedIssue}
onClose={() => setShowIssueDialog(false)}
onAIAction={handleAIAction}
/>
<AnalysisDetailsDialog
open={showDetailsDialog}
onClose={() => setShowDetailsDialog(false)}
/>
</>
);
};
export default SEOAnalyzerPanel;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Box } from '@mui/material';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index, ...other }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`analysis-tabpanel-${index}`}
aria-labelledby={`analysis-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 2 }}>
{children}
</Box>
)}
</div>
);
};
export default TabPanel;

View File

@@ -0,0 +1,17 @@
// SEO Analysis Components
export { default as SEOAnalyzerPanel } from './SEOAnalyzerPanel';
export { default as CategoryCard } from './CategoryCard';
export { default as IssueList } from './IssueList';
export { default as CriticalIssueCard } from './CriticalIssueCard';
export { default as AnalysisTabs } from './AnalysisTabs';
export { default as TabPanel } from './TabPanel';
export { default as IssueDetailsDialog } from './IssueDetailsDialog';
export { default as AnalysisDetailsDialog } from './AnalysisDetailsDialog';
export { default as SEOAnalysisLoading } from './SEOAnalysisLoading';
export { default as SEOAnalysisError } from './SEOAnalysisError';
// Existing components
export { default as PlatformStatus } from './PlatformStatus';
export { default as AIInsightsPanel } from './AIInsightsPanel';
export { default as MetricCard } from './MetricCard';
export { default as HealthScore } from './HealthScore';

View File

@@ -0,0 +1,162 @@
import React from 'react';
import {
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Error as ErrorIcon,
Info as InfoIcon,
Speed as SpeedIcon,
Security as SecurityIcon,
Code as CodeIcon,
Accessibility as AccessibilityIcon,
MobileFriendly as MobileIcon,
Search as SearchIcon,
Article as ArticleIcon
} from '@mui/icons-material';
// SEO Analysis Utilities
export const getStatusColor = (status: string) => {
switch (status) {
case 'excellent':
return '#00C853';
case 'good':
return '#4CAF50';
case 'needs_improvement':
return '#FF9800';
case 'poor':
return '#D32F2F'; // Softer red instead of bright #F44336
default:
return '#9E9E9E';
}
};
export const getStatusIcon = (status: string) => {
switch (status) {
case 'excellent':
return <CheckCircleIcon sx={{ color: '#00C853' }} />;
case 'good':
return <CheckCircleIcon sx={{ color: '#4CAF50' }} />;
case 'needs_improvement':
return <WarningIcon sx={{ color: '#FF9800' }} />;
case 'poor':
return <ErrorIcon sx={{ color: '#D32F2F' }} />; // Softer red
default:
return <InfoIcon sx={{ color: '#9E9E9E' }} />;
}
};
export const getCategoryIcon = (category: string) => {
switch (category) {
case 'url_structure':
return <SearchIcon sx={{ color: '#2196F3' }} />;
case 'meta_data':
return <ArticleIcon sx={{ color: '#FF9800' }} />;
case 'content_analysis':
return <ArticleIcon sx={{ color: '#4CAF50' }} />;
case 'technical_seo':
return <CodeIcon sx={{ color: '#9C27B0' }} />;
case 'performance':
return <SpeedIcon sx={{ color: '#00BCD4' }} />;
case 'accessibility':
return <AccessibilityIcon sx={{ color: '#FF5722' }} />;
case 'user_experience':
return <MobileIcon sx={{ color: '#795548' }} />;
case 'security_headers':
return <SecurityIcon sx={{ color: '#E91E63' }} />;
default:
return <InfoIcon sx={{ color: '#607D8B' }} />;
}
};
export const getCategoryTitle = (category: string) => {
const titles: { [key: string]: string } = {
'url_structure': 'URL Structure & Security',
'meta_data': 'Meta Data & Technical SEO',
'content_analysis': 'Content Analysis',
'technical_seo': 'Technical SEO',
'performance': 'Performance',
'accessibility': 'Accessibility',
'user_experience': 'User Experience',
'security_headers': 'Security Headers',
'keyword_analysis': 'Keyword Analysis'
};
return titles[category] || category.replace('_', ' ').toUpperCase();
};
export const getAnalysisDetails = () => {
return [
{
title: "URL Structure & Security",
description: "Analyzes URL format, length, special characters, and security protocols like HTTPS.",
tests: ["URL length check", "Special character analysis", "HTTPS implementation", "URL readability"]
},
{
title: "Meta Data & Technical SEO",
description: "Examines title tags, meta descriptions, viewport settings, and character encoding.",
tests: ["Title tag optimization", "Meta description length", "Viewport meta tag", "Character encoding"]
},
{
title: "Content Analysis",
description: "Evaluates content quality, word count, heading structure, and readability.",
tests: ["Content length analysis", "Heading hierarchy", "Readability scoring", "Internal linking"]
},
{
title: "Technical SEO",
description: "Checks robots.txt, sitemaps, structured data, and canonical URLs.",
tests: ["Robots.txt accessibility", "XML sitemap presence", "Structured data markup", "Canonical URLs"]
},
{
title: "Performance",
description: "Measures page load speed, compression, caching, and optimization.",
tests: ["Page load time", "GZIP compression", "Caching headers", "Resource optimization"]
},
{
title: "Accessibility",
description: "Ensures alt text, form labels, heading structure, and color contrast.",
tests: ["Image alt text", "Form accessibility", "Heading hierarchy", "Color contrast"]
},
{
title: "User Experience",
description: "Checks mobile responsiveness, navigation, contact info, and social links.",
tests: ["Mobile optimization", "Navigation structure", "Contact information", "Social media links"]
},
{
title: "Security Headers",
description: "Analyzes security headers for protection against common vulnerabilities.",
tests: ["X-Frame-Options", "X-Content-Type-Options", "X-XSS-Protection", "Content-Security-Policy"]
}
];
};
export const categorizeAnalysisData = (analysisData: any) => {
if (!analysisData?.data) return { good: [], bad: [], ugly: [] };
const categories = Object.entries(analysisData.data);
const categorized = {
good: [] as any[],
bad: [] as any[],
ugly: [] as any[]
};
categories.forEach(([category, data]) => {
if (!data || typeof data !== 'object' || !(data as any).score) return;
const score = (data as any).score;
if (score >= 80) {
categorized.good.push({ category, data });
} else if (score >= 60) {
categorized.bad.push({ category, data });
} else {
categorized.ugly.push({ category, data });
}
});
return categorized;
};
export const formatMessage = (message: string) => {
if (message.includes(':')) {
const [title, details] = message.split(':');
return { title: title.trim(), details: details.trim() };
}
return { title: message, details: null };
};

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Box, Typography, Chip } from '@mui/material';
import { CategoryHeaderProps } from './types';
const CategoryHeader: React.FC<CategoryHeaderProps> = ({
categoryName,
category,
theme
}) => {
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
mb: 4,
p: 3,
background: 'rgba(255, 255, 255, 0.08)',
borderRadius: 3,
border: '1px solid rgba(255, 255, 255, 0.15)',
backdropFilter: 'blur(20px)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: category.gradient,
borderRadius: '3px 3px 0 0',
},
}}>
<Box
sx={{
width: 56,
height: 56,
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `${category.color}20`,
border: `2px solid ${category.color}40`,
boxShadow: `0 8px 24px ${category.color}30`,
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
background: category.gradient,
borderRadius: 3,
zIndex: -1,
opacity: 0.3,
},
}}
>
{category.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h3" sx={{
fontWeight: 800,
color: 'white',
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
fontSize: { xs: '1.75rem', md: '2.25rem' },
mb: 0.5,
}}>
{categoryName}
</Typography>
<Typography variant="body1" sx={{
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: 500,
}}>
{'subCategories' in category ?
`${Object.keys(category.subCategories).length} sub-categories` :
`${category.tools.length} tools`
}
</Typography>
</Box>
<Chip
label={'subCategories' in category ?
`${Object.values(category.subCategories).flatMap(subCat => subCat.tools).length} tools` :
`${category.tools.length} tools`
}
size="medium"
sx={{
background: 'rgba(255, 255, 255, 0.15)',
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 700,
fontSize: '0.9rem',
height: '32px',
border: '1px solid rgba(255, 255, 255, 0.2)',
}}
/>
</Box>
);
};
export default CategoryHeader;

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Box, Typography, Chip } from '@mui/material';
import { ShimmerHeader } from './styled';
import { DashboardHeaderProps } from './types';
const DashboardHeader: React.FC<DashboardHeaderProps> = ({
title,
subtitle,
statusChips = []
}) => {
return (
<ShimmerHeader sx={{ mb: 5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h2" component="h1" sx={{
fontWeight: 800,
color: 'white',
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
mb: 1,
fontSize: { xs: '2rem', md: '3rem' },
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{title}
</Typography>
<Typography variant="h5" sx={{
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 400,
fontSize: { xs: '1rem', md: '1.25rem' },
}}>
{subtitle}
</Typography>
</Box>
{statusChips.length > 0 && (
<Box sx={{ display: 'flex', gap: 1.5 }}>
{statusChips.map((chip, index) => (
<Chip
key={index}
icon={chip.icon}
label={chip.label}
sx={{
background: `${chip.color}20`,
border: `1px solid ${chip.color}40`,
color: chip.color,
fontWeight: 700,
}}
/>
))}
</Box>
)}
</Box>
</ShimmerHeader>
);
};
export default DashboardHeader;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Box, Typography, Button } from '@mui/material';
import { EmptyStateProps } from './types';
const EmptyState: React.FC<EmptyStateProps> = ({
title,
message,
onClearFilters,
clearButtonText = 'Clear Filters'
}) => {
return (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h5" sx={{ color: 'rgba(255, 255, 255, 0.9)', mb: 2, fontWeight: 600 }}>
{title}
</Typography>
<Typography variant="body1" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 3 }}>
{message}
</Typography>
{onClearFilters && (
<Button
variant="outlined"
onClick={onClearFilters}
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.3)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
background: 'rgba(255, 255, 255, 0.1)',
},
}}
>
{clearButtonText}
</Button>
)}
</Box>
);
};
export default EmptyState;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Box, Container, Alert, Button } from '@mui/material';
import { DashboardContainer } from './styled';
import { ErrorDisplayProps } from './types';
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
error,
onRetry,
retryButtonText = 'Retry'
}) => {
return (
<DashboardContainer>
<Container maxWidth="xl">
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
{error}
</Alert>
{onRetry && (
<Button onClick={onRetry} variant="contained">
{retryButtonText}
</Button>
)}
</Container>
</DashboardContainer>
);
};
export default ErrorDisplay;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Box, Container, Skeleton, Grid } from '@mui/material';
import { DashboardContainer } from './styled';
import { LoadingSkeletonProps } from './types';
const LoadingSkeleton: React.FC<LoadingSkeletonProps> = ({
itemCount = 8,
itemHeight = 200,
headerHeight = 80
}) => {
return (
<DashboardContainer>
<Container maxWidth="xl">
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Skeleton variant="rectangular" height={headerHeight} sx={{ borderRadius: 2 }} />
<Grid container spacing={3}>
{Array.from({ length: itemCount }).map((_, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Skeleton variant="rectangular" height={itemHeight} sx={{ borderRadius: 2 }} />
</Grid>
))}
</Grid>
</Box>
</Container>
</DashboardContainer>
);
};
export default LoadingSkeleton;

View File

@@ -0,0 +1,125 @@
import React from 'react';
import {
Box,
TextField,
InputAdornment,
IconButton,
Typography,
Tooltip
} from '@mui/material';
import {
Search as SearchIcon,
Clear as ClearIcon,
FilterList as FilterIcon
} from '@mui/icons-material';
import { SearchContainer, CategoryChip } from './styled';
import { SearchFilterProps } from './types';
const SearchFilter: React.FC<SearchFilterProps> = ({
searchQuery,
onSearchChange,
onClearSearch,
selectedCategory,
onCategoryChange,
selectedSubCategory,
onSubCategoryChange,
toolCategories,
theme
}) => {
return (
<SearchContainer>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
<TextField
fullWidth
placeholder="Search tools..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton onClick={onClearSearch} size="small">
<ClearIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</IconButton>
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
color: 'white',
'& fieldset': {
borderColor: 'rgba(255, 255, 255, 0.3)',
},
'&:hover fieldset': {
borderColor: 'rgba(255, 255, 255, 0.5)',
},
'&.Mui-focused fieldset': {
borderColor: 'rgba(255, 255, 255, 0.8)',
},
'& input::placeholder': {
color: 'rgba(255, 255, 255, 0.6)',
opacity: 1,
},
},
}}
/>
<Tooltip title="Filter by category">
<IconButton sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
<FilterIcon />
</IconButton>
</Tooltip>
</Box>
{/* Enhanced Category Filter */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<CategoryChip
label="All Tools"
onClick={() => onCategoryChange(null)}
active={selectedCategory === null}
theme={theme}
/>
{Object.keys(toolCategories).map((category) => (
<CategoryChip
key={category}
label={category}
onClick={() => onCategoryChange(category)}
active={selectedCategory === category}
theme={theme}
/>
))}
</Box>
{/* Sub-category Filter for SEO & Analytics */}
{selectedCategory === 'SEO & Analytics' && 'subCategories' in toolCategories['SEO & Analytics'] && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.8)', mb: 1, fontWeight: 600 }}>
Filter by sub-category:
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<CategoryChip
label="All SEO Tools"
onClick={() => onSubCategoryChange(null)}
active={selectedSubCategory === null}
theme={theme}
/>
{Object.keys(toolCategories['SEO & Analytics'].subCategories).map((subCategory) => (
<CategoryChip
key={subCategory}
label={subCategory}
onClick={() => onSubCategoryChange(subCategory)}
active={selectedSubCategory === subCategory}
theme={theme}
/>
))}
</Box>
</Box>
)}
</SearchContainer>
);
};
export default SearchFilter;

View File

@@ -0,0 +1,138 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Chip,
Box,
IconButton,
Tooltip
} from '@mui/material';
import {
Star as StarIcon,
StarBorder as StarBorderIcon
} from '@mui/icons-material';
import { ToolCardProps } from './types';
import { getStatusConfig } from './utils';
const ToolCard: React.FC<ToolCardProps> = ({
tool,
onToolClick,
isFavorite,
onToggleFavorite
}) => {
const config = getStatusConfig(tool.status);
return (
<Card
sx={{
background: 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
borderRadius: 3,
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
overflow: 'hidden',
'&:hover': {
transform: 'translateY(-8px) scale(1.02)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.2)',
},
}}
onClick={() => onToolClick(tool)}
>
<CardContent sx={{ p: 3 }}>
{/* Header with Icon and Status */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Box sx={{ mr: 2 }}>
{tool.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 0.5 }}>
{tool.name}
</Typography>
<Chip
label={config.label || tool.status}
size="small"
sx={{
background: `${config.color}20`,
color: config.color,
border: `1px solid ${config.color}40`,
fontWeight: 600,
fontSize: '0.75rem',
}}
/>
</Box>
<Tooltip title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}>
<IconButton
onClick={(e) => {
e.stopPropagation();
onToggleFavorite(tool.name);
}}
sx={{
color: isFavorite ? '#FFD700' : 'rgba(255, 255, 255, 0.7)',
'&:hover': {
color: isFavorite ? '#FFD700' : 'white',
},
}}
>
{isFavorite ? <StarIcon /> : <StarBorderIcon />}
</IconButton>
</Tooltip>
</Box>
{/* Description */}
<Typography
variant="body2"
sx={{
color: 'rgba(255, 255, 255, 0.8)',
mb: 2,
lineHeight: 1.6,
minHeight: '3.2em'
}}
>
{tool.description}
</Typography>
{/* Features */}
{tool.features && tool.features.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.6)', mb: 1, display: 'block' }}>
Features:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{tool.features.slice(0, 3).map((feature, index) => (
<Chip
key={index}
label={feature}
size="small"
sx={{
background: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.8)',
fontSize: '0.7rem',
height: '20px',
}}
/>
))}
{tool.features.length > 3 && (
<Chip
label={`+${tool.features.length - 3} more`}
size="small"
sx={{
background: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.6)',
fontSize: '0.7rem',
height: '20px',
}}
/>
)}
</Box>
</Box>
)}
</CardContent>
</Card>
);
};
export default ToolCard;

View File

@@ -0,0 +1,17 @@
// Shared components exports
export { default as DashboardHeader } from './DashboardHeader';
export { default as SearchFilter } from './SearchFilter';
export { default as ToolCard } from './ToolCard';
export { default as CategoryHeader } from './CategoryHeader';
export { default as LoadingSkeleton } from './LoadingSkeleton';
export { default as ErrorDisplay } from './ErrorDisplay';
export { default as EmptyState } from './EmptyState';
// Shared styled components
export * from './styled';
// Shared types
export * from './types';
// Shared utilities
export * from './utils';

View File

@@ -0,0 +1,138 @@
import { Box, Card, Chip } from '@mui/material';
import { styled } from '@mui/material/styles';
// Shared styled components for dashboard components
export const DashboardContainer = styled(Box)(({ theme }) => ({
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
padding: theme.spacing(4),
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'url("data:image/svg+xml,%3Csvg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.03"%3E%3Ccircle cx="40" cy="40" r="3"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")',
pointerEvents: 'none',
},
'&::after': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
width: '600px',
height: '600px',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: 0,
},
}));
export const GlassCard = styled(Card)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
borderRadius: theme.spacing(3),
boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent)',
transition: 'left 0.6s ease-in-out',
},
'&:hover': {
transform: 'translateY(-12px) scale(1.02)',
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.18)',
border: '1px solid rgba(255, 255, 255, 0.2)',
'&::before': {
left: '100%',
},
},
}));
export const ShimmerHeader = styled(Box)(({ theme }) => ({
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '3px',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent)',
animation: 'shimmer 3s infinite',
},
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
}));
export const SearchContainer = styled(Box)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: theme.spacing(3),
padding: theme.spacing(2),
marginBottom: theme.spacing(4),
transition: 'all 0.3s ease',
'&:hover': {
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
},
}));
export const CategoryChip = styled(Chip, {
shouldForwardProp: (prop) => prop !== 'active',
})<{ active?: boolean }>(({ theme, active }) => ({
background: active ? 'rgba(255, 255, 255, 0.25)' : 'rgba(255, 255, 255, 0.1)',
color: 'white',
fontWeight: 600,
fontSize: '0.9rem',
padding: theme.spacing(1, 2),
border: `1px solid ${active ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 0.2)'}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
background: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
'& .MuiChip-label': {
padding: theme.spacing(0.5, 1),
},
}));
export const EnhancedGlassCard = styled(GlassCard)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.12)',
border: '2px solid rgba(255, 255, 255, 0.2)',
'&:hover': {
border: '2px solid rgba(255, 255, 255, 0.3)',
},
}));
export const AIInsightsPanel = styled(GlassCard)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.1)',
border: '1px solid rgba(255, 255, 255, 0.15)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #667eea, #764ba2, #f093fb)',
borderRadius: '3px 3px 0 0',
},
}));

View File

@@ -0,0 +1,225 @@
// Shared TypeScript interfaces for dashboard components
export interface Tool {
name: string;
description: string;
icon: React.ReactElement;
status: string;
path: string;
features: string[];
isPinned?: boolean;
isHighlighted?: boolean;
}
export interface SubCategory {
tools: Tool[];
}
export interface RegularCategory {
icon: React.ReactElement;
color: string;
gradient: string;
tools: Tool[];
}
export interface SubCategoryCategory {
icon: React.ReactElement;
color: string;
gradient: string;
subCategories: Record<string, SubCategory>;
}
export type Category = RegularCategory | SubCategoryCategory;
export interface ToolCategories {
[key: string]: Category;
}
export interface SnackbarState {
open: boolean;
message: string;
severity: 'success' | 'error' | 'info' | 'warning';
}
export interface DashboardState {
loading: boolean;
error: string | null;
searchQuery: string;
selectedCategory: string | null;
selectedSubCategory: string | null;
favorites: string[];
snackbar: SnackbarState;
}
export interface ToolCardProps {
tool: Tool;
onToolClick: (tool: Tool) => void;
isFavorite: boolean;
onToggleFavorite: (toolName: string) => void;
}
export interface CategoryHeaderProps {
categoryName: string;
category: Category;
theme: any;
}
export interface SearchFilterProps {
searchQuery: string;
onSearchChange: (query: string) => void;
onClearSearch: () => void;
selectedCategory: string | null;
onCategoryChange: (category: string | null) => void;
selectedSubCategory: string | null;
onSubCategoryChange: (subCategory: string | null) => void;
toolCategories: ToolCategories;
theme: any;
}
export interface DashboardHeaderProps {
title: string;
subtitle: string;
statusChips?: Array<{
label: string;
color: string;
icon: React.ReactElement;
}>;
}
export interface LoadingSkeletonProps {
itemCount?: number;
itemHeight?: number;
headerHeight?: number;
}
export interface ErrorDisplayProps {
error: string;
onRetry?: () => void;
retryButtonText?: string;
}
export interface EmptyStateProps {
icon: React.ReactElement;
title: string;
message: string;
onClearFilters?: () => void;
clearButtonText?: string;
}
// SEO Analysis Types
export interface SEOIssue {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
current_value?: string;
}
export interface SEOWarning {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
current_value?: string;
}
export interface SEORecommendation {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
priority?: string;
description?: string;
}
export interface SEOAnalysisData {
url: string;
overall_score: number;
health_status: string;
critical_issues: SEOIssue[];
warnings: SEOWarning[];
recommendations: SEORecommendation[];
data: {
url_structure: any;
meta_data: any;
content_analysis: any;
technical_seo: any;
performance: any;
accessibility: any;
user_experience: any;
security_headers: any;
keyword_analysis?: any;
};
timestamp: string;
success?: boolean;
message?: string;
}
export interface SEOAnalyzerPanelProps {
analysisData: SEOAnalysisData | null;
onRunAnalysis: () => Promise<void>;
loading: boolean;
error: string | null;
}
export interface CategoryCardProps {
category: string;
data: any;
isExpanded: boolean;
onToggle: (category: string) => void;
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface IssueListProps {
issues: any[];
type: 'critical' | 'warning' | 'recommendation';
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface CriticalIssueCardProps {
issue: any;
index: number;
onClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface AnalysisTabsProps {
categorizedData: {
good: any[];
bad: any[];
ugly: any[];
};
expandedCategories: Set<string>;
onToggleCategory: (category: string) => void;
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface IssueDetailsDialogProps {
open: boolean;
issue: any | null;
onClose: () => void;
onAIAction: (action: string, issue: any) => void;
}
export interface AnalysisDetailsDialogProps {
open: boolean;
onClose: () => void;
}
export interface SEOAnalysisLoadingProps {
loading: boolean;
}
export interface SEOAnalysisErrorProps {
error: string | null;
showError: boolean;
onCloseError: () => void;
}

View File

@@ -0,0 +1,149 @@
import { Category, Tool, ToolCategories } from './types';
// Utility functions for dashboard components
export const getToolsForCategory = (category: Category, selectedSubCategory: string | null): Tool[] => {
if ('subCategories' in category) {
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
return category.subCategories[selectedSubCategory].tools;
}
return [];
}
return category.tools;
};
export const getFilteredCategories = (
toolCategories: ToolCategories,
selectedCategory: string | null,
searchQuery: string
) => {
const filtered: ToolCategories = {};
Object.entries(toolCategories).forEach(([categoryName, category]) => {
if (selectedCategory && categoryName !== selectedCategory) {
return;
}
if ('subCategories' in category) {
const filteredSubCategories: Record<string, any> = {};
Object.entries(category.subCategories).forEach(([subCategoryName, subCategory]) => {
const filteredTools = subCategory.tools.filter(tool =>
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filteredTools.length > 0) {
filteredSubCategories[subCategoryName] = { ...subCategory, tools: filteredTools };
}
});
if (Object.keys(filteredSubCategories).length > 0) {
filtered[categoryName] = { ...category, subCategories: filteredSubCategories };
}
} else {
const filteredTools = category.tools.filter(tool =>
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filteredTools.length > 0) {
filtered[categoryName] = { ...category, tools: filteredTools };
}
}
});
return filtered;
};
export const getStatusConfig = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return { color: '#4CAF50', icon: '✓', label: 'Excellent' };
case 'good':
return { color: '#FF9800', icon: '⚠', label: 'Good' };
case 'needs_action':
return { color: '#F44336', icon: '✗', label: 'Needs Action' };
default:
return { color: '#9E9E9E', icon: '', label: 'Unknown' };
}
};
export const getStatusColor = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return '#4CAF50';
case 'good':
return '#FF9800';
case 'needs_action':
return '#F44336';
default:
return '#9E9E9E';
}
};
export const getStatusIcon = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return '✓';
case 'good':
return '⚠';
case 'needs_action':
return '✗';
default:
return '';
}
};
export const formatNumber = (num: number): string => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
export const formatPercentage = (num: number): string => {
return `${num > 0 ? '+' : ''}${num.toFixed(1)}%`;
};
export const getTrendColor = (trend: string): string => {
switch (trend) {
case 'up':
return '#4CAF50';
case 'down':
return '#F44336';
default:
return '#9E9E9E';
}
};
export const getTrendIcon = (trend: string): string => {
switch (trend) {
case 'up':
return '↗';
case 'down':
return '↘';
default:
return '→';
}
};
export const capitalizeFirst = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const truncateText = (text: string, maxLength: number): string => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};

View File

@@ -0,0 +1,377 @@
import React from 'react';
import {
Article as ArticleIcon,
Search as SearchIcon,
TrendingUp as TrendingUpIcon,
Campaign as CampaignIcon,
Analytics as AnalyticsIcon,
Psychology as PsychologyIcon,
AutoAwesome as AutoAwesomeIcon,
Speed as SpeedIcon,
Business as BusinessIcon,
SocialDistance as SocialIcon,
Create as CreateIcon
} from '@mui/icons-material';
import { ToolCategories } from '../components/shared/types';
export const toolCategories: ToolCategories = {
'AI Content Writers': {
icon: React.createElement(ArticleIcon),
color: '#4CAF50',
gradient: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
tools: [
{
name: 'AI Blog Writer',
description: 'Generate engaging blog posts with AI',
icon: React.createElement(ArticleIcon),
status: 'active',
path: '/ai-blog-writer',
features: ['SEO Optimized', 'Multiple Formats', 'Custom Tone']
},
{
name: 'AI Essay Writer',
description: 'Academic and professional essay writing',
icon: React.createElement(CreateIcon),
status: 'active',
path: '/ai-essay-writer',
features: ['Academic Style', 'Citation Support', 'Plagiarism Free']
},
{
name: 'AI News Article Writer',
description: 'Professional news and article writing',
icon: React.createElement(ArticleIcon),
status: 'active',
path: '/ai-news-writer',
features: ['Fact-Checked', 'Journalistic Style', 'Breaking News']
},
{
name: 'AI Story Writer',
description: 'Creative storytelling and fiction writing',
icon: React.createElement(CreateIcon),
status: 'active',
path: '/ai-story-writer',
features: ['Creative Writing', 'Character Development', 'Plot Generation']
},
{
name: 'AI Copywriter',
description: 'Marketing copy and advertising content',
icon: React.createElement(CampaignIcon),
status: 'active',
path: '/ai-copywriter',
features: ['Persuasive Writing', 'Brand Voice', 'Call-to-Action']
},
{
name: 'AI Product Description Writer',
description: 'Compelling product descriptions',
icon: React.createElement(BusinessIcon),
status: 'active',
path: '/ai-product-writer',
features: ['E-commerce Optimized', 'Feature Highlighting', 'Conversion Focused']
}
]
},
'SEO & Analytics': {
icon: React.createElement(SearchIcon),
color: '#2196F3',
gradient: 'linear-gradient(135deg, #2196F3 0%, #1976D2 100%)',
subCategories: {
'Enterprise & Advanced': {
tools: [
{
name: 'SEO Dashboard',
description: 'AI-powered SEO analysis and actionable insights',
icon: React.createElement(AnalyticsIcon),
status: 'premium',
path: '/seo-dashboard',
features: ['AI Insights', 'Performance Tracking', 'Actionable Recommendations'],
isPinned: true,
isHighlighted: true
},
{
name: 'Content Planning Dashboard',
description: 'AI-powered content strategy and planning with gap analysis',
icon: React.createElement(PsychologyIcon),
status: 'premium',
path: '/content-planning',
features: ['Content Strategy', 'Gap Analysis', 'AI Recommendations', 'Calendar Management'],
isPinned: true,
isHighlighted: true
},
{
name: 'Enterprise SEO Suite',
description: 'Unified workflow orchestration for comprehensive SEO management',
icon: React.createElement(BusinessIcon),
status: 'premium',
path: '/enterprise-seo-suite',
features: ['Complete Audits', 'AI Recommendations', 'Strategic Planning']
},
{
name: 'Google Search Console Intelligence',
description: 'Advanced GSC data analysis with AI-powered insights',
icon: React.createElement(AnalyticsIcon),
status: 'premium',
path: '/gsc-intelligence',
features: ['Content Opportunities', 'Search Intelligence', 'Competitive Analysis']
},
{
name: 'AI Content Strategy Generator',
description: 'Comprehensive content planning with market intelligence',
icon: React.createElement(PsychologyIcon),
status: 'premium',
path: '/ai-content-strategy',
features: ['Market Intelligence', 'Topic Clusters', 'Implementation Roadmaps']
}
]
},
'Technical SEO Tools': {
tools: [
{
name: 'On-Page SEO Analyzer',
description: 'Comprehensive page-level SEO optimization analysis',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/on-page-seo-analyzer',
features: ['Technical SEO', 'Content Analysis', 'Optimization Suggestions']
},
{
name: 'Technical SEO Crawler',
description: 'Site-wide technical analysis and performance metrics',
icon: React.createElement(SpeedIcon),
status: 'active',
path: '/technical-seo-crawler',
features: ['Crawl Analysis', 'Performance Metrics', 'AI Recommendations']
},
{
name: 'Google PageSpeed Insights',
description: 'Website performance and Core Web Vitals analysis',
icon: React.createElement(SpeedIcon),
status: 'active',
path: '/pagespeed-insights',
features: ['Core Web Vitals', 'Speed Optimization', 'Mobile Performance']
},
{
name: 'URL SEO Checker',
description: 'Individual URL analysis and optimization recommendations',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/url-seo-checker',
features: ['Technical Factors', 'Optimization Tips', 'Detailed Reports']
},
{
name: 'Sitemap Analysis',
description: 'XML and HTML sitemap analysis and optimization',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/sitemap-analysis',
features: ['Sitemap Validation', 'Structure Analysis', 'Optimization Tips']
}
]
},
'AI & Analysis Tools': {
tools: [
{
name: 'Content Gap Analysis',
description: 'Advanced competitive content analysis and opportunities',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/content-gap-analysis',
features: ['Competitive Analysis', 'AI Insights', 'Opportunity Identification']
},
{
name: 'CGPT SEO Analyzer',
description: 'AI-powered SEO analysis using advanced language models',
icon: React.createElement(PsychologyIcon),
status: 'active',
path: '/cgpt-seo-analyzer',
features: ['AI Analysis', 'Advanced Insights', 'Strategic Recommendations']
},
{
name: 'Webpage Content Analysis',
description: 'Deep content analysis and optimization insights',
icon: React.createElement(ArticleIcon),
status: 'active',
path: '/webpage-content-analysis',
features: ['Content Quality', 'Readability Analysis', 'Optimization Tips']
},
{
name: 'WordCloud Generator',
description: 'Visual keyword and content analysis with word clouds',
icon: React.createElement(AnalyticsIcon),
status: 'active',
path: '/wordcloud-generator',
features: ['Visual Analysis', 'Keyword Mapping', 'Content Insights']
},
{
name: 'TextStat Analysis',
description: 'Advanced text statistics and readability analysis',
icon: React.createElement(AnalyticsIcon),
status: 'active',
path: '/textstat-analysis',
features: ['Readability Metrics', 'Text Statistics', 'Content Optimization']
}
]
},
'SEO Optimization Tools': {
tools: [
{
name: 'SEO Analysis',
description: 'Comprehensive SEO analysis and reporting',
icon: React.createElement(AnalyticsIcon),
status: 'active',
path: '/seo-analysis',
features: ['Complete Analysis', 'Detailed Reports', 'Actionable Insights']
},
{
name: 'OpenGraph Generator',
description: 'Social media optimization for Facebook and LinkedIn',
icon: React.createElement(SocialIcon),
status: 'active',
path: '/opengraph-generator',
features: ['Social Optimization', 'Visual Appeal', 'Click Enhancement']
},
{
name: 'Schema Markup Generator',
description: 'Structured data creation for rich snippets',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/schema-generator',
features: ['Rich Snippets', 'Search Enhancement', 'Content Understanding']
}
]
}
}
},
'Social Media': {
icon: React.createElement(SocialIcon),
color: '#FF9800',
gradient: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
tools: [
{
name: 'Facebook Content Writer',
description: 'Engaging Facebook posts and ads',
icon: React.createElement(SocialIcon),
status: 'active',
path: '/facebook-writer',
features: ['Engagement Focused', 'Ad Copy', 'Post Scheduling']
},
{
name: 'LinkedIn Content Writer',
description: 'Professional LinkedIn content',
icon: React.createElement(BusinessIcon),
status: 'active',
path: '/linkedin-writer',
features: ['Professional Tone', 'Thought Leadership', 'B2B Focus']
},
{
name: 'Twitter Content Writer',
description: 'Viral Twitter threads and tweets',
icon: React.createElement(SocialIcon),
status: 'active',
path: '/twitter-writer',
features: ['Viral Potential', 'Thread Creation', 'Hashtag Optimization']
},
{
name: 'Instagram Content Writer',
description: 'Visual and engaging Instagram content',
icon: React.createElement(SocialIcon),
status: 'active',
path: '/instagram-writer',
features: ['Visual Descriptions', 'Hashtag Strategy', 'Story Content']
},
{
name: 'YouTube Content Writer',
description: 'Video scripts and descriptions',
icon: React.createElement(SocialIcon),
status: 'active',
path: '/youtube-writer',
features: ['Video Scripts', 'SEO Descriptions', 'Engagement Hooks']
}
]
},
'Business & Marketing': {
icon: React.createElement(BusinessIcon),
color: '#9C27B0',
gradient: 'linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%)',
tools: [
{
name: 'Financial Report Generator',
description: 'Professional financial analysis and reports',
icon: React.createElement(AnalyticsIcon),
status: 'active',
path: '/financial-reports',
features: ['Data Analysis', 'Professional Reports', 'Insights Generation']
},
{
name: 'Email Templates',
description: 'Professional email templates and campaigns',
icon: React.createElement(CampaignIcon),
status: 'active',
path: '/email-templates',
features: ['Professional Templates', 'A/B Testing', 'Automation']
},
{
name: 'Press Release Writer',
description: 'Newsworthy press releases',
icon: React.createElement(ArticleIcon),
status: 'active',
path: '/press-releases',
features: ['Newsworthy Content', 'Media Ready', 'Distribution Ready']
},
{
name: 'Landing Page Copy',
description: 'High-converting landing page content',
icon: React.createElement(BusinessIcon),
status: 'active',
path: '/landing-page-copy',
features: ['Conversion Focused', 'A/B Testing', 'UX Optimized']
},
{
name: 'Competitive Intelligence',
description: 'Analyze competitors and market trends',
icon: React.createElement(PsychologyIcon),
status: 'premium',
path: '/competitive-intelligence',
features: ['Market Analysis', 'Competitor Tracking', 'Strategy Insights']
}
]
},
'Creative & Advanced': {
icon: React.createElement(AutoAwesomeIcon),
color: '#E91E63',
gradient: 'linear-gradient(135deg, #E91E63 0%, #C2185B 100%)',
tools: [
{
name: 'AI Agents Crew',
description: 'Multi-agent AI content creation team',
icon: React.createElement(AutoAwesomeIcon),
status: 'premium',
path: '/ai-agents-crew',
features: ['Multi-Agent System', 'Collaborative Writing', 'Advanced AI']
},
{
name: 'Content Performance Predictor',
description: 'Predict content performance and engagement',
icon: React.createElement(AnalyticsIcon),
status: 'premium',
path: '/content-predictor',
features: ['Performance Prediction', 'Engagement Analysis', 'ROI Forecasting']
},
{
name: 'Web Researcher',
description: 'AI-powered web research and analysis',
icon: React.createElement(SearchIcon),
status: 'active',
path: '/web-researcher',
features: ['Real-time Research', 'Data Analysis', 'Insight Generation']
},
{
name: 'Content Scheduler',
description: 'Intelligent content scheduling and planning',
icon: React.createElement(CampaignIcon),
status: 'active',
path: '/content-scheduler',
features: ['Smart Scheduling', 'Calendar Integration', 'Performance Tracking']
}
]
}
};

View File

@@ -0,0 +1,120 @@
import { useState, useEffect } from 'react';
import { DashboardState, SnackbarState } from '../components/shared/types';
const useDashboardState = () => {
const [state, setState] = useState<DashboardState>({
loading: true,
error: null,
searchQuery: '',
selectedCategory: null,
selectedSubCategory: null,
favorites: [],
snackbar: {
open: false,
message: '',
severity: 'info',
},
});
// Load favorites from localStorage
useEffect(() => {
const savedFavorites = localStorage.getItem('alwrity-favorites');
if (savedFavorites) {
setState(prev => ({
...prev,
favorites: JSON.parse(savedFavorites),
loading: false,
}));
} else {
setState(prev => ({ ...prev, loading: false }));
}
}, []);
// Save favorites to localStorage
const toggleFavorite = (toolName: string) => {
const newFavorites = state.favorites.includes(toolName)
? state.favorites.filter(f => f !== toolName)
: [...state.favorites, toolName];
setState(prev => ({
...prev,
favorites: newFavorites,
}));
localStorage.setItem('alwrity-favorites', JSON.stringify(newFavorites));
showSnackbar(
state.favorites.includes(toolName) ? 'Removed from favorites' : 'Added to favorites',
'success'
);
};
const setSearchQuery = (query: string) => {
setState(prev => ({ ...prev, searchQuery: query }));
};
const setSelectedCategory = (category: string | null) => {
setState(prev => ({
...prev,
selectedCategory: category,
selectedSubCategory: null, // Reset sub-category when changing main category
}));
};
const setSelectedSubCategory = (subCategory: string | null) => {
setState(prev => ({ ...prev, selectedSubCategory: subCategory }));
};
const setError = (error: string | null) => {
setState(prev => ({ ...prev, error }));
};
const setLoading = (loading: boolean) => {
setState(prev => ({ ...prev, loading }));
};
const showSnackbar = (message: string, severity: SnackbarState['severity'] = 'info') => {
setState(prev => ({
...prev,
snackbar: {
open: true,
message,
severity,
},
}));
};
const hideSnackbar = () => {
setState(prev => ({
...prev,
snackbar: {
...prev.snackbar,
open: false,
},
}));
};
const clearFilters = () => {
setState(prev => ({
...prev,
searchQuery: '',
selectedCategory: null,
selectedSubCategory: null,
}));
};
return {
state,
toggleFavorite,
setSearchQuery,
setSelectedCategory,
setSelectedSubCategory,
setError,
setLoading,
showSnackbar,
hideSnackbar,
clearFilters,
};
};
export default useDashboardState;

View File

@@ -0,0 +1,54 @@
import { useEffect, useCallback, useRef } from 'react';
export const usePerformanceOptimization = () => {
const animationFrameRef = useRef<number>();
const timeoutRef = useRef<NodeJS.Timeout>();
// Debounce function for expensive operations
const debounce = useCallback((func: Function, delay: number) => {
return (...args: any[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => func(...args), delay);
};
}, []);
// Throttle function for scroll/resize events
const throttle = useCallback((func: Function, delay: number) => {
let lastCall = 0;
return (...args: any[]) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func(...args);
}
};
}, []);
// Optimize animations with requestAnimationFrame
const smoothAnimation = useCallback((callback: () => void) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(callback);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, []);
return {
debounce,
throttle,
smoothAnimation,
};
};

96
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,96 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import App from './App';
import './styles/global.css';
// Create a custom theme for better professional appearance
const theme = createTheme({
palette: {
primary: {
main: '#6366f1', // Indigo-500
light: '#818cf8', // Indigo-400
dark: '#4f46e5', // Indigo-600
},
secondary: {
main: '#8b5cf6', // Violet-500
light: '#a78bfa', // Violet-400
dark: '#7c3aed', // Violet-600
},
background: {
default: '#f8fafc', // Slate-50
paper: '#ffffff',
},
text: {
primary: '#1e293b', // Slate-800
secondary: '#64748b', // Slate-500
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h4: {
fontWeight: 700,
letterSpacing: '-0.025em',
},
h5: {
fontWeight: 600,
letterSpacing: '-0.025em',
},
h6: {
fontWeight: 600,
letterSpacing: '-0.025em',
},
body1: {
lineHeight: 1.6,
},
body2: {
lineHeight: 1.6,
},
},
shape: {
borderRadius: 12,
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
fontWeight: 600,
borderRadius: 8,
padding: '10px 24px',
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: 8,
},
},
},
},
},
});
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,690 @@
import { apiClient, aiApiClient } from '../api/client';
// Types
export interface ContentStrategyCreate {
name: string;
description: string;
industry: string;
target_audience: string;
content_pillars: string[];
user_id?: number;
}
export interface ContentStrategyUpdate {
name?: string;
description?: string;
industry?: string;
target_audience?: string;
content_pillars?: string[];
}
export interface CalendarEventCreate {
title: string;
description: string;
date: string;
platform: string;
content_type: string;
status: 'draft' | 'scheduled' | 'published';
strategy_id?: string;
user_id?: number;
}
export interface CalendarEventUpdate {
title?: string;
description?: string;
date?: string;
platform?: string;
content_type?: string;
status?: 'draft' | 'scheduled' | 'published';
strategy_id?: string;
}
export interface GapAnalysisCreate {
website_url: string;
competitors: string[];
keywords: string[];
user_id?: number;
}
export interface GapAnalysisUpdate {
website_url?: string;
competitors?: string[];
keywords?: string[];
gaps?: string[];
recommendations?: any[];
}
export interface AIAnalyticsCreate {
analysis_type: string;
data: any;
insights: any[];
user_id?: number;
}
export interface AIAnalyticsUpdate {
analysis_type?: string;
data?: any;
insights?: any[];
}
// New Calendar Generation Interfaces
export interface CalendarGenerationRequest {
user_id: number;
strategy_id?: number;
calendar_type: string;
industry?: string;
business_size: string;
force_refresh?: boolean;
}
export interface CalendarGenerationResponse {
user_id: number;
strategy_id?: number;
calendar_type: string;
industry: string;
business_size: string;
generated_at: string;
content_pillars: string[];
platform_strategies: any;
content_mix: Record<string, number>;
daily_schedule: any[];
weekly_themes: any[];
content_recommendations: any[];
optimal_timing: any;
performance_predictions: any;
trending_topics: any[];
repurposing_opportunities: any[];
ai_insights: any[];
competitor_analysis: any;
gap_analysis_insights: any;
strategy_insights: any;
onboarding_insights: any;
processing_time: number;
ai_confidence: number;
}
export interface ContentOptimizationRequest {
user_id: number;
event_id?: number;
title: string;
description: string;
content_type: string;
target_platform: string;
original_content?: any;
}
export interface ContentOptimizationResponse {
user_id: number;
event_id?: number;
original_content: any;
optimized_content: any;
platform_adaptations: string[];
visual_recommendations: string[];
hashtag_suggestions: string[];
keyword_optimization: any;
tone_adjustments: any;
length_optimization: any;
performance_prediction: any;
optimization_score: number;
created_at: string;
}
export interface PerformancePredictionRequest {
user_id: number;
strategy_id?: number;
content_type: string;
platform: string;
content_data: any;
}
export interface PerformancePredictionResponse {
user_id: number;
strategy_id?: number;
content_type: string;
platform: string;
predicted_engagement_rate: number;
predicted_reach: number;
predicted_conversions: number;
predicted_roi: number;
confidence_score: number;
recommendations: string[];
created_at: string;
}
export interface ContentRepurposingRequest {
user_id: number;
strategy_id?: number;
original_content: any;
target_platforms: string[];
}
export interface ContentRepurposingResponse {
user_id: number;
strategy_id?: number;
original_content: any;
platform_adaptations: any[];
transformations: any[];
implementation_tips: string[];
gap_addresses: string[];
created_at: string;
}
export interface TrendingTopicsRequest {
user_id: number;
industry: string;
limit?: number;
}
export interface TrendingTopicsResponse {
user_id: number;
industry: string;
trending_topics: any[];
gap_relevance_scores: Record<string, number>;
audience_alignment_scores: Record<string, number>;
created_at: string;
}
// Content Planning API Service
class ContentPlanningAPI {
private baseURL = '/api/content-planning';
// Content Strategy APIs
async createStrategy(strategy: ContentStrategyCreate) {
const response = await apiClient.post(`${this.baseURL}/strategies/`, strategy);
return response.data;
}
async getStrategies(userId?: number) {
const params = userId ? { user_id: userId } : {};
const response = await apiClient.get(`${this.baseURL}/strategies/`, { params });
return response.data;
}
async getStrategy(id: string) {
const response = await apiClient.get(`${this.baseURL}/strategies/${id}`);
return response.data;
}
async updateStrategy(id: string, updates: ContentStrategyUpdate) {
const response = await apiClient.put(`${this.baseURL}/strategies/${id}`, updates);
return response.data;
}
async deleteStrategy(id: string) {
const response = await apiClient.delete(`${this.baseURL}/strategies/${id}`);
return response.data;
}
// Calendar Event APIs
async createEvent(event: CalendarEventCreate) {
const response = await apiClient.post(`${this.baseURL}/calendar-events/`, event);
return response.data;
}
async getEvents(userId?: number, filters?: any) {
const params = { ...filters };
if (userId) params.user_id = userId;
const response = await apiClient.get(`${this.baseURL}/calendar-events/`, { params });
return response.data;
}
async getEvent(id: string) {
const response = await apiClient.get(`${this.baseURL}/calendar-events/${id}`);
return response.data;
}
async updateEvent(id: string, updates: CalendarEventUpdate) {
const response = await apiClient.put(`${this.baseURL}/calendar-events/${id}`, updates);
return response.data;
}
async deleteEvent(id: string) {
const response = await apiClient.delete(`${this.baseURL}/calendar-events/${id}`);
return response.data;
}
// Gap Analysis APIs
async createGapAnalysis(analysis: GapAnalysisCreate) {
const response = await apiClient.post(`${this.baseURL}/gap-analysis/`, analysis);
return response.data;
}
async getGapAnalyses(userId?: number) {
const params = userId ? { user_id: userId } : {};
const response = await apiClient.get(`${this.baseURL}/gap-analysis/`, { params });
return response.data;
}
async getGapAnalysis(id: string) {
const response = await apiClient.get(`${this.baseURL}/gap-analysis/${id}`);
return response.data;
}
async updateGapAnalysis(id: string, updates: GapAnalysisUpdate) {
const response = await apiClient.put(`${this.baseURL}/gap-analysis/${id}`, updates);
return response.data;
}
async deleteGapAnalysis(id: string) {
const response = await apiClient.delete(`${this.baseURL}/gap-analysis/${id}`);
return response.data;
}
// AI-Powered Gap Analysis - Using AI client for longer timeout
async analyzeContentGaps(params: {
website_url: string;
competitors: string[];
keywords: string[];
user_id?: number;
}) {
const response = await aiApiClient.post(`${this.baseURL}/gap-analysis/analyze`, params);
return response.data;
}
// AI Analytics APIs - Using AI client for longer timeout
async createAIAnalytics(analytics: AIAnalyticsCreate) {
const response = await aiApiClient.post(`${this.baseURL}/ai-analytics/`, analytics);
return response.data;
}
async getAIAnalytics(userId?: number) {
const params = userId ? { user_id: userId } : {};
const response = await aiApiClient.get(`${this.baseURL}/ai-analytics/`, { params });
return response.data;
}
async getAIAnalyticsById(id: string) {
const response = await aiApiClient.get(`${this.baseURL}/ai-analytics/${id}`);
return response.data;
}
async updateAIAnalytics(id: string, updates: AIAnalyticsUpdate) {
const response = await aiApiClient.put(`${this.baseURL}/ai-analytics/${id}`, updates);
return response.data;
}
async deleteAIAnalytics(id: string) {
const response = await aiApiClient.delete(`${this.baseURL}/ai-analytics/${id}`);
return response.data;
}
// AI Analytics with Server-Sent Events
async streamAIAnalytics(
onProgress: (data: any) => void,
onComplete: (data: any) => void,
onError: (error: any) => void,
userId?: number
) {
try {
const params: Record<string, string> = {};
if (userId) {
params.user_id = userId.toString();
}
const queryString = new URLSearchParams(params).toString();
const url = `${this.baseURL}/ai-analytics/stream?${queryString}`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'connected':
onProgress({ message: data.message, progress: 0 });
break;
case 'progress':
onProgress({
message: data.message,
progress: data.progress,
step: data.step
});
break;
case 'complete':
onComplete(data);
eventSource.close();
break;
case 'error':
onError(new Error(data.message));
eventSource.close();
break;
}
} catch (parseError) {
onError(new Error('Failed to parse server message'));
}
};
eventSource.onerror = (error) => {
onError(new Error('EventSource failed'));
eventSource.close();
};
// Return cleanup function
return () => {
eventSource.close();
};
} catch (error: any) {
onError(error);
}
}
// Health Check APIs
async checkHealth() {
const response = await apiClient.get(`${this.baseURL}/health`);
return response.data;
}
async checkBackendHealth() {
const response = await apiClient.get(`${this.baseURL}/health/backend`);
return response.data;
}
async checkAIHealth() {
const response = await apiClient.get(`${this.baseURL}/health/ai`);
return response.data;
}
async checkDatabaseHealth() {
const response = await apiClient.get(`${this.baseURL}/database/health`);
return response.data;
}
// Error handling wrapper with AI-specific error messages
private async handleRequest<T>(request: () => Promise<T>, isAI: boolean = false): Promise<T> {
try {
return await request();
} catch (error: any) {
console.error('API Error:', error);
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
if (isAI) {
throw new Error('AI analysis is taking longer than expected. This is normal for complex AI operations. Please wait a moment and try again.');
} else {
throw new Error('Request timed out. Please check your connection and try again.');
}
} else if (error.response) {
// Server responded with error status
const message = error.response.data?.detail || error.response.data?.message || 'API request failed';
throw new Error(message);
} else if (error.request) {
// Request was made but no response received
if (isAI) {
throw new Error('AI service is not responding. The AI analysis may be in progress. Please wait and try again.');
} else {
throw new Error('No response from server. Please check your connection.');
}
} else {
// Something else happened
throw new Error('An unexpected error occurred.');
}
}
}
// Wrapped methods with error handling
async createStrategySafe(strategy: ContentStrategyCreate) {
return this.handleRequest(() => this.createStrategy(strategy));
}
async getStrategiesSafe(userId?: number) {
return this.handleRequest(() => this.getStrategies(userId));
}
async createEventSafe(event: CalendarEventCreate) {
return this.handleRequest(() => this.createEvent(event));
}
async getEventsSafe(userId?: number, filters?: any) {
return this.handleRequest(() => this.getEvents(userId, filters));
}
async createGapAnalysisSafe(analysis: GapAnalysisCreate) {
return this.handleRequest(() => this.createGapAnalysis(analysis));
}
async getGapAnalysesSafe(userId?: number) {
return this.handleRequest(() => this.getGapAnalyses(userId));
}
async analyzeContentGapsSafe(params: {
website_url: string;
competitors: string[];
keywords: string[];
user_id?: number;
}) {
return this.handleRequest(() => this.analyzeContentGaps(params), true);
}
async getAIAnalyticsSafe(userId?: number) {
return this.handleRequest(() => this.getAIAnalytics(userId), true);
}
// AI Analytics with force refresh option
async getAIAnalyticsWithRefresh(userId?: number, forceRefresh = false): Promise<any> {
try {
const params: any = { user_id: userId || 1 };
if (forceRefresh) {
params.force_refresh = true;
}
const response = await apiClient.get(`${this.baseURL}/ai-analytics/`, { params });
return response.data;
} catch (error) {
console.error('Error getting AI analytics with refresh:', error);
return { insights: [], recommendations: [], total_insights: 0, total_recommendations: 0 };
}
}
async getGapAnalysesWithRefresh(userId?: number, forceRefresh = false): Promise<any> {
try {
const params: any = { user_id: userId || 1 };
if (forceRefresh) {
params.force_refresh = true;
}
const response = await apiClient.get(`${this.baseURL}/gap-analysis/`, { params });
return response.data;
} catch (error) {
console.error('Error getting gap analyses with refresh:', error);
return { gap_analyses: [], total_gaps: 0 };
}
}
// New Calendar Generation APIs
async generateCalendar(request: CalendarGenerationRequest): Promise<CalendarGenerationResponse> {
const response = await apiClient.post(`${this.baseURL}/generate-calendar`, request);
return response.data;
}
async optimizeContent(request: ContentOptimizationRequest): Promise<ContentOptimizationResponse> {
const response = await apiClient.post(`${this.baseURL}/optimize-content`, request);
return response.data;
}
async predictPerformance(request: PerformancePredictionRequest): Promise<PerformancePredictionResponse> {
const response = await apiClient.post(`${this.baseURL}/performance-predictions`, request);
return response.data;
}
async repurposeContent(request: ContentRepurposingRequest): Promise<ContentRepurposingResponse> {
const response = await apiClient.post(`${this.baseURL}/repurpose-content`, request);
return response.data;
}
async getTrendingTopics(request: TrendingTopicsRequest): Promise<TrendingTopicsResponse> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/trending-topics`, request);
return response.data;
});
}
async getComprehensiveUserData(userId?: number): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/comprehensive-user-data`, {
params: { user_id: userId }
});
return response.data;
});
}
async generateComprehensiveCalendar(config: any): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/generate-comprehensive-calendar`, config);
return response.data;
});
}
async checkCalendarGenerationHealth(): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/calendar-generation/health`);
return response.data;
});
}
// Enhanced Strategy API Methods
async createEnhancedStrategy(strategy: any): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/create`, strategy);
// Extract data from the response wrapper
return response.data.data || response.data;
});
}
async updateEnhancedStrategy(id: string, updates: any): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.put(`${this.baseURL}/enhanced-strategies/${id}`, updates);
return response.data.data || response.data;
});
}
async deleteEnhancedStrategy(id: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.delete(`${this.baseURL}/enhanced-strategies/${id}`);
return response.data.data || response.data;
});
}
async getEnhancedStrategies(userId?: number): Promise<any> {
return this.handleRequest(async () => {
const params = userId ? { user_id: userId } : {};
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies`, { params });
return response.data.data || response.data;
});
}
async getEnhancedStrategy(id: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/${id}`);
return response.data.data || response.data;
});
}
async generateEnhancedAIRecommendations(strategyId: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/${strategyId}/ai-recommendations`);
return response.data.data || response.data;
}, true);
}
async regenerateAIAnalysis(strategyId: string, analysisType: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/${strategyId}/ai-analysis/regenerate`, {
analysis_type: analysisType
});
return response.data;
}, true);
}
async getEnhancedAIAnalyses(strategyId: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/${strategyId}/ai-analyses`);
return response.data;
});
}
async getOnboardingData(userId?: number): Promise<any> {
return this.handleRequest(async () => {
const params = userId ? { user_id: userId } : {};
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/onboarding-data`, { params });
return response.data;
});
}
async getOnboardingIntegration(strategyId: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/${strategyId}/onboarding-integration`);
return response.data;
});
}
async getEnhancedStrategyAnalytics(strategyId: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/${strategyId}/analytics`);
return response.data;
});
}
async getEnhancedStrategyCompletion(strategyId: string): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/${strategyId}/completion`);
return response.data;
});
}
async getEnhancedStrategyTooltips(): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/tooltips`);
return response.data;
});
}
async getEnhancedStrategyDisclosureSteps(): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/disclosure-steps`);
return response.data;
});
}
// Enhanced Strategy Streaming Methods
async streamEnhancedStrategies(userId?: number): Promise<EventSource> {
const url = `${this.baseURL}/enhanced-strategies/stream/strategies?user_id=${userId || 1}`;
return new EventSource(url);
}
async streamStrategicIntelligence(userId?: number): Promise<EventSource> {
const url = `${this.baseURL}/enhanced-strategies/stream/strategic-intelligence?user_id=${userId || 1}`;
return new EventSource(url);
}
async streamKeywordResearch(userId?: number): Promise<EventSource> {
const url = `${this.baseURL}/enhanced-strategies/stream/keyword-research?user_id=${userId || 1}`;
return new EventSource(url);
}
// Helper method to handle SSE data
handleSSEData(eventSource: EventSource, onData: (data: any) => void, onError?: (error: any) => void, onComplete?: () => void) {
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onData(data);
// Close connection when we get a result or error
if (data.type === 'result' || data.type === 'error') {
eventSource.close();
onComplete?.();
}
} catch (error) {
console.error('Error parsing SSE data:', error);
onError?.(error);
}
};
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
onError?.(error);
eventSource.close();
};
return eventSource;
}
}
// Export singleton instance
export const contentPlanningApi = new ContentPlanningAPI();

View File

@@ -0,0 +1,403 @@
import { contentPlanningApi } from './contentPlanningApi';
export interface ServiceStatus {
name: string;
status: 'loading' | 'success' | 'error' | 'idle';
progress: number;
message: string;
data?: any;
error?: string;
}
export interface DashboardData {
strategies: any[];
gapAnalyses: any[];
aiInsights: any[];
aiRecommendations: any[];
calendarEvents: any[];
healthStatus: {
backend: boolean;
database: boolean;
aiServices: boolean;
};
}
export class ContentPlanningOrchestrator {
private serviceStatuses: Map<string, ServiceStatus> = new Map();
private onProgressUpdate?: (statuses: ServiceStatus[]) => void;
private onDataUpdate?: (data: Partial<DashboardData>) => void;
constructor() {
this.initializeServiceStatuses();
}
private initializeServiceStatuses() {
const services = [
{ name: 'strategies', displayName: 'Content Strategies' },
{ name: 'gapAnalyses', displayName: 'Gap Analysis' },
{ name: 'aiAnalytics', displayName: 'AI Analytics' },
{ name: 'calendarEvents', displayName: 'Calendar Events' },
{ name: 'healthCheck', displayName: 'System Health' }
];
services.forEach(service => {
this.serviceStatuses.set(service.name, {
name: service.displayName,
status: 'idle',
progress: 0,
message: 'Ready to load'
});
});
}
public setProgressCallback(callback: (statuses: ServiceStatus[]) => void) {
this.onProgressUpdate = callback;
}
public setDataUpdateCallback(callback: (data: Partial<DashboardData>) => void) {
this.onDataUpdate = callback;
}
private updateServiceStatus(name: string, updates: Partial<ServiceStatus>) {
const current = this.serviceStatuses.get(name);
if (current) {
const updated = { ...current, ...updates };
this.serviceStatuses.set(name, updated);
this.notifyProgressUpdate();
}
}
private notifyProgressUpdate() {
if (this.onProgressUpdate) {
this.onProgressUpdate(Array.from(this.serviceStatuses.values()));
}
}
private notifyDataUpdate(data: Partial<DashboardData>) {
if (this.onDataUpdate) {
this.onDataUpdate(data);
}
}
public async loadDashboardData(): Promise<DashboardData> {
// Reset all service statuses
this.serviceStatuses.forEach((status, name) => {
this.updateServiceStatus(name, {
status: 'loading',
progress: 0,
message: 'Initializing...'
});
});
// Start parallel requests
const promises = [
this.loadStrategies(),
this.loadGapAnalyses(),
this.loadAIAnalytics(),
this.loadCalendarEvents(),
this.loadHealthStatus()
];
// Wait for all to complete but handle each independently
const results = await Promise.allSettled(promises);
// Compile final data
const dashboardData: DashboardData = {
strategies: [],
gapAnalyses: [],
aiInsights: [],
aiRecommendations: [],
calendarEvents: [],
healthStatus: {
backend: false,
database: false,
aiServices: false
}
};
results.forEach((result) => {
if (result.status === 'fulfilled') {
const data = result.value;
// Type-safe data assignment
if ('strategies' in data) dashboardData.strategies = data.strategies;
if ('gapAnalyses' in data) dashboardData.gapAnalyses = data.gapAnalyses;
if ('aiInsights' in data) dashboardData.aiInsights = data.aiInsights;
if ('aiRecommendations' in data) dashboardData.aiRecommendations = data.aiRecommendations;
if ('calendarEvents' in data) dashboardData.calendarEvents = data.calendarEvents;
if ('healthStatus' in data) dashboardData.healthStatus = data.healthStatus;
}
});
return dashboardData;
}
private async loadStrategies() {
try {
this.updateServiceStatus('strategies', {
status: 'loading',
progress: 10,
message: 'Loading content strategies...'
});
const strategies = await contentPlanningApi.getStrategiesSafe();
this.updateServiceStatus('strategies', {
status: 'loading',
progress: 50,
message: 'Processing strategy data...'
});
// Simulate processing time for better UX
await new Promise(resolve => setTimeout(resolve, 500));
this.updateServiceStatus('strategies', {
status: 'success',
progress: 100,
message: `Loaded ${strategies.length} content strategies`,
data: strategies
});
this.notifyDataUpdate({ strategies });
return { strategies };
} catch (error: any) {
this.updateServiceStatus('strategies', {
status: 'error',
progress: 0,
message: 'Failed to load strategies',
error: error.message
});
return { strategies: [] };
}
}
private async loadGapAnalyses() {
try {
this.updateServiceStatus('gapAnalyses', {
status: 'loading',
progress: 10,
message: 'Initializing gap analysis...'
});
const response = await contentPlanningApi.getGapAnalysesSafe();
this.updateServiceStatus('gapAnalyses', {
status: 'loading',
progress: 30,
message: 'Analyzing content gaps...'
});
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 800));
this.updateServiceStatus('gapAnalyses', {
status: 'loading',
progress: 70,
message: 'Processing gap analysis results...'
});
await new Promise(resolve => setTimeout(resolve, 500));
this.updateServiceStatus('gapAnalyses', {
status: 'success',
progress: 100,
message: `Found ${response.gap_analyses?.length || 0} content gaps`,
data: response
});
this.notifyDataUpdate({ gapAnalyses: response.gap_analyses || [] });
return { gapAnalyses: response.gap_analyses || [] };
} catch (error: any) {
this.updateServiceStatus('gapAnalyses', {
status: 'error',
progress: 0,
message: 'Failed to load gap analysis',
error: error.message
});
return { gapAnalyses: [] };
}
}
private async loadAIAnalytics() {
try {
this.updateServiceStatus('aiAnalytics', {
status: 'loading',
progress: 10,
message: 'Initializing AI analysis...'
});
return new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>((resolve, reject) => {
contentPlanningApi.streamAIAnalytics(
// Progress callback
(progressData) => {
this.updateServiceStatus('aiAnalytics', {
progress: progressData.progress,
message: progressData.message || 'AI analysis in progress...'
});
},
// Complete callback
(aiData) => {
this.updateServiceStatus('aiAnalytics', {
status: 'success',
progress: 100,
message: `Generated ${aiData.insights?.length || 0} insights and ${aiData.recommendations?.length || 0} recommendations`,
data: aiData
});
this.notifyDataUpdate({
aiInsights: aiData.insights || [],
aiRecommendations: aiData.recommendations || []
});
resolve({
aiInsights: aiData.insights || [],
aiRecommendations: aiData.recommendations || []
});
},
// Error callback
(error) => {
this.updateServiceStatus('aiAnalytics', {
status: 'error',
progress: 0,
message: 'AI analysis failed',
error: error.message
});
reject(error);
}
);
});
} catch (error: any) {
this.updateServiceStatus('aiAnalytics', {
status: 'error',
progress: 0,
message: 'AI analysis failed',
error: error.message
});
return { aiInsights: [], aiRecommendations: [] };
}
}
private async loadCalendarEvents() {
try {
this.updateServiceStatus('calendarEvents', {
status: 'loading',
progress: 10,
message: 'Loading calendar events...'
});
const calendarEvents = await contentPlanningApi.getEventsSafe();
this.updateServiceStatus('calendarEvents', {
status: 'loading',
progress: 50,
message: 'Processing calendar data...'
});
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 300));
this.updateServiceStatus('calendarEvents', {
status: 'success',
progress: 100,
message: `Loaded ${calendarEvents.length} calendar events`,
data: calendarEvents
});
this.notifyDataUpdate({ calendarEvents });
return { calendarEvents };
} catch (error: any) {
this.updateServiceStatus('calendarEvents', {
status: 'error',
progress: 0,
message: 'Failed to load calendar events',
error: error.message
});
return { calendarEvents: [] };
}
}
private async loadHealthStatus() {
try {
this.updateServiceStatus('healthCheck', {
status: 'loading',
progress: 25,
message: 'Checking system health...'
});
const [backendHealth, aiHealth] = await Promise.allSettled([
contentPlanningApi.checkBackendHealth(),
contentPlanningApi.checkAIHealth()
]);
const healthStatus = {
backend: backendHealth.status === 'fulfilled' && backendHealth.value.status === 'healthy',
database: backendHealth.status === 'fulfilled' && backendHealth.value.services?.database_connection === true,
aiServices: aiHealth.status === 'fulfilled' && aiHealth.value.status === 'healthy'
};
this.updateServiceStatus('healthCheck', {
status: 'success',
progress: 100,
message: 'System health check complete',
data: healthStatus
});
this.notifyDataUpdate({ healthStatus });
return { healthStatus };
} catch (error: any) {
this.updateServiceStatus('healthCheck', {
status: 'error',
progress: 0,
message: 'Health check failed',
error: error.message
});
return {
healthStatus: {
backend: false,
database: false,
aiServices: false
}
};
}
}
public getServiceStatuses(): ServiceStatus[] {
return Array.from(this.serviceStatuses.values());
}
public refreshService(serviceName: string) {
const status = this.serviceStatuses.get(serviceName);
if (status) {
this.updateServiceStatus(serviceName, {
status: 'loading',
progress: 0,
message: 'Refreshing...'
});
// Re-run the specific service
switch (serviceName) {
case 'strategies':
this.loadStrategies();
break;
case 'gapAnalyses':
this.loadGapAnalyses();
break;
case 'aiAnalytics':
this.loadAIAnalytics();
break;
case 'calendarEvents':
this.loadCalendarEvents();
break;
case 'healthCheck':
this.loadHealthStatus();
break;
}
}
}
}
// Export singleton instance
export const contentPlanningOrchestrator = new ContentPlanningOrchestrator();

View File

@@ -0,0 +1,167 @@
# Zustand Store Implementation
This directory contains Zustand stores for managing state across the Alwrity dashboard components.
## Overview
Zustand has been implemented to replace the previous state management approach using custom hooks and local state. This provides:
- **Centralized state management** across components
- **Automatic persistence** with Zustand's persist middleware
- **Better performance** with selective re-renders
- **Simpler state updates** with immer-like syntax
- **Better debugging** with Redux DevTools support
## Stores
### 1. `dashboardStore.ts` - Main Dashboard Store
Manages state for the main dashboard including:
- Search and filter state
- Favorites management
- Snackbar notifications
- Loading and error states
**Key Features:**
- Automatic persistence of favorites and filter preferences
- Snackbar management with automatic hiding
- Optimized re-renders with selective state subscriptions
**Usage:**
```typescript
import { useDashboardStore } from '../stores/dashboardStore';
const {
loading,
error,
searchQuery,
favorites,
toggleFavorite,
setSearchQuery,
showSnackbar,
} = useDashboardStore();
```
### 2. `seoDashboardStore.ts` - SEO Dashboard Store
Manages state for the SEO dashboard including:
- Dashboard data fetching and caching
- Loading and error states
- Data refresh functionality
**Key Features:**
- Automatic data fetching on component mount
- Error handling with retry functionality
- Data caching with last updated timestamp
- DevTools integration for debugging
**Usage:**
```typescript
import { useSEODashboardStore } from '../stores/seoDashboardStore';
const {
loading,
error,
data,
fetchDashboardData,
refreshData,
} = useSEODashboardStore();
```
### 3. `sharedDashboardStore.ts` - Shared Dashboard Store
Manages common functionality across all dashboards:
- Sidebar state
- Theme management
- Global notifications
**Key Features:**
- Theme switching with system preference detection
- Notification management with auto-cleanup
- Sidebar state management
**Usage:**
```typescript
import { useSharedDashboardStore } from '../stores/sharedDashboardStore';
const {
isSidebarOpen,
currentTheme,
notifications,
toggleSidebar,
setTheme,
addNotification,
} = useSharedDashboardStore();
```
## Benefits Over Previous Implementation
### Before (Custom Hooks + Local State)
```typescript
// MainDashboard - Custom hook with manual localStorage
const useDashboardState = () => {
const [state, setState] = useState<DashboardState>({...});
// Manual localStorage handling
// Complex state updates
// No cross-component communication
};
// SEODashboard - Local state
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<SEODashboardData | null>(null);
```
### After (Zustand Stores)
```typescript
// Centralized, persistent, and optimized
const {
loading,
error,
data,
fetchDashboardData,
} = useSEODashboardStore();
// Automatic persistence
const {
favorites,
searchQuery,
toggleFavorite,
} = useDashboardStore();
```
## Performance Improvements
1. **Selective Re-renders**: Components only re-render when their specific state changes
2. **Automatic Persistence**: No manual localStorage management needed
3. **Optimized Updates**: Zustand's internal optimizations reduce unnecessary renders
4. **DevTools Integration**: Better debugging and state inspection
## Migration Notes
- The old `useDashboardState` hook can be removed after confirming the new implementation works correctly
- All localStorage operations are now handled automatically by Zustand's persist middleware
- Error handling is more robust with centralized error states
- Snackbar management is simplified with automatic cleanup
## Future Enhancements
1. **Real-time Updates**: Can easily add WebSocket integration for live data updates
2. **Offline Support**: Zustand's persistence can be extended for offline functionality
3. **State Synchronization**: Multiple tabs can share state through storage events
4. **Advanced Caching**: Can implement more sophisticated caching strategies
## Testing
The stores can be tested independently:
```typescript
import { renderHook, act } from '@testing-library/react';
import { useDashboardStore } from './dashboardStore';
test('should toggle favorite', () => {
const { result } = renderHook(() => useDashboardStore());
act(() => {
result.current.toggleFavorite('test-tool');
});
expect(result.current.favorites).toContain('test-tool');
});
```

View File

@@ -0,0 +1,680 @@
import { create } from 'zustand';
import { contentPlanningApi } from '../services/contentPlanningApi';
// Types
export interface ContentStrategy {
id: string;
name: string;
description: string;
industry: string;
target_audience: string;
content_pillars: string[];
created_at: string;
updated_at: string;
user_id?: number;
}
export interface CalendarEvent {
id: string;
title: string;
description: string;
date: string;
scheduled_date?: string;
platform: string;
content_type: string;
status: 'draft' | 'scheduled' | 'published';
strategy_id?: string;
user_id?: number;
created_at: string;
updated_at: string;
}
export interface ContentGapAnalysis {
id: string;
website_url: string;
competitors: string[];
keywords: string[];
gaps: string[];
recommendations: AIRecommendation[];
created_at: string;
user_id?: number;
}
export interface AIRecommendation {
id: string;
type: 'strategy' | 'topic' | 'timing' | 'platform' | 'optimization';
title: string;
description: string;
confidence: number;
reasoning: string;
action_items: string[];
status: 'pending' | 'accepted' | 'rejected' | 'modified';
}
export interface AIInsight {
id: string;
type: 'performance' | 'opportunity' | 'warning' | 'trend';
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
created_at: string;
}
export interface PerformanceMetrics {
engagement: number;
reach: number;
conversion: number;
roi: number;
time_range: string;
}
// New Calendar Generation Types
export interface GeneratedCalendar {
user_id: number;
strategy_id?: number;
calendar_type: string;
industry: string;
business_size: string;
generated_at: string;
content_pillars: string[];
platform_strategies: any;
content_mix: Record<string, number>;
daily_schedule: any[];
weekly_themes: any[];
content_recommendations: any[];
optimal_timing: any;
performance_predictions: any;
trending_topics: any[];
repurposing_opportunities: any[];
ai_insights: any[];
competitor_analysis: any;
gap_analysis_insights: any;
strategy_insights: any;
onboarding_insights: any;
processing_time: number;
ai_confidence: number;
}
export interface ContentOptimization {
user_id: number;
event_id?: number;
original_content: any;
optimized_content: any;
platform_adaptations: string[];
visual_recommendations: string[];
hashtag_suggestions: string[];
keyword_optimization: any;
tone_adjustments: any;
length_optimization: any;
performance_prediction: any;
optimization_score: number;
recommendations?: any[];
created_at: string;
}
export interface PerformancePrediction {
user_id: number;
strategy_id?: number;
content_type: string;
platform: string;
predicted_engagement_rate: number;
predicted_reach: number;
predicted_conversions: number;
predicted_roi: number;
confidence_score: number;
recommendations: string[];
created_at: string;
}
export interface ContentRepurposing {
user_id: number;
strategy_id?: number;
original_content: any;
platform_adaptations: any[];
transformations: any[];
implementation_tips: string[];
gap_addresses: string[];
created_at: string;
}
export interface TrendingTopics {
user_id: number;
industry: string;
trending_topics: any[];
gap_relevance_scores: Record<string, number>;
audience_alignment_scores: Record<string, number>;
created_at: string;
}
// Store interface
interface ContentPlanningStore {
// State
strategies: ContentStrategy[];
currentStrategy: ContentStrategy | null;
calendarEvents: CalendarEvent[];
gapAnalyses: ContentGapAnalysis[];
aiRecommendations: AIRecommendation[];
aiInsights: AIInsight[];
performanceMetrics: PerformanceMetrics | null;
// New Calendar Generation State
generatedCalendar: GeneratedCalendar | null;
contentOptimization: ContentOptimization | null;
performancePrediction: PerformancePrediction | null;
contentRepurposing: ContentRepurposing | null;
trendingTopics: TrendingTopics | null;
calendarGenerationLoading: boolean;
calendarGenerationError: string | null;
// UI state
loading: boolean;
error: string | null;
activeTab: 'strategy' | 'calendar' | 'analytics' | 'gaps';
dataLoading: boolean;
// Actions
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setActiveTab: (tab: 'strategy' | 'calendar' | 'analytics' | 'gaps') => void;
// Strategy actions
createStrategy: (strategy: Omit<ContentStrategy, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
updateStrategy: (id: string, updates: Partial<ContentStrategy>) => Promise<void>;
deleteStrategy: (id: string) => Promise<void>;
setCurrentStrategy: (strategy: ContentStrategy | null) => void;
// Calendar actions
createEvent: (event: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
updateEvent: (id: string, updates: Partial<CalendarEvent>) => Promise<void>;
deleteEvent: (id: string) => Promise<void>;
// Gap analysis actions
createGapAnalysis: (analysis: Omit<ContentGapAnalysis, 'id' | 'created_at'>) => Promise<void>;
updateGapAnalysis: (id: string, updates: Partial<ContentGapAnalysis>) => Promise<void>;
analyzeContentGaps: (params: { website_url: string; competitors: string[]; keywords: string[] }) => Promise<void>;
// AI actions
addAIRecommendation: (recommendation: AIRecommendation) => void;
updateAIRecommendation: (id: string, status: AIRecommendation['status']) => void;
addAIInsight: (insight: AIInsight) => void;
// Analytics actions
setPerformanceMetrics: (metrics: PerformanceMetrics) => void;
// Load data
loadStrategies: () => Promise<void>;
loadCalendarEvents: () => Promise<void>;
loadGapAnalyses: () => Promise<void>;
loadAIInsights: () => Promise<void>;
loadAIRecommendations: () => Promise<void>;
// Update data (for orchestrator)
updateStrategies: (strategies: ContentStrategy[]) => void;
updateCalendarEvents: (events: CalendarEvent[]) => void;
updateGapAnalyses: (analyses: ContentGapAnalysis[]) => void;
updateAIInsights: (data: { insights: AIInsight[]; recommendations: AIRecommendation[] }) => void;
// Health checks
checkHealth: () => Promise<boolean>;
checkDatabaseHealth: () => Promise<boolean>;
// New Calendar Generation Actions
generateCalendar: (request: {
user_id: number;
strategy_id?: number;
calendar_type: string;
industry?: string;
business_size: string;
force_refresh?: boolean;
}) => Promise<void>;
optimizeContent: (request: {
user_id: number;
event_id?: number;
title: string;
description: string;
content_type: string;
target_platform: string;
original_content?: any;
}) => Promise<void>;
predictPerformance: (request: {
user_id: number;
strategy_id?: number;
content_type: string;
platform: string;
content_data: any;
}) => Promise<void>;
repurposeContent: (request: {
user_id: number;
strategy_id?: number;
original_content: any;
target_platforms: string[];
}) => Promise<void>;
getTrendingTopics: (request: {
user_id: number;
industry: string;
limit?: number;
}) => Promise<void>;
setCalendarGenerationLoading: (loading: boolean) => void;
setCalendarGenerationError: (error: string | null) => void;
clearCalendarGenerationData: () => void;
}
// Store implementation
export const useContentPlanningStore = create<ContentPlanningStore>((set, get) => ({
// Initial state
strategies: [],
currentStrategy: null,
calendarEvents: [],
gapAnalyses: [],
aiRecommendations: [],
aiInsights: [],
performanceMetrics: null,
// New Calendar Generation State
generatedCalendar: null,
contentOptimization: null,
performancePrediction: null,
contentRepurposing: null,
trendingTopics: null,
calendarGenerationLoading: false,
calendarGenerationError: null,
loading: false,
error: null,
activeTab: 'strategy',
dataLoading: false,
// UI actions
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setActiveTab: (activeTab) => set({ activeTab }),
// Strategy actions
createStrategy: async (strategy) => {
set({ loading: true, error: null });
try {
const newStrategy = await contentPlanningApi.createStrategySafe({
name: strategy.name,
description: strategy.description,
industry: strategy.industry,
target_audience: strategy.target_audience,
content_pillars: strategy.content_pillars,
user_id: strategy.user_id
});
set((state) => ({
strategies: [...state.strategies, newStrategy],
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to create strategy', loading: false });
}
},
updateStrategy: async (id, updates) => {
set({ loading: true, error: null });
try {
const updatedStrategy = await contentPlanningApi.updateStrategy(id, updates);
set((state) => ({
strategies: state.strategies.map((strategy) =>
strategy.id === id ? updatedStrategy : strategy
),
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to update strategy', loading: false });
}
},
deleteStrategy: async (id) => {
set({ loading: true, error: null });
try {
await contentPlanningApi.deleteStrategy(id);
set((state) => ({
strategies: state.strategies.filter((strategy) => strategy.id !== id),
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to delete strategy', loading: false });
}
},
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
// Calendar actions
createEvent: async (event) => {
set({ loading: true, error: null });
try {
const newEvent = await contentPlanningApi.createEventSafe({
title: event.title,
description: event.description,
date: event.date,
platform: event.platform,
content_type: event.content_type,
status: event.status,
strategy_id: event.strategy_id,
user_id: event.user_id
});
set((state) => ({
calendarEvents: [...state.calendarEvents, newEvent],
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to create event', loading: false });
}
},
updateEvent: async (id, updates) => {
set({ loading: true, error: null });
try {
const updatedEvent = await contentPlanningApi.updateEvent(id, updates);
set((state) => ({
calendarEvents: state.calendarEvents.map((event) =>
event.id === id ? updatedEvent : event
),
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to update event', loading: false });
}
},
deleteEvent: async (id) => {
set({ loading: true, error: null });
try {
await contentPlanningApi.deleteEvent(id);
set((state) => ({
calendarEvents: state.calendarEvents.filter((event) => event.id !== id),
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to delete event', loading: false });
}
},
// Gap analysis actions
createGapAnalysis: async (analysis) => {
set({ loading: true, error: null });
try {
const newAnalysis = await contentPlanningApi.createGapAnalysisSafe({
website_url: analysis.website_url,
competitors: analysis.competitors,
keywords: analysis.keywords,
user_id: analysis.user_id
});
set((state) => ({
gapAnalyses: [...state.gapAnalyses, newAnalysis],
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to create gap analysis', loading: false });
}
},
updateGapAnalysis: async (id, updates) => {
set({ loading: true, error: null });
try {
const updatedAnalysis = await contentPlanningApi.updateGapAnalysis(id, updates);
set((state) => ({
gapAnalyses: state.gapAnalyses.map((analysis) =>
analysis.id === id ? updatedAnalysis : analysis
),
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to update gap analysis', loading: false });
}
},
analyzeContentGaps: async (params) => {
set({ loading: true, error: null });
try {
const analysisResult = await contentPlanningApi.analyzeContentGapsSafe(params);
// Add the analysis result to the store
set((state) => ({
gapAnalyses: [...state.gapAnalyses, analysisResult],
loading: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to analyze content gaps', loading: false });
}
},
// AI actions
addAIRecommendation: (recommendation) => {
set((state) => ({
aiRecommendations: [...state.aiRecommendations, recommendation],
}));
},
updateAIRecommendation: (id, status) => {
set((state) => ({
aiRecommendations: state.aiRecommendations.map((rec) =>
rec.id === id ? { ...rec, status } : rec
),
}));
},
addAIInsight: (insight) => {
set((state) => ({
aiInsights: [...state.aiInsights, insight],
}));
},
// Analytics actions
setPerformanceMetrics: (metrics) => set({ performanceMetrics: metrics }),
// Load data actions
loadStrategies: async () => {
set({ loading: true, error: null });
try {
const strategies = await contentPlanningApi.getStrategiesSafe();
set({ strategies, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load strategies', loading: false });
}
},
loadCalendarEvents: async () => {
set({ loading: true, error: null });
try {
const events = await contentPlanningApi.getEventsSafe();
set({ calendarEvents: events, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load calendar events', loading: false });
}
},
loadGapAnalyses: async () => {
set({ loading: true, error: null });
try {
const analyses = await contentPlanningApi.getGapAnalyses();
set({ gapAnalyses: analyses, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load gap analyses', loading: false });
}
},
loadAIInsights: async () => {
set({ loading: true, error: null });
try {
const response = await contentPlanningApi.getAIAnalyticsSafe();
// Validate response structure
if (!response || typeof response !== 'object') {
console.warn('Invalid AI analytics response:', response);
set({ aiInsights: [], loading: false });
return;
}
// Handle the response structure - it returns an object with insights array
const insights = Array.isArray(response.insights) ? response.insights : [];
// If no insights from backend, create some default insights from recommendations
let transformedInsights = insights;
if (insights.length === 0 && response.recommendations && Array.isArray(response.recommendations)) {
transformedInsights = response.recommendations.slice(0, 3).map((rec: any, index: number) => ({
id: `insight_${Date.now()}_${index}`,
type: 'opportunity',
title: rec.title || 'AI Insight',
description: rec.description || 'AI-generated insight',
priority: rec.priority === 'High' ? 'high' : rec.priority === 'Medium' ? 'medium' : 'low',
created_at: new Date().toISOString()
}));
} else {
// Transform insights data to the expected format
transformedInsights = insights.map((insight: any) => ({
id: insight.id || `insight_${Date.now()}`,
type: insight.type || 'performance',
title: insight.title || 'AI Insight',
description: insight.description || 'AI-generated insight',
priority: insight.priority || 'medium',
created_at: insight.created_at || new Date().toISOString()
}));
}
set({ aiInsights: transformedInsights, loading: false });
} catch (error: any) {
console.error('Error loading AI insights:', error);
set({ error: error.message || 'Failed to load AI insights', loading: false, aiInsights: [] });
}
},
loadAIRecommendations: async () => {
set({ loading: true, error: null });
try {
const response = await contentPlanningApi.getAIAnalyticsSafe();
// Validate response structure
if (!response || typeof response !== 'object') {
console.warn('Invalid AI analytics response:', response);
set({ aiRecommendations: [], loading: false });
return;
}
// Handle the response structure - it returns an object with recommendations array
const recommendations = Array.isArray(response.recommendations) ? response.recommendations : [];
// Transform recommendations data to the expected format
const transformedRecommendations = recommendations.map((rec: any, index: number) => ({
id: rec.id || `rec_${Date.now()}_${index}`,
type: rec.type?.toLowerCase() || 'strategy',
title: rec.title || 'AI Recommendation',
description: rec.description || 'AI-generated recommendation',
confidence: rec.ai_confidence || rec.confidence || 0.8,
reasoning: rec.reasoning || rec.description || 'Generated by AI analysis',
action_items: Array.isArray(rec.content_suggestions) ? rec.content_suggestions : [],
status: rec.status || 'pending'
}));
set({ aiRecommendations: transformedRecommendations, loading: false });
} catch (error: any) {
console.error('Error loading AI recommendations:', error);
set({ error: error.message || 'Failed to load AI recommendations', loading: false, aiRecommendations: [] });
}
},
// Update data (for orchestrator)
updateStrategies: (strategies: ContentStrategy[]) => {
set({ strategies });
},
updateCalendarEvents: (events: CalendarEvent[]) => {
set({ calendarEvents: events });
},
updateGapAnalyses: (analyses: ContentGapAnalysis[]) => {
set({ gapAnalyses: analyses });
},
updateAIInsights: (data: { insights: AIInsight[]; recommendations: AIRecommendation[] }) => {
set({
aiInsights: data.insights,
aiRecommendations: data.recommendations
});
},
// Health checks
checkHealth: async () => {
try {
const health = await contentPlanningApi.checkHealth();
return health.status === 'healthy';
} catch (error) {
console.error('Health check failed:', error);
return false;
}
},
checkDatabaseHealth: async () => {
try {
const dbHealth = await contentPlanningApi.checkDatabaseHealth();
return dbHealth.status === 'healthy';
} catch (error) {
console.error('Database health check failed:', error);
return false;
}
},
// New Calendar Generation Actions
generateCalendar: async (request) => {
set({ calendarGenerationLoading: true, calendarGenerationError: null });
try {
const generatedCalendar = await contentPlanningApi.generateCalendar(request);
set({ generatedCalendar, calendarGenerationLoading: false });
} catch (error: any) {
set({ calendarGenerationError: error.message || 'Failed to generate calendar', calendarGenerationLoading: false });
}
},
optimizeContent: async (request) => {
set({ loading: true, error: null });
try {
const optimizedContent = await contentPlanningApi.optimizeContent(request);
set({ contentOptimization: optimizedContent, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to optimize content', loading: false });
}
},
predictPerformance: async (request) => {
set({ loading: true, error: null });
try {
const performancePrediction = await contentPlanningApi.predictPerformance(request);
set({ performancePrediction, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to predict performance', loading: false });
}
},
repurposeContent: async (request) => {
set({ loading: true, error: null });
try {
const contentRepurposing = await contentPlanningApi.repurposeContent(request);
set({ contentRepurposing, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to repurpose content', loading: false });
}
},
getTrendingTopics: async (request) => {
set({ loading: true, error: null });
try {
const trendingTopics = await contentPlanningApi.getTrendingTopics(request);
set({ trendingTopics, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to get trending topics', loading: false });
}
},
setCalendarGenerationLoading: (loading) => set({ calendarGenerationLoading: loading }),
setCalendarGenerationError: (error) => set({ calendarGenerationError: error }),
clearCalendarGenerationData: () => set({ generatedCalendar: null, contentOptimization: null, performancePrediction: null, contentRepurposing: null, trendingTopics: null }),
}));

View File

@@ -0,0 +1,110 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { DashboardState, SnackbarState } from '../components/shared/types';
export interface DashboardStore extends DashboardState {
// Actions
toggleFavorite: (toolName: string) => void;
setSearchQuery: (query: string) => void;
setSelectedCategory: (category: string | null) => void;
setSelectedSubCategory: (subCategory: string | null) => void;
setError: (error: string | null) => void;
setLoading: (loading: boolean) => void;
showSnackbar: (message: string, severity?: SnackbarState['severity']) => void;
hideSnackbar: () => void;
clearFilters: () => void;
}
export const useDashboardStore = create<DashboardStore>()(
persist(
(set, get) => ({
// Initial state
loading: false,
error: null,
searchQuery: '',
selectedCategory: null,
selectedSubCategory: null,
favorites: [],
snackbar: {
open: false,
message: '',
severity: 'info',
},
// Actions
toggleFavorite: (toolName: string) => {
const { favorites } = get();
const newFavorites = favorites.includes(toolName)
? favorites.filter(f => f !== toolName)
: [...favorites, toolName];
set({ favorites: newFavorites });
// Show snackbar
const message = favorites.includes(toolName)
? 'Removed from favorites'
: 'Added to favorites';
get().showSnackbar(message, 'success');
},
setSearchQuery: (query: string) => {
set({ searchQuery: query });
},
setSelectedCategory: (category: string | null) => {
set({
selectedCategory: category,
selectedSubCategory: null, // Reset sub-category when changing main category
});
},
setSelectedSubCategory: (subCategory: string | null) => {
set({ selectedSubCategory: subCategory });
},
setError: (error: string | null) => {
set({ error });
},
setLoading: (loading: boolean) => {
set({ loading });
},
showSnackbar: (message: string, severity: SnackbarState['severity'] = 'info') => {
set({
snackbar: {
open: true,
message,
severity,
},
});
},
hideSnackbar: () => {
set({
snackbar: {
...get().snackbar,
open: false,
},
});
},
clearFilters: () => {
set({
searchQuery: '',
selectedCategory: null,
selectedSubCategory: null,
});
},
}),
{
name: 'alwrity-dashboard-storage',
partialize: (state) => ({
favorites: state.favorites,
searchQuery: state.searchQuery,
selectedCategory: state.selectedCategory,
selectedSubCategory: state.selectedSubCategory,
}),
}
)
);

View File

@@ -0,0 +1,944 @@
import { create } from 'zustand';
import { contentPlanningApi } from '../services/contentPlanningApi';
// Enhanced Strategy Types
export interface EnhancedStrategy {
id: string;
user_id: number;
name: string;
industry: string;
// Business Context (8 inputs)
business_objectives?: any;
target_metrics?: any;
content_budget?: number;
team_size?: number;
implementation_timeline?: string;
market_share?: string;
competitive_position?: string;
performance_metrics?: any;
// Audience Intelligence (6 inputs)
content_preferences?: any;
consumption_patterns?: any;
audience_pain_points?: any;
buying_journey?: any;
seasonal_trends?: any;
engagement_metrics?: any;
// Competitive Intelligence (5 inputs)
top_competitors?: any;
competitor_content_strategies?: any;
market_gaps?: any;
industry_trends?: any;
emerging_trends?: any;
// Content Strategy (7 inputs)
preferred_formats?: any;
content_mix?: any;
content_frequency?: string;
optimal_timing?: any;
quality_metrics?: any;
editorial_guidelines?: any;
brand_voice?: any;
// Performance & Analytics (4 inputs)
traffic_sources?: any;
conversion_rates?: any;
content_roi_targets?: any;
ab_testing_capabilities?: boolean;
// Enhanced AI Analysis
comprehensive_ai_analysis?: any;
onboarding_data_used?: any;
strategic_scores?: any;
market_positioning?: any;
competitive_advantages?: any;
strategic_risks?: any;
opportunity_analysis?: any;
// Metadata
created_at: string;
updated_at: string;
completion_percentage: number;
data_source_transparency?: any;
}
export interface EnhancedAIAnalysis {
id: string;
user_id: number;
strategy_id: string;
analysis_type: string;
comprehensive_insights?: any;
audience_intelligence?: any;
competitive_intelligence?: any;
performance_optimization?: any;
content_calendar_optimization?: any;
onboarding_data_used?: any;
data_confidence_scores?: any;
recommendation_quality_scores?: any;
processing_time?: number;
ai_service_status: string;
prompt_version?: string;
created_at: string;
updated_at: string;
}
export interface OnboardingIntegration {
id: string;
user_id: number;
strategy_id: string;
website_analysis_data?: any;
research_preferences_data?: any;
api_keys_data?: any;
field_mappings?: any;
auto_populated_fields?: any;
user_overrides?: any;
data_quality_scores?: any;
confidence_levels?: any;
data_freshness?: any;
created_at: string;
updated_at: string;
}
export interface StrategicInputField {
id: string;
category: string;
label: string;
description: string;
tooltip: string;
type: 'text' | 'number' | 'select' | 'multiselect' | 'json' | 'boolean';
required: boolean;
options?: string[];
placeholder?: string;
validation?: any;
auto_populated?: boolean;
data_source?: string;
confidence_level?: number;
}
export interface ProgressiveDisclosureStep {
id: string;
title: string;
description: string;
fields: string[];
is_complete: boolean;
is_visible: boolean;
dependencies: string[];
}
export interface TooltipData {
field_id: string;
title: string;
description: string;
examples: string[];
best_practices: string[];
data_source?: string;
confidence_level?: number;
}
// Store interface
interface EnhancedStrategyStore {
// State
strategies: EnhancedStrategy[];
currentStrategy: EnhancedStrategy | null;
aiAnalyses: EnhancedAIAnalysis[];
onboardingIntegrations: OnboardingIntegration[];
// Progressive Disclosure
disclosureSteps: ProgressiveDisclosureStep[];
currentStep: number;
completedSteps: string[];
// Tooltips
tooltips: Record<string, TooltipData>;
// Form State
formData: Record<string, any>;
formErrors: Record<string, string>;
autoPopulatedFields: Record<string, any>;
dataSources: Record<string, string>;
// UI State
loading: boolean;
error: string | null;
saving: boolean;
aiGenerating: boolean;
// Actions
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setSaving: (saving: boolean) => void;
setAIGenerating: (generating: boolean) => void;
// Strategy actions
createEnhancedStrategy: (strategy: Partial<EnhancedStrategy>) => Promise<EnhancedStrategy>;
updateEnhancedStrategy: (id: string, updates: Partial<EnhancedStrategy>) => Promise<void>;
deleteEnhancedStrategy: (id: string) => Promise<void>;
setCurrentStrategy: (strategy: EnhancedStrategy | null) => void;
// Form actions
updateFormField: (fieldId: string, value: any) => void;
validateFormField: (fieldId: string) => boolean;
validateAllFields: () => boolean;
resetForm: () => void;
// Progressive disclosure actions
setCurrentStep: (step: number) => void;
completeStep: (stepId: string) => void;
canProceedToStep: (stepId: string) => boolean;
getNextStep: () => ProgressiveDisclosureStep | null;
getPreviousStep: () => ProgressiveDisclosureStep | null;
// Auto-population actions
autoPopulateFromOnboarding: () => Promise<void>;
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
// AI Analysis actions
generateAIRecommendations: (strategyId: string) => Promise<void>;
regenerateAIAnalysis: (strategyId: string, analysisType: string) => Promise<void>;
// Data loading
loadEnhancedStrategies: () => Promise<void>;
loadAIAnalyses: (strategyId: string) => Promise<void>;
loadOnboardingIntegration: (strategyId: string) => Promise<void>;
// Tooltip actions
getTooltipData: (fieldId: string) => TooltipData | null;
updateTooltipData: (fieldId: string, data: TooltipData) => void;
// Completion tracking
calculateCompletionPercentage: () => number;
getCompletionStats: () => {
total_fields: number;
filled_fields: number;
completion_percentage: number;
category_completion: Record<string, number>;
};
}
// Strategic input fields configuration
export const STRATEGIC_INPUT_FIELDS: StrategicInputField[] = [
// Business Context
{
id: 'business_objectives',
category: 'business_context',
label: 'Business Objectives',
description: 'Primary and secondary business goals for content strategy',
tooltip: 'Define your main business goals that content will support. Include both primary objectives (e.g., brand awareness) and secondary objectives (e.g., lead generation).',
type: 'json',
required: true,
placeholder: 'Enter your business objectives'
},
{
id: 'target_metrics',
category: 'business_context',
label: 'Target Metrics',
description: 'KPIs and success metrics for content performance',
tooltip: 'Specify the key performance indicators (KPIs) that will measure the success of your content strategy. Include metrics like traffic growth, engagement rates, and conversion rates.',
type: 'json',
required: true,
placeholder: 'Define your target metrics'
},
{
id: 'content_budget',
category: 'business_context',
label: 'Content Budget',
description: 'Monthly or annual budget for content creation',
tooltip: 'Set your content marketing budget. This helps determine the scope and scale of your content strategy, including team size, tools, and content production capabilities.',
type: 'number',
required: false,
placeholder: 'Enter your content budget'
},
{
id: 'team_size',
category: 'business_context',
label: 'Team Size',
description: 'Number of people working on content',
tooltip: 'Specify the size of your content team. This affects content production capacity and helps determine realistic content frequency and volume.',
type: 'number',
required: false,
placeholder: 'Enter team size'
},
{
id: 'implementation_timeline',
category: 'business_context',
label: 'Implementation Timeline',
description: 'Timeline for strategy implementation',
tooltip: 'Define how long you plan to implement this content strategy. Common timelines include 3 months, 6 months, or 1 year.',
type: 'select',
required: false,
options: ['3 months', '6 months', '1 year', '2 years', 'Ongoing']
},
{
id: 'market_share',
category: 'business_context',
label: 'Market Share',
description: 'Current market share percentage',
tooltip: 'Your current market share helps determine your competitive position and content strategy approach. Leaders focus on thought leadership, while challengers focus on differentiation.',
type: 'text',
required: false,
placeholder: 'Enter market share percentage'
},
{
id: 'competitive_position',
category: 'business_context',
label: 'Competitive Position',
description: 'Market position relative to competitors',
tooltip: 'Define your competitive position in the market. Options include Leader, Challenger, Niche, or Emerging. This influences your content strategy approach.',
type: 'select',
required: false,
options: ['Leader', 'Challenger', 'Niche', 'Emerging']
},
{
id: 'performance_metrics',
category: 'business_context',
label: 'Current Performance Metrics',
description: 'Existing performance data and benchmarks',
tooltip: 'Provide your current content performance metrics as a baseline. This helps measure improvement and set realistic targets.',
type: 'json',
required: false,
placeholder: 'Enter current performance data'
},
// Audience Intelligence
{
id: 'content_preferences',
category: 'audience_intelligence',
label: 'Content Preferences',
description: 'Preferred content formats and topics',
tooltip: 'Identify what types of content your audience prefers. Consider formats (blog posts, videos, infographics) and topics that resonate most.',
type: 'json',
required: true,
placeholder: 'Define content preferences'
},
{
id: 'consumption_patterns',
category: 'audience_intelligence',
label: 'Consumption Patterns',
description: 'When and how audience consumes content',
tooltip: 'Understand when and how your audience consumes content. This includes peak times, preferred devices, and consumption channels.',
type: 'json',
required: false,
placeholder: 'Describe consumption patterns'
},
{
id: 'audience_pain_points',
category: 'audience_intelligence',
label: 'Audience Pain Points',
description: 'Key challenges and pain points',
tooltip: 'Identify the main challenges and pain points your audience faces. This helps create content that addresses real needs and provides value.',
type: 'json',
required: false,
placeholder: 'List audience pain points'
},
{
id: 'buying_journey',
category: 'audience_intelligence',
label: 'Buying Journey',
description: 'Customer journey stages and touchpoints',
tooltip: 'Map your audience\'s buying journey stages and the content touchpoints that influence their decisions.',
type: 'json',
required: false,
placeholder: 'Define buying journey stages'
},
{
id: 'seasonal_trends',
category: 'audience_intelligence',
label: 'Seasonal Trends',
description: 'Seasonal content opportunities',
tooltip: 'Identify seasonal trends and opportunities that affect your audience\'s content consumption and needs.',
type: 'json',
required: false,
placeholder: 'Define seasonal trends'
},
{
id: 'engagement_metrics',
category: 'audience_intelligence',
label: 'Engagement Metrics',
description: 'Current engagement data',
tooltip: 'Provide current engagement metrics to understand what content resonates with your audience.',
type: 'json',
required: false,
placeholder: 'Enter engagement metrics'
},
// Competitive Intelligence
{
id: 'top_competitors',
category: 'competitive_intelligence',
label: 'Top Competitors',
description: 'List of main competitors',
tooltip: 'Identify your main competitors in the market. This helps understand competitive landscape and identify content opportunities.',
type: 'json',
required: false,
placeholder: 'List top competitors'
},
{
id: 'competitor_content_strategies',
category: 'competitive_intelligence',
label: 'Competitor Content Strategies',
description: 'Analysis of competitor approaches',
tooltip: 'Analyze your competitors\' content strategies to identify gaps, opportunities, and differentiation possibilities.',
type: 'json',
required: false,
placeholder: 'Analyze competitor strategies'
},
{
id: 'market_gaps',
category: 'competitive_intelligence',
label: 'Market Gaps',
description: 'Identified market opportunities',
tooltip: 'Identify gaps in the market that your content can address. These are opportunities where competitors are not providing adequate content.',
type: 'json',
required: false,
placeholder: 'Identify market gaps'
},
{
id: 'industry_trends',
category: 'competitive_intelligence',
label: 'Industry Trends',
description: 'Current industry trends',
tooltip: 'Stay current with industry trends that affect your audience and content strategy.',
type: 'json',
required: false,
placeholder: 'List industry trends'
},
{
id: 'emerging_trends',
category: 'competitive_intelligence',
label: 'Emerging Trends',
description: 'Upcoming trends and opportunities',
tooltip: 'Identify emerging trends that could provide early-mover advantages in content creation.',
type: 'json',
required: false,
placeholder: 'Identify emerging trends'
},
// Content Strategy
{
id: 'preferred_formats',
category: 'content_strategy',
label: 'Preferred Formats',
description: 'Content formats to focus on',
tooltip: 'Choose the content formats that align with your audience preferences and business objectives.',
type: 'multiselect',
required: true,
options: ['Blog Posts', 'Videos', 'Infographics', 'Webinars', 'Podcasts', 'Case Studies', 'Whitepapers', 'Social Media Posts']
},
{
id: 'content_mix',
category: 'content_strategy',
label: 'Content Mix',
description: 'Distribution of content types',
tooltip: 'Define the percentage distribution of different content types in your strategy.',
type: 'json',
required: false,
placeholder: 'Define content mix percentages'
},
{
id: 'content_frequency',
category: 'content_strategy',
label: 'Content Frequency',
description: 'How often to publish content',
tooltip: 'Set realistic content publishing frequency based on your team capacity and audience expectations.',
type: 'select',
required: true,
options: ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly']
},
{
id: 'optimal_timing',
category: 'content_strategy',
label: 'Optimal Timing',
description: 'Best times for publishing',
tooltip: 'Identify the optimal times for publishing different types of content to maximize engagement.',
type: 'json',
required: false,
placeholder: 'Define optimal publishing times'
},
{
id: 'quality_metrics',
category: 'content_strategy',
label: 'Quality Metrics',
description: 'Content quality standards',
tooltip: 'Define the quality standards and metrics that will ensure your content meets your audience\'s expectations.',
type: 'json',
required: false,
placeholder: 'Define quality standards'
},
{
id: 'editorial_guidelines',
category: 'content_strategy',
label: 'Editorial Guidelines',
description: 'Style and tone guidelines',
tooltip: 'Establish editorial guidelines for consistent brand voice, tone, and style across all content.',
type: 'json',
required: false,
placeholder: 'Define editorial guidelines'
},
{
id: 'brand_voice',
category: 'content_strategy',
label: 'Brand Voice',
description: 'Brand personality and voice',
tooltip: 'Define your brand\'s personality and voice characteristics to ensure consistent messaging.',
type: 'json',
required: false,
placeholder: 'Define brand voice'
},
// Performance & Analytics
{
id: 'traffic_sources',
category: 'performance_analytics',
label: 'Traffic Sources',
description: 'Primary traffic sources',
tooltip: 'Identify your main traffic sources to understand where your audience comes from and optimize accordingly.',
type: 'json',
required: false,
placeholder: 'Define traffic sources'
},
{
id: 'conversion_rates',
category: 'performance_analytics',
label: 'Conversion Rates',
description: 'Current conversion data',
tooltip: 'Track conversion rates across different content types and channels to identify what drives results.',
type: 'json',
required: false,
placeholder: 'Enter conversion data'
},
{
id: 'content_roi_targets',
category: 'performance_analytics',
label: 'Content ROI Targets',
description: 'ROI goals and targets',
tooltip: 'Set realistic ROI targets for your content marketing efforts to measure return on investment.',
type: 'json',
required: false,
placeholder: 'Define ROI targets'
},
{
id: 'ab_testing_capabilities',
category: 'performance_analytics',
label: 'A/B Testing Capabilities',
description: 'A/B testing availability',
tooltip: 'Indicate whether you have A/B testing capabilities to optimize content performance.',
type: 'boolean',
required: false
}
];
// Progressive disclosure steps
const PROGRESSIVE_DISCLOSURE_STEPS: ProgressiveDisclosureStep[] = [
{
id: 'business_context',
title: 'Business Context',
description: 'Define your business objectives and context',
fields: ['business_objectives', 'target_metrics', 'content_budget', 'team_size', 'implementation_timeline', 'market_share', 'competitive_position', 'performance_metrics'],
is_complete: false,
is_visible: true,
dependencies: []
},
{
id: 'audience_intelligence',
title: 'Audience Intelligence',
description: 'Understand your target audience',
fields: ['content_preferences', 'consumption_patterns', 'audience_pain_points', 'buying_journey', 'seasonal_trends', 'engagement_metrics'],
is_complete: false,
is_visible: false,
dependencies: ['business_context']
},
{
id: 'competitive_intelligence',
title: 'Competitive Intelligence',
description: 'Analyze your competitive landscape',
fields: ['top_competitors', 'competitor_content_strategies', 'market_gaps', 'industry_trends', 'emerging_trends'],
is_complete: false,
is_visible: false,
dependencies: ['audience_intelligence']
},
{
id: 'content_strategy',
title: 'Content Strategy',
description: 'Define your content approach',
fields: ['preferred_formats', 'content_mix', 'content_frequency', 'optimal_timing', 'quality_metrics', 'editorial_guidelines', 'brand_voice'],
is_complete: false,
is_visible: false,
dependencies: ['competitive_intelligence']
},
{
id: 'performance_analytics',
title: 'Performance & Analytics',
description: 'Set up measurement and optimization',
fields: ['traffic_sources', 'conversion_rates', 'content_roi_targets', 'ab_testing_capabilities'],
is_complete: false,
is_visible: false,
dependencies: ['content_strategy']
}
];
// Store implementation
export const useEnhancedStrategyStore = create<EnhancedStrategyStore>((set, get) => ({
// Initial state
strategies: [],
currentStrategy: null,
aiAnalyses: [],
onboardingIntegrations: [],
// Progressive Disclosure
disclosureSteps: PROGRESSIVE_DISCLOSURE_STEPS,
currentStep: 0,
completedSteps: [],
// Tooltips
tooltips: {},
// Form State
formData: {},
formErrors: {},
autoPopulatedFields: {},
dataSources: {},
// UI State
loading: false,
error: null,
saving: false,
aiGenerating: false,
// Actions
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setSaving: (saving) => set({ saving }),
setAIGenerating: (generating) => set({ aiGenerating: generating }),
// Strategy actions
createEnhancedStrategy: async (strategy) => {
set({ saving: true, error: null });
try {
const newStrategy = await contentPlanningApi.createEnhancedStrategy(strategy);
set((state) => ({
strategies: [...state.strategies, newStrategy],
saving: false,
}));
return newStrategy; // Return the created strategy
} catch (error: any) {
set({ error: error.message || 'Failed to create enhanced strategy', saving: false });
throw error; // Re-throw the error so the calling function can handle it
}
},
updateEnhancedStrategy: async (id, updates) => {
set({ saving: true, error: null });
try {
const updatedStrategy = await contentPlanningApi.updateEnhancedStrategy(id, updates);
set((state) => ({
strategies: state.strategies.map((strategy) =>
strategy.id === id ? updatedStrategy : strategy
),
saving: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to update enhanced strategy', saving: false });
}
},
deleteEnhancedStrategy: async (id) => {
set({ saving: true, error: null });
try {
await contentPlanningApi.deleteEnhancedStrategy(id);
set((state) => ({
strategies: state.strategies.filter((strategy) => strategy.id !== id),
saving: false,
}));
} catch (error: any) {
set({ error: error.message || 'Failed to delete enhanced strategy', saving: false });
}
},
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
// Form actions
updateFormField: (fieldId, value) => {
set((state) => ({
formData: { ...state.formData, [fieldId]: value },
formErrors: { ...state.formErrors, [fieldId]: '' }
}));
},
validateFormField: (fieldId) => {
const field = STRATEGIC_INPUT_FIELDS.find(f => f.id === fieldId);
if (!field) return true;
const value = get().formData[fieldId];
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
set((state) => ({
formErrors: { ...state.formErrors, [fieldId]: `${field.label} is required` }
}));
return false;
}
return true;
},
validateAllFields: () => {
const { formData } = get();
let isValid = true;
const errors: Record<string, string> = {};
STRATEGIC_INPUT_FIELDS.forEach(field => {
const value = formData[field.id];
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
errors[field.id] = `${field.label} is required`;
isValid = false;
}
});
set({ formErrors: errors });
return isValid;
},
resetForm: () => {
set({
formData: {},
formErrors: {},
autoPopulatedFields: {},
dataSources: {},
currentStep: 0,
completedSteps: []
});
},
// Progressive disclosure actions
setCurrentStep: (step) => set({ currentStep: step }),
completeStep: (stepId) => {
set((state) => ({
completedSteps: [...state.completedSteps, stepId],
disclosureSteps: state.disclosureSteps.map(step =>
step.id === stepId ? { ...step, is_complete: true } : step
)
}));
},
canProceedToStep: (stepId) => {
const { disclosureSteps, completedSteps } = get();
const step = disclosureSteps.find(s => s.id === stepId);
if (!step) return false;
return step.dependencies.every(dep => completedSteps.includes(dep));
},
getNextStep: () => {
const { disclosureSteps, currentStep } = get();
const nextStep = disclosureSteps[currentStep + 1];
return nextStep && get().canProceedToStep(nextStep.id) ? nextStep : null;
},
getPreviousStep: () => {
const { disclosureSteps, currentStep } = get();
return currentStep > 0 ? disclosureSteps[currentStep - 1] : null;
},
// Auto-population actions
autoPopulateFromOnboarding: async () => {
set({ loading: true });
try {
console.log('🔄 Starting auto-population from onboarding data...');
// This would call the backend to get onboarding data and auto-populate fields
const response = await contentPlanningApi.getOnboardingData();
console.log('📡 Backend response:', response);
// Extract field values and sources from the new backend format
const fields = response.data?.fields || {};
const sources = response.data?.sources || {};
console.log('📋 Extracted fields:', fields);
console.log('🔗 Data sources:', sources);
// Transform the fields object to extract values for formData
const fieldValues: Record<string, any> = {};
const autoPopulatedFields: Record<string, any> = {};
Object.keys(fields).forEach(fieldId => {
const fieldData = fields[fieldId];
console.log(`🔍 Processing field ${fieldId}:`, fieldData);
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
fieldValues[fieldId] = fieldData.value;
autoPopulatedFields[fieldId] = fieldData.value;
console.log(`✅ Auto-populated ${fieldId}:`, fieldData.value);
} else {
console.log(`❌ Skipping ${fieldId} - invalid data structure`);
}
});
console.log('📝 Final field values:', fieldValues);
console.log('🔄 Final auto-populated fields:', autoPopulatedFields);
set((state) => ({
autoPopulatedFields,
dataSources: sources,
formData: { ...state.formData, ...fieldValues }
}));
console.log('✅ Auto-population completed successfully');
} catch (error: any) {
console.error('❌ Auto-population error:', error);
set({ error: error.message || 'Failed to auto-populate from onboarding' });
} finally {
set({ loading: false });
}
},
updateAutoPopulatedField: (fieldId, value, source) => {
set((state) => ({
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value },
dataSources: { ...state.dataSources, [fieldId]: source }
}));
},
overrideAutoPopulatedField: (fieldId, value) => {
set((state) => ({
formData: { ...state.formData, [fieldId]: value },
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value }
}));
},
// AI Analysis actions
generateAIRecommendations: async (strategyId) => {
set({ aiGenerating: true, error: null });
try {
const aiAnalysis = await contentPlanningApi.generateEnhancedAIRecommendations(strategyId);
set((state) => ({
aiAnalyses: [...state.aiAnalyses, aiAnalysis],
aiGenerating: false
}));
} catch (error: any) {
set({ error: error.message || 'Failed to generate AI recommendations', aiGenerating: false });
}
},
regenerateAIAnalysis: async (strategyId: string, analysisType: string) => {
set({ aiGenerating: true, error: null });
try {
const aiAnalysis = await contentPlanningApi.regenerateAIAnalysis(strategyId, analysisType);
set((state) => ({
aiAnalyses: state.aiAnalyses.map(analysis =>
analysis.strategy_id === strategyId && analysis.analysis_type === analysisType
? { ...analysis, ...aiAnalysis }
: analysis
),
aiGenerating: false
}));
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to regenerate AI analysis',
aiGenerating: false
});
}
},
// Data loading
loadEnhancedStrategies: async () => {
set({ loading: true, error: null });
try {
const strategies = await contentPlanningApi.getEnhancedStrategies();
set({ strategies, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load enhanced strategies', loading: false });
}
},
loadAIAnalyses: async (strategyId) => {
set({ loading: true, error: null });
try {
const analyses = await contentPlanningApi.getEnhancedAIAnalyses(strategyId);
set({ aiAnalyses: analyses, loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load AI analyses', loading: false });
}
},
loadOnboardingIntegration: async (strategyId) => {
set({ loading: true, error: null });
try {
const integration = await contentPlanningApi.getOnboardingIntegration(strategyId);
set({ onboardingIntegrations: [integration], loading: false });
} catch (error: any) {
set({ error: error.message || 'Failed to load onboarding integration', loading: false });
}
},
// Tooltip actions
getTooltipData: (fieldId) => {
const field = STRATEGIC_INPUT_FIELDS.find(f => f.id === fieldId);
if (!field) return null;
const state = get();
const autoPopulatedFields = state.autoPopulatedFields || {};
const dataSources = state.dataSources || {};
return {
field_id: fieldId,
title: field.label,
description: field.tooltip,
examples: [],
best_practices: [],
data_source: dataSources[fieldId],
confidence_level: autoPopulatedFields[fieldId] ? 0.8 : undefined
};
},
updateTooltipData: (fieldId, data) => {
set((state) => ({
tooltips: { ...state.tooltips, [fieldId]: data }
}));
},
// Completion tracking
calculateCompletionPercentage: () => {
const { formData } = get();
const requiredFields = STRATEGIC_INPUT_FIELDS.filter(field => field.required);
const filledRequiredFields = requiredFields.filter(field =>
formData[field.id] &&
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
);
return (filledRequiredFields.length / requiredFields.length) * 100;
},
getCompletionStats: () => {
const { formData } = get();
const categories = ['business_context', 'audience_intelligence', 'competitive_intelligence', 'content_strategy', 'performance_analytics'];
const category_completion: Record<string, number> = {};
categories.forEach(category => {
const categoryFields = STRATEGIC_INPUT_FIELDS.filter(field => field.category === category);
const filledFields = categoryFields.filter(field =>
formData[field.id] &&
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
);
category_completion[category] = (filledFields.length / categoryFields.length) * 100;
});
const total_fields = STRATEGIC_INPUT_FIELDS.length;
const filled_fields = STRATEGIC_INPUT_FIELDS.filter(field =>
formData[field.id] &&
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
).length;
return {
total_fields,
filled_fields,
completion_percentage: (filled_fields / total_fields) * 100,
category_completion
};
}
}));

View File

@@ -0,0 +1,9 @@
// Export all stores
export { useDashboardStore } from './dashboardStore';
export { useSEODashboardStore } from './seoDashboardStore';
export { useSharedDashboardStore } from './sharedDashboardStore';
// Re-export types for convenience
export type { DashboardStore } from './dashboardStore';
export type { SEODashboardStore } from './seoDashboardStore';
export type { SharedDashboardState } from './sharedDashboardStore';

View File

@@ -0,0 +1,174 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { SEODashboardData } from '../api/seoDashboard';
import { SEOAnalysisData } from '../components/shared/types';
import { seoAnalysisAPI } from '../api/seoAnalysis';
export interface SEODashboardStore {
// State
data: SEODashboardData | null;
loading: boolean;
error: string | null;
analysisData: SEOAnalysisData | null;
analysisLoading: boolean;
analysisError: string | null;
hasRunInitialAnalysis: boolean;
// Actions
setData: (data: SEODashboardData) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setAnalysisData: (data: SEOAnalysisData | null) => void;
setAnalysisLoading: (loading: boolean) => void;
setAnalysisError: (error: string | null) => void;
runSEOAnalysis: () => Promise<void>;
clearAnalysisError: () => void;
checkAndRunInitialAnalysis: () => void;
}
export const useSEODashboardStore = create<SEODashboardStore>()(
devtools(
(set, get) => ({
// Initial state
data: null,
loading: false,
error: null,
analysisData: null,
analysisLoading: false,
analysisError: null,
hasRunInitialAnalysis: false,
// Actions
setData: (data) => set({ data }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setAnalysisData: (data) => set({ analysisData: data }),
setAnalysisLoading: (loading) => set({ analysisLoading: loading }),
setAnalysisError: (error) => set({ analysisError: error }),
clearAnalysisError: () => set({ analysisError: null }),
runSEOAnalysis: async () => {
const currentData = get().data;
// Get URL from onboarding data or use a fallback
let url = currentData?.website_url;
// If no URL from dashboard data, try to fetch from onboarding
if (!url) {
try {
// Import the user data API to get user's website URL
const { userDataAPI } = await import('../api/userData');
const userData = await userDataAPI.getUserData();
url = userData?.website_url || userData?.website_analysis?.website_url;
console.log('Fetched URL from user data:', url);
} catch (error) {
console.warn('Could not fetch URL from user data:', error);
}
}
// If still no URL, try the dedicated website URL endpoint
if (!url) {
try {
const { userDataAPI } = await import('../api/userData');
const websiteUrl = await userDataAPI.getWebsiteURL();
if (websiteUrl) {
url = websiteUrl;
console.log('Fetched URL from dedicated endpoint:', url);
}
} catch (error) {
console.warn('Could not fetch URL from dedicated endpoint:', error);
}
}
// Final fallback - only use if no URL was found from database
if (!url) {
url = 'https://example.com';
console.warn('Using fallback URL:', url);
}
console.log('Starting SEO analysis with URL:', url);
console.log('Current store state:', get());
set({ analysisLoading: true, analysisError: null });
try {
console.log(`Starting SEO analysis for URL: ${url}`);
const result = await seoAnalysisAPI.analyzeURL(url);
console.log('API result received:', result);
if (result) {
console.log('SEO analysis completed successfully:', result);
set({
analysisData: result,
analysisLoading: false,
hasRunInitialAnalysis: true
});
console.log('Store state after setting analysis data:', get());
// Update main dashboard data based on analysis
if (currentData) {
const updatedData = {
...currentData,
health_score: {
score: result.overall_score,
change: 0,
trend: 'stable',
label: result.health_status.replace('_', ' ').toUpperCase(),
color: result.health_status === 'poor' ? '#D32F2F' :
result.health_status === 'needs_improvement' ? '#FF9800' : '#4CAF50'
},
key_insight: result.critical_issues.length > 0
? `${result.critical_issues.length} critical issues found`
: 'SEO analysis completed successfully',
priority_alert: result.health_status === 'poor'
? 'Immediate attention required'
: result.health_status === 'needs_improvement'
? 'Improvements recommended'
: 'Good SEO health',
website_url: url // Update the website URL with the actual URL used
};
set({ data: updatedData });
}
} else {
console.error('Analysis returned null result');
set({
analysisError: 'Analysis failed to return results',
analysisLoading: false
});
}
} catch (error: any) {
console.error('SEO Analysis error:', error);
let errorMessage = 'Analysis failed';
if (error.code === 'ECONNABORTED') {
errorMessage = 'Analysis timed out. Please try again.';
} else if (error.response?.status === 500) {
errorMessage = 'Server error. Please try again later.';
} else if (error.response?.status === 404) {
errorMessage = 'Analysis service not found.';
} else if (error.message) {
errorMessage = error.message;
}
set({
analysisError: errorMessage,
analysisLoading: false
});
}
},
checkAndRunInitialAnalysis: () => {
const { analysisData, hasRunInitialAnalysis, data } = get();
if (!analysisData && !hasRunInitialAnalysis && data) {
get().runSEOAnalysis();
}
}
}),
{
name: 'seo-dashboard-store',
}
)
);

View File

@@ -0,0 +1,100 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export interface SharedDashboardState {
// Common state
isSidebarOpen: boolean;
currentTheme: 'light' | 'dark' | 'auto';
notifications: Array<{
id: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error';
timestamp: Date;
read: boolean;
}>;
// Actions
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setTheme: (theme: 'light' | 'dark' | 'auto') => void;
addNotification: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void;
markNotificationAsRead: (id: string) => void;
clearNotifications: () => void;
clearOldNotifications: () => void;
}
export const useSharedDashboardStore = create<SharedDashboardState>()(
devtools(
(set, get) => ({
// Initial state
isSidebarOpen: false,
currentTheme: 'auto',
notifications: [],
// Actions
toggleSidebar: () => {
set((state) => ({ isSidebarOpen: !state.isSidebarOpen }));
},
setSidebarOpen: (open: boolean) => {
set({ isSidebarOpen: open });
},
setTheme: (theme: 'light' | 'dark' | 'auto') => {
set({ currentTheme: theme });
// Apply theme to document
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
// Auto theme - check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
},
addNotification: (message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
const notification = {
id: Date.now().toString(),
message,
type,
timestamp: new Date(),
read: false,
};
set((state) => ({
notifications: [notification, ...state.notifications].slice(0, 10), // Keep only last 10
}));
},
markNotificationAsRead: (id: string) => {
set((state) => ({
notifications: state.notifications.map((notification) =>
notification.id === id ? { ...notification, read: true } : notification
),
}));
},
clearNotifications: () => {
set({ notifications: [] });
},
clearOldNotifications: () => {
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
set((state) => ({
notifications: state.notifications.filter(
(notification) => notification.timestamp > oneDayAgo
),
}));
},
}),
{
name: 'shared-dashboard-store',
}
)
);

View File

@@ -0,0 +1,376 @@
/* Global Styles for Alwrity Onboarding */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Better focus styles */
*:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* Enhanced custom animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes shimmer {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
@keyframes glow {
0%, 100% {
box-shadow: 0 0 5px rgba(102, 126, 234, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.8);
}
}
/* Enhanced typography */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
font-weight: 400;
}
/* Better button interactions */
button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
button:active {
transform: scale(0.98);
}
/* Enhanced card shadows and effects */
.MuiCard-root {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.MuiCard-root:hover {
transform: translateY(-2px);
}
/* Glassmorphism effects */
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
}
.glass-effect-hover:hover {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
/* Better form field styling */
.MuiTextField-root .MuiOutlinedInput-root {
transition: all 0.3s ease;
}
.MuiTextField-root .MuiOutlinedInput-root:hover {
transform: translateY(-1px);
}
/* Enhanced progress bars */
.MuiLinearProgress-root {
border-radius: 4px;
overflow: hidden;
}
.MuiLinearProgress-bar {
transition: transform 0.3s ease;
}
/* Better chip styling */
.MuiChip-root {
font-weight: 600;
transition: all 0.3s ease;
}
.MuiChip-root:hover {
transform: translateY(-1px);
}
/* Enhanced alert styling */
.MuiAlert-root {
border-radius: 8px;
font-weight: 500;
}
/* Better tooltip styling */
.MuiTooltip-tooltip {
border-radius: 6px;
font-weight: 500;
}
/* Enhanced stepper styling */
.MuiStepLabel-root {
transition: all 0.3s ease;
}
.MuiStepLabel-root:hover {
transform: scale(1.05);
}
/* Better icon button styling */
.MuiIconButton-root {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.MuiIconButton-root:hover {
transform: scale(1.1);
}
/* Enhanced paper styling */
.MuiPaper-root {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Better link styling */
a {
text-decoration: none;
transition: all 0.3s ease;
}
a:hover {
text-decoration: underline;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Loading animation */
.loading-shimmer {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200px 100%;
animation: shimmer 1.5s infinite;
}
/* Focus ring for accessibility */
.focus-ring {
position: relative;
}
.focus-ring::after {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 2px solid transparent;
border-radius: inherit;
transition: border-color 0.3s ease;
}
.focus-ring:focus::after {
border-color: #667eea;
}
/* Enhanced gradient backgrounds */
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-bg-secondary {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
}
.gradient-bg-premium {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
}
/* Professional shadows */
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.shadow-md {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.shadow-xl {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.shadow-2xl {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* Glassmorphism shadows */
.glass-shadow {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.glass-shadow-hover:hover {
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
}
/* Animation classes */
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-glow {
animation: glow 2s ease-in-out infinite;
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out;
}
.animate-slide-up {
animation: slideUp 0.6s ease-out;
}
/* Hover effects */
.hover-lift:hover {
transform: translateY(-4px);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-scale:hover {
transform: scale(1.05);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-glow:hover {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
transition: box-shadow 0.3s ease;
}
/* Text effects */
.text-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.text-shadow {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Responsive utilities */
@media (max-width: 768px) {
.mobile-hidden {
display: none;
}
.mobile-full {
width: 100% !important;
}
.mobile-text-center {
text-align: center;
}
}
@media (max-width: 480px) {
.mobile-padding {
padding: 16px;
}
}
/* Print styles */
@media print {
.no-print {
display: none !important;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.auto-dark {
background: #1a1a1a;
color: #ffffff;
}
}
/* High contrast mode */
@media (prefers-contrast: high) {
.high-contrast {
border: 2px solid;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -0,0 +1,40 @@
// Simple test script to verify AI integration
const testAIIntegration = async () => {
try {
console.log('Testing AI Integration...');
// Test the AI analytics endpoint
const response = await fetch('http://localhost:8000/api/content-planning/ai-analytics/');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('✅ AI Integration Test Successful!');
console.log('Response:', data);
// Verify the response structure
if (data.insights && data.recommendations) {
console.log('✅ Response structure is correct');
console.log(`📊 Found ${data.insights.length} insights`);
console.log(`💡 Found ${data.recommendations.length} recommendations`);
} else {
console.log('⚠️ Response structure is missing expected fields');
}
} catch (error) {
console.error('❌ AI Integration Test Failed:', error.message);
}
};
// Run the test if this script is executed directly
if (typeof window === 'undefined') {
// Node.js environment
const fetch = require('node-fetch');
testAIIntegration();
} else {
// Browser environment
window.testAIIntegration = testAIIntegration;
console.log('AI Integration test function available as window.testAIIntegration()');
}