Added documentation for the auto-population feature and the analytics integration.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
@@ -13,7 +12,9 @@ import {
|
||||
Alert,
|
||||
Drawer,
|
||||
Button,
|
||||
Badge
|
||||
Badge,
|
||||
ThemeProvider,
|
||||
createTheme
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Psychology as StrategyIcon,
|
||||
@@ -42,6 +43,189 @@ import { StrategyCalendarProvider } from '../../contexts/StrategyCalendarContext
|
||||
|
||||
// CopilotKit actions will be initialized in a separate component
|
||||
|
||||
// Scoped light theme for Content Planning - matches ENHANCED_STYLES
|
||||
const contentPlanningTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'light', // Light theme for content-planning
|
||||
primary: {
|
||||
main: '#667eea', // Matches ENHANCED_STYLES gradient start
|
||||
light: '#a78bfa',
|
||||
dark: '#4f46e5',
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
secondary: {
|
||||
main: '#764ba2', // Matches ENHANCED_STYLES gradient end
|
||||
light: '#a78bfa',
|
||||
dark: '#5a3d7f',
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
background: {
|
||||
default: '#f5f7fa', // Light background (matches common light theme)
|
||||
paper: '#ffffff', // White cards (matches ENHANCED_STYLES.card)
|
||||
},
|
||||
text: {
|
||||
primary: '#2c3e50', // Dark text for headers (matches ENHANCED_STYLES.sectionHeader)
|
||||
secondary: '#555', // Medium gray for secondary text (matches ENHANCED_STYLES.formControl)
|
||||
},
|
||||
divider: 'rgba(0, 0, 0, 0.1)', // Light divider (matches ENHANCED_STYLES.card.border)
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
h4: {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.025em',
|
||||
color: '#2c3e50',
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.025em',
|
||||
color: '#2c3e50',
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.025em',
|
||||
color: '#2c3e50',
|
||||
},
|
||||
body1: {
|
||||
lineHeight: 1.6,
|
||||
color: '#333',
|
||||
},
|
||||
body2: {
|
||||
lineHeight: 1.6,
|
||||
color: '#555',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8, // Matches ENHANCED_STYLES.card.borderRadius
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
borderRadius: 8,
|
||||
padding: '10px 24px',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||
color: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiInputLabel-root': {
|
||||
color: '#555',
|
||||
fontWeight: 500,
|
||||
'&.Mui-focused': {
|
||||
color: '#667eea',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 8,
|
||||
color: '#333',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(102, 126, 234, 0.5)',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#667eea',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFormControl: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiInputLabel-root': {
|
||||
color: '#555',
|
||||
fontWeight: 500,
|
||||
'&.Mui-focused': {
|
||||
color: '#667eea',
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: '#333',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(102, 126, 234, 0.5)',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#667eea',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: '#555',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#2c3e50',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTab-root': {
|
||||
color: '#555',
|
||||
'&.Mui-selected': {
|
||||
color: '#667eea',
|
||||
},
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
backgroundColor: '#667eea',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: '#b0b0b0',
|
||||
'&.Mui-checked': {
|
||||
color: '#667eea',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
@@ -172,8 +356,9 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
|
||||
|
||||
return (
|
||||
<StrategyCalendarProvider>
|
||||
<Container maxWidth={false} sx={{ height: '100vh', p: 0 }}>
|
||||
<ThemeProvider theme={contentPlanningTheme}>
|
||||
<StrategyCalendarProvider>
|
||||
<Container maxWidth={false} sx={{ height: '100vh', p: 0, bgcolor: 'background.default' }}>
|
||||
<AppBar position="static" color="default" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
@@ -199,7 +384,7 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.dark',
|
||||
backgroundColor: 'primary.50'
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.08)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -300,8 +485,9 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
</Box>
|
||||
<AIInsightsPanel />
|
||||
</Drawer>
|
||||
</Container>
|
||||
</StrategyCalendarProvider>
|
||||
</Container>
|
||||
</StrategyCalendarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Alert,
|
||||
IconButton,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Speed as SpeedIcon,
|
||||
Insights as InsightsIcon,
|
||||
Security as SecurityIcon,
|
||||
Refresh as RefreshIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface AutoPopulationConsentModalProps {
|
||||
open: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const AutoPopulationConsentModal: React.FC<AutoPopulationConsentModalProps> = ({
|
||||
open,
|
||||
onConfirm,
|
||||
onCancel
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onCancel}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 3,
|
||||
background: 'linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
py: 2.5
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<AutoAwesomeIcon sx={{ fontSize: 32 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
Auto-Populate Strategy Fields
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={onCancel}
|
||||
sx={{ color: 'white', '&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' } }}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 4 }}>
|
||||
<Alert severity="info" sx={{ mb: 3, backgroundColor: 'rgba(102, 126, 234, 0.1)' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
<strong>Save Time:</strong> We can automatically fill in 30 strategy fields using your onboarding data and AI insights.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, color: '#2c3e50', mb: 2 }}>
|
||||
What is Auto-Population?
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph sx={{ color: '#555', mb: 3 }}>
|
||||
Auto-population uses your existing onboarding information (website analysis, research preferences, and business details)
|
||||
combined with AI to intelligently pre-fill all 30 strategy input fields. This saves you time while ensuring your strategy
|
||||
is tailored to your business.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, color: '#2c3e50', mb: 2 }}>
|
||||
What You Get
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<SpeedIcon sx={{ color: '#667eea', mr: 1 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Instant Setup
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
All 30 fields pre-filled in seconds, ready for your review
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<InsightsIcon sx={{ color: '#667eea', mr: 1 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
AI-Powered Insights
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Smart recommendations based on your business profile and industry
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<SecurityIcon sx={{ color: '#667eea', mr: 1 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Your Data, Your Control
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
You can review and edit every field before creating your strategy
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ height: '100%', border: '1px solid rgba(102, 126, 234, 0.2)' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<RefreshIcon sx={{ color: '#667eea', mr: 1 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Always Editable
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Change any field at any time or fill them manually if you prefer
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600, color: '#2c3e50', mb: 2 }}>
|
||||
What Data We Use
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Website Analysis"
|
||||
secondary="Your website URL, content style, and performance metrics"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Research Preferences"
|
||||
secondary="Your content types, target audience, and research depth"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Business Details"
|
||||
secondary="Your business size, budget, team size, and timeline"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="AI Analysis"
|
||||
secondary="Smart insights generated from your data using AI"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 3, backgroundColor: 'rgba(255, 152, 0, 0.1)' }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Note:</strong> Auto-population makes API calls to generate AI-powered field values.
|
||||
You can skip this step and fill the fields manually if you prefer.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 3, gap: 2, backgroundColor: 'rgba(255, 255, 255, 0.9)' }}>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#764ba2',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Skip Auto-Population
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #764ba2 0%, #667eea 100%)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Auto-Populate Fields
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoPopulationConsentModal;
|
||||
@@ -26,7 +26,7 @@ import EnterpriseDatapointsModal from './EnterpriseDatapointsModal';
|
||||
// Import extracted hooks
|
||||
import { useCategoryReview } from './ContentStrategyBuilder/hooks/useCategoryReview';
|
||||
import { useProgressTracking } from './ContentStrategyBuilder/hooks/useProgressTracking';
|
||||
import { useAutoPopulation } from './ContentStrategyBuilder/hooks/useAutoPopulation';
|
||||
// import { useAutoPopulation } from './ContentStrategyBuilder/hooks/useAutoPopulation'; // Removed - now handled by consent modal
|
||||
import { useModalManagement } from './ContentStrategyBuilder/hooks/useModalManagement';
|
||||
import { useAIRefresh } from './ContentStrategyBuilder/hooks/useAIRefresh';
|
||||
import { useEventHandlers } from './ContentStrategyBuilder/hooks/useEventHandlers';
|
||||
@@ -75,6 +75,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
validateFormField,
|
||||
validateAllFields,
|
||||
autoPopulateFromOnboarding,
|
||||
smartAutofill,
|
||||
createStrategy: createEnhancedStrategy,
|
||||
calculateCompletionPercentage,
|
||||
getCompletionStats,
|
||||
@@ -140,38 +141,38 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
handleShowEducationalInfo
|
||||
} = useEventHandlers();
|
||||
|
||||
// Provide context to CopilotKit for intelligent assistance
|
||||
console.log("🚀 Initializing CopilotKit context provision...");
|
||||
|
||||
// Provide form state context
|
||||
useCopilotReadable({
|
||||
description: "Current strategy form state and field data. This shows the current state of the 30+ strategy form fields.",
|
||||
value: {
|
||||
// Memoize form state context to prevent re-renders
|
||||
const formStateContext = useMemo(() => {
|
||||
const filledFields = Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
});
|
||||
const emptyFields = Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return !value || typeof value !== 'string' || value.trim() === '';
|
||||
});
|
||||
return {
|
||||
formData,
|
||||
completionPercentage: calculateCompletionPercentage(),
|
||||
filledFields: Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
}),
|
||||
emptyFields: Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return !value || typeof value !== 'string' || value.trim() === '';
|
||||
}),
|
||||
filledFields,
|
||||
emptyFields,
|
||||
categoryProgress: getCompletionStats().category_completion,
|
||||
activeCategory,
|
||||
formErrors,
|
||||
totalFields: 30,
|
||||
filledCount: Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
}).length
|
||||
}
|
||||
filledCount: filledFields.length
|
||||
};
|
||||
}, [formData, activeCategory, formErrors, calculateCompletionPercentage, getCompletionStats]);
|
||||
|
||||
// Provide form state context
|
||||
useCopilotReadable({
|
||||
description: "Current strategy form state and field data. This shows the current state of the 30+ strategy form fields.",
|
||||
value: formStateContext
|
||||
});
|
||||
|
||||
// Provide field definitions context
|
||||
useCopilotReadable({
|
||||
description: "Strategy field definitions and requirements. This contains all 30+ form fields with their descriptions, requirements, and categories.",
|
||||
value: STRATEGIC_INPUT_FIELDS.map(field => ({
|
||||
// Memoize field definitions context to prevent re-renders
|
||||
const fieldDefinitionsContext = useMemo(() => {
|
||||
return STRATEGIC_INPUT_FIELDS.map(field => ({
|
||||
id: field.id,
|
||||
label: field.label,
|
||||
description: field.description,
|
||||
@@ -181,38 +182,52 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
options: field.options,
|
||||
category: field.category,
|
||||
currentValue: formData[field.id] || null
|
||||
}))
|
||||
}));
|
||||
}, [formData]);
|
||||
|
||||
// Provide field definitions context
|
||||
useCopilotReadable({
|
||||
description: "Strategy field definitions and requirements. This contains all 30+ form fields with their descriptions, requirements, and categories.",
|
||||
value: fieldDefinitionsContext
|
||||
});
|
||||
|
||||
// Provide onboarding data context
|
||||
useCopilotReadable({
|
||||
description: "User onboarding data for personalization. This contains the user's website analysis, research preferences, and profile information.",
|
||||
value: {
|
||||
// Memoize onboarding data context to prevent re-renders
|
||||
const onboardingDataContext = useMemo(() => {
|
||||
return {
|
||||
websiteAnalysis: personalizationData?.website_analysis,
|
||||
researchPreferences: personalizationData?.research_preferences,
|
||||
apiKeys: personalizationData?.api_keys,
|
||||
userProfile: personalizationData?.user_profile,
|
||||
hasOnboardingData: !!personalizationData
|
||||
}
|
||||
};
|
||||
}, [personalizationData]);
|
||||
|
||||
// Provide onboarding data context
|
||||
useCopilotReadable({
|
||||
description: "User onboarding data for personalization. This contains the user's website analysis, research preferences, and profile information.",
|
||||
value: onboardingDataContext
|
||||
});
|
||||
|
||||
// Provide dynamic instructions
|
||||
useCopilotAdditionalInstructions({
|
||||
instructions: `
|
||||
// Memoize instructions to prevent re-renders
|
||||
const completionPercentage = calculateCompletionPercentage();
|
||||
const filledCount = Object.keys(formData).filter(k => {
|
||||
const value = formData[k];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
}).length;
|
||||
const emptyCount = Object.keys(formData).filter(k => {
|
||||
const value = formData[k];
|
||||
return !value || typeof value !== 'string' || value.trim() === '';
|
||||
}).length;
|
||||
|
||||
const copilotInstructions = useMemo(() => `
|
||||
You are ALwrity's Strategy Assistant, helping users create comprehensive content strategies.
|
||||
|
||||
IMPORTANT CONTEXT:
|
||||
- You are working with a form that has 30+ strategy fields
|
||||
- Current form completion: ${calculateCompletionPercentage()}%
|
||||
- Current form completion: ${completionPercentage}%
|
||||
- Active category: ${activeCategory}
|
||||
- Filled fields: ${Object.keys(formData).filter(k => {
|
||||
const value = formData[k];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
}).length}/30
|
||||
- Empty fields: ${Object.keys(formData).filter(k => {
|
||||
const value = formData[k];
|
||||
return !value || typeof value !== 'string' || value.trim() === '';
|
||||
}).length}/30
|
||||
- Filled fields: ${filledCount}/30
|
||||
- Empty fields: ${emptyCount}/30
|
||||
|
||||
AVAILABLE ACTIONS:
|
||||
- testAction: Test if actions are working
|
||||
@@ -240,10 +255,12 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
- Be specific about which fields you're referring to
|
||||
- When users click suggestions, immediately execute the requested action
|
||||
- Provide clear feedback on what you're doing and why
|
||||
`
|
||||
});
|
||||
`, [completionPercentage, activeCategory, filledCount, emptyCount]);
|
||||
|
||||
console.log("✅ CopilotKit context provision initialized successfully");
|
||||
// Provide dynamic instructions
|
||||
useCopilotAdditionalInstructions({
|
||||
instructions: copilotInstructions
|
||||
});
|
||||
|
||||
// Create a state for educational modal that can be passed to both hooks
|
||||
const [showEducationalModal, setShowEducationalModal] = useState(false);
|
||||
@@ -334,15 +351,18 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
totalCategories,
|
||||
reviewedCategoriesCount,
|
||||
reviewProgressPercentage,
|
||||
getCategoryProgress,
|
||||
getCategoryStatus: getCategoryStatusFromHook,
|
||||
// getCategoryProgress, // Unused - commented out to fix linting error
|
||||
// getCategoryStatus: getCategoryStatusFromHook, // Unused - commented out to fix linting error
|
||||
isNextInSequence
|
||||
} = useProgressTracking({ completionStats, reviewedCategories });
|
||||
|
||||
const { autoPopulateAttempted, setAutoPopulateAttempted } = useAutoPopulation({
|
||||
autoPopulateFromOnboarding,
|
||||
completionStats
|
||||
});
|
||||
// Remove automatic auto-population hook - now handled by consent modal
|
||||
// const { autoPopulateAttempted, setAutoPopulateAttempted } = useAutoPopulation({
|
||||
// autoPopulateFromOnboarding,
|
||||
// completionStats
|
||||
// });
|
||||
|
||||
// Removed: Auto-population consent state (replaced with buttons in HeaderSection)
|
||||
|
||||
// Add ref for scroll to review section
|
||||
const reviewSectionRef = useRef<HTMLDivElement>(null);
|
||||
@@ -372,19 +392,28 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
// Get data source from store
|
||||
const dataSource = Object.keys(dataSources).length > 0 ? 'Onboarding Database' : undefined;
|
||||
|
||||
// Log autofill data status for debugging
|
||||
// Log autofill data status for debugging (only log when values actually change)
|
||||
const autoPopulatedFieldsCount = Object.keys(autoPopulatedFields).length;
|
||||
const dataSourcesCount = Object.keys(dataSources).length;
|
||||
const inputDataPointsCount = Object.keys(inputDataPoints).length;
|
||||
const personalizationDataCount = Object.keys(personalizationData || {}).length;
|
||||
const confidenceScoresCount = Object.keys(confidenceScores).length;
|
||||
|
||||
useEffect(() => {
|
||||
console.log('📋 StrategyBuilder: Autofill data status:', {
|
||||
hasAutofillData,
|
||||
autoPopulatedFieldsCount: Object.keys(autoPopulatedFields).length,
|
||||
dataSourcesCount: Object.keys(dataSources).length,
|
||||
inputDataPointsCount: Object.keys(inputDataPoints).length,
|
||||
personalizationDataCount: Object.keys(personalizationData).length,
|
||||
confidenceScoresCount: Object.keys(confidenceScores).length,
|
||||
lastAutofillTime,
|
||||
dataSource
|
||||
});
|
||||
}, [hasAutofillData, autoPopulatedFields, dataSources, inputDataPoints, personalizationData, confidenceScores, lastAutofillTime, dataSource]);
|
||||
// Only log in development and when there's meaningful data change
|
||||
if (process.env.NODE_ENV === 'development' && (autoPopulatedFieldsCount > 0 || dataSourcesCount > 0)) {
|
||||
console.log('📋 StrategyBuilder: Autofill data status:', {
|
||||
hasAutofillData,
|
||||
autoPopulatedFieldsCount,
|
||||
dataSourcesCount,
|
||||
inputDataPointsCount,
|
||||
personalizationDataCount,
|
||||
confidenceScoresCount,
|
||||
lastAutofillTime,
|
||||
dataSource
|
||||
});
|
||||
}
|
||||
}, [hasAutofillData, autoPopulatedFieldsCount, dataSourcesCount, inputDataPointsCount, personalizationDataCount, confidenceScoresCount, lastAutofillTime, dataSource]);
|
||||
|
||||
|
||||
|
||||
@@ -430,12 +459,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
|
||||
|
||||
|
||||
// Auto-populate from onboarding on first load
|
||||
useEffect(() => {
|
||||
if (!autoPopulateAttempted) {
|
||||
autoPopulateFromOnboarding();
|
||||
}
|
||||
}, [autoPopulateAttempted]); // Removed autoPopulateFromOnboarding from dependencies
|
||||
// Removed: Auto-population consent modal (replaced with buttons in HeaderSection)
|
||||
|
||||
// Set default category selection
|
||||
useEffect(() => {
|
||||
@@ -450,7 +474,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
setActiveCategory(firstCategory);
|
||||
hasSetDefaultCategory.current = true;
|
||||
}
|
||||
}, [completionStats.category_completion]); // Removed activeCategory dependency
|
||||
}, [completionStats.category_completion, setActiveCategory]); // Added setActiveCategory dependency
|
||||
|
||||
// Monitor enterprise modal state for debugging
|
||||
useEffect(() => {
|
||||
@@ -477,8 +501,8 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
handleConfirmCategoryReview(activeCategory);
|
||||
};
|
||||
|
||||
// Generate comprehensive suggestions for all 7 CopilotKit actions
|
||||
const getSuggestions = () => {
|
||||
// Memoize suggestions to prevent unnecessary re-renders
|
||||
const suggestions = useMemo(() => {
|
||||
const filledFields = Object.keys(formData).filter(key => {
|
||||
const value = formData[key];
|
||||
return value && typeof value === 'string' && value.trim() !== '';
|
||||
@@ -550,10 +574,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
|
||||
// Return all suggestions (no limit) to show full CopilotKit capabilities
|
||||
return combinedSuggestions;
|
||||
};
|
||||
|
||||
// Memoize suggestions to prevent unnecessary re-renders
|
||||
const suggestions = useMemo(() => getSuggestions(), [formData, activeCategory, calculateCompletionPercentage]);
|
||||
}, [formData, activeCategory, calculateCompletionPercentage]);
|
||||
|
||||
return (
|
||||
<CopilotSidebar
|
||||
@@ -579,6 +600,8 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRefreshAutofill={handleAIRefresh}
|
||||
onDatabaseAutofill={autoPopulateFromOnboarding}
|
||||
onSmartAutofill={smartAutofill}
|
||||
onContinueWithPresent={handleContinueWithPresent}
|
||||
onScrollToReview={handleScrollToReview}
|
||||
hasAutofillData={hasAutofillData}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStrategyBuilderStore } from '../../../../stores/strategyBuilderStore
|
||||
import { useEnhancedStrategyStore } from '../../../../stores/enhancedStrategyStore';
|
||||
|
||||
export const useCopilotActions = () => {
|
||||
console.log("CopilotActions hook initialized");
|
||||
// Hook initialized - actions are available
|
||||
|
||||
// Get store methods for updating form state
|
||||
const {
|
||||
@@ -186,7 +186,7 @@ export const useCopilotActions = () => {
|
||||
setTransparencyGenerating(false);
|
||||
return { success: false, message: error.message || 'Unknown error' };
|
||||
}
|
||||
}, [formData, updateFormField, setError, calculateCompletionPercentage, setTransparencyModalOpen, setTransparencyGenerating, setTransparencyGenerationProgress, setCurrentPhase, clearTransparencyMessages, addTransparencyMessage, setAIGenerating, triggerTransparencyFlow]);
|
||||
}, [formData, updateFormField, setError, calculateCompletionPercentage, setTransparencyModalOpen, setTransparencyGenerating, setTransparencyGenerationProgress, setCurrentPhase, addTransparencyMessage, setAIGenerating, triggerTransparencyFlow]);
|
||||
|
||||
// Action 4: Validate field
|
||||
const validateStrategyField = useCallback(async ({ fieldId }: any) => {
|
||||
@@ -423,7 +423,7 @@ export const useCopilotActions = () => {
|
||||
setTransparencyGenerating(false);
|
||||
return { success: false, message: error.message || 'Unknown error' };
|
||||
}
|
||||
}, [formData, updateFormField, calculateCompletionPercentage, setError, setTransparencyModalOpen, setTransparencyGenerating, setTransparencyGenerationProgress, setCurrentPhase, clearTransparencyMessages, addTransparencyMessage, setAIGenerating, triggerTransparencyFlow]);
|
||||
}, [formData, calculateCompletionPercentage, setError, setTransparencyModalOpen, setTransparencyGenerating, setTransparencyGenerationProgress, setCurrentPhase, addTransparencyMessage, setAIGenerating, triggerTransparencyFlow]);
|
||||
|
||||
// Call useCopilotAction hooks unconditionally - they will handle context availability internally
|
||||
// This is the only way to comply with React hooks rules
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
ExpandLess as ExpandLessIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useStrategyBuilderStore } from '../../../../stores/strategyBuilderStore';
|
||||
import StructuredJsonField from './components/StructuredJsonField';
|
||||
import { JSON_FIELD_SCHEMAS } from './utils/jsonFieldSchemas';
|
||||
|
||||
interface StrategicInputFieldProps {
|
||||
fieldId: string;
|
||||
@@ -574,24 +576,89 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
||||
);
|
||||
|
||||
case 'json':
|
||||
// Check if we have a schema for this field - use structured form
|
||||
const jsonSchema = JSON_FIELD_SCHEMAS[fieldId];
|
||||
if (jsonSchema) {
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<StructuredJsonField
|
||||
fieldId={fieldId}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
schema={jsonSchema}
|
||||
label={config.label || fieldId}
|
||||
error={error}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
||||
<Tooltip title="Get help with this field">
|
||||
<IconButton onClick={onShowTooltip} size="small">
|
||||
<HelpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to raw JSON textarea for fields without schemas
|
||||
const formatJsonValue = (val: any): string => {
|
||||
if (val === null || val === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
if (typeof val === 'object') {
|
||||
if (Array.isArray(val) && val.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (!Array.isArray(val) && Object.keys(val).length === 0) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return JSON.stringify(val, null, 2);
|
||||
};
|
||||
|
||||
const displayValue = formatJsonValue(value);
|
||||
const isEmpty = !displayValue || displayValue.trim() === '' ||
|
||||
displayValue === '{}' || displayValue === '[]';
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
rows={isEmpty ? 2 : 4}
|
||||
label={config.label || fieldId}
|
||||
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
||||
value={displayValue}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
handleChange(parsed);
|
||||
} catch {
|
||||
handleChange(e.target.value);
|
||||
const inputValue = e.target.value.trim();
|
||||
if (!inputValue || inputValue === '' || inputValue === '{}' || inputValue === '[]') {
|
||||
if (fieldId === 'audience_pain_points' || fieldId.includes('trends') || fieldId.includes('competitors')) {
|
||||
handleChange([]);
|
||||
} else {
|
||||
handleChange({});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const parsed = JSON.parse(inputValue);
|
||||
handleChange(parsed);
|
||||
} catch {
|
||||
handleChange(inputValue);
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId} as JSON`}
|
||||
placeholder={
|
||||
isEmpty
|
||||
? (config as TextFieldConfig).placeholder || `Enter ${fieldId.replace(/_/g, ' ')} as JSON`
|
||||
: (config as TextFieldConfig).placeholder || `Enter ${fieldId.replace(/_/g, ' ')} as JSON`
|
||||
}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
helperText={error || (isEmpty ? 'No data available. Please enter values or use autofill.' : '')}
|
||||
required={config.required || false}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
@@ -602,6 +669,13 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiInputBase-input': {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
lineHeight: 1.5
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
DataUsage as DataUsageIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Security as SecurityIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Storage as StorageIcon,
|
||||
SmartToy as SmartToyIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import AutofillDataTransparency from './AutofillDataTransparency';
|
||||
@@ -36,6 +38,8 @@ interface HeaderSectionProps {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onRefreshAutofill: () => void;
|
||||
onDatabaseAutofill: () => void;
|
||||
onSmartAutofill: () => void;
|
||||
onContinueWithPresent: () => void;
|
||||
onScrollToReview: () => void;
|
||||
hasAutofillData: boolean;
|
||||
@@ -52,6 +56,8 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
loading,
|
||||
error,
|
||||
onRefreshAutofill,
|
||||
onDatabaseAutofill,
|
||||
onSmartAutofill,
|
||||
onContinueWithPresent,
|
||||
onScrollToReview,
|
||||
hasAutofillData,
|
||||
@@ -61,6 +67,7 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
const [showTransparencyModal, setShowTransparencyModal] = useState(false);
|
||||
const [showDataInfo, setShowDataInfo] = useState(false);
|
||||
const [showNextButton, setShowNextButton] = useState(false);
|
||||
const [showEducationalInfo, setShowEducationalInfo] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Show next button when autofill is complete
|
||||
useEffect(() => {
|
||||
@@ -172,98 +179,209 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
{/* Auto-populated Fields Count */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<DataUsageIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||
{Object.keys(autoPopulatedFields).length}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||
Fields Auto-populated
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title="Number of strategy fields automatically populated from your onboarding data. These fields are ready to use or can be edited."
|
||||
arrow
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
}
|
||||
}}>
|
||||
<DataUsageIcon sx={{ fontSize: 24, color: 'rgba(102, 126, 234, 0.9)' }} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.2rem', lineHeight: 1.2 }}>
|
||||
{Object.keys(autoPopulatedFields).length}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.9, fontSize: '0.75rem', lineHeight: 1.2 }}>
|
||||
Fields Auto-populated
|
||||
</Typography>
|
||||
</Box>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { color: 'white' }
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowEducationalInfo(prev => ({ ...prev, fieldsCount: !prev.fieldsCount }));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Collapse in={showEducationalInfo.fieldsCount}>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
mt: 1,
|
||||
backgroundColor: 'rgba(33, 150, 243, 0.15)',
|
||||
border: '1px solid rgba(33, 150, 243, 0.3)',
|
||||
color: 'white',
|
||||
'& .MuiAlert-icon': { color: 'rgba(144, 202, 249, 0.9)' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
|
||||
<strong>What are auto-populated fields?</strong><br />
|
||||
These are strategy inputs automatically filled from your onboarding data, including website analysis, research preferences, and API integrations. You can review and edit any field before creating your strategy.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
|
||||
{/* Data Quality Score */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<TrendingUpIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||
{dataQualityScore}%
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||
Data Quality
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title="Overall confidence score based on data completeness and reliability. Higher scores indicate more reliable autofilled data."
|
||||
arrow
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
}
|
||||
}}>
|
||||
<TrendingUpIcon sx={{ fontSize: 24, color: dataQualityScore >= 80 ? 'rgba(76, 175, 80, 0.9)' : dataQualityScore >= 60 ? 'rgba(255, 152, 0, 0.9)' : 'rgba(244, 67, 54, 0.9)' }} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.2rem', lineHeight: 1.2 }}>
|
||||
{dataQualityScore}%
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.9, fontSize: '0.75rem', lineHeight: 1.2 }}>
|
||||
Data Quality
|
||||
</Typography>
|
||||
</Box>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { color: 'white' }
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowEducationalInfo(prev => ({ ...prev, dataQuality: !prev.dataQuality }));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Collapse in={showEducationalInfo.dataQuality}>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
mt: 1,
|
||||
backgroundColor: 'rgba(33, 150, 243, 0.15)',
|
||||
border: '1px solid rgba(33, 150, 243, 0.3)',
|
||||
color: 'white',
|
||||
'& .MuiAlert-icon': { color: 'rgba(144, 202, 249, 0.9)' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
|
||||
<strong>Understanding Data Quality:</strong><br />
|
||||
This score reflects the reliability of your autofilled data. Scores above 80% indicate high-quality data from reliable sources. Scores below 60% suggest you may want to review and manually update some fields for better accuracy.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
|
||||
{/* Last Updated */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<ScheduleIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||
{lastAutofillTime ? formatTimeAgo(lastAutofillTime).split(' ')[0] : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||
Last Updated
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={lastAutofillTime
|
||||
? `Data was last refreshed ${formatTimeAgo(lastAutofillTime)}. Click Database Autofill to refresh with latest onboarding data.`
|
||||
: 'No data has been loaded yet. Click Database Autofill to populate fields from your onboarding data.'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}>
|
||||
<ScheduleIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem', lineHeight: 1.2 }}>
|
||||
{lastAutofillTime ? formatTimeAgo(lastAutofillTime) : 'Never'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem', lineHeight: 1.2 }}>
|
||||
Last Updated
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
{/* Data Sources */}
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<SecurityIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||
{Object.keys(dataSources).length}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||
Data Sources
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={`${Object.keys(dataSources).length} unique data sources were used to populate your strategy fields. These include website analysis, research preferences, and API integrations from your onboarding data.`}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}>
|
||||
<SecurityIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem', lineHeight: 1.2 }}>
|
||||
{Object.keys(dataSources).length}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem', lineHeight: 1.2 }}>
|
||||
Data Sources
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -301,35 +419,57 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
{/* Enhanced Status Chips */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2, flexWrap: 'wrap' }}>
|
||||
{cacheStatus === 'cached' && (
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${Object.keys(autoPopulatedFields).length} fields auto-populated`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||
'& .MuiChip-icon': { color: 'rgba(76, 175, 80, 0.8)' },
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem'
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
title={`${Object.keys(autoPopulatedFields).length} fields have been automatically populated from your onboarding data. These fields are ready to use or can be edited before creating your strategy.`}
|
||||
arrow
|
||||
>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${Object.keys(autoPopulatedFields).length} fields auto-populated`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.25)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(76, 175, 80, 0.4)',
|
||||
'& .MuiChip-icon': { color: 'rgba(129, 199, 132, 0.9)', fontSize: '18px' },
|
||||
fontWeight: 600,
|
||||
fontSize: '0.85rem',
|
||||
height: '32px',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.35)',
|
||||
borderColor: 'rgba(76, 175, 80, 0.5)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(76, 175, 80, 0.3)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{dataSource && (
|
||||
<Tooltip title="Click to view data source information">
|
||||
<Tooltip
|
||||
title={`Data source: ${dataSource}. Click to view detailed information about where your autofilled data comes from.`}
|
||||
arrow
|
||||
>
|
||||
<Chip
|
||||
icon={<InfoIcon />}
|
||||
label={`Source: ${dataSource}`}
|
||||
onClick={() => setShowDataInfo(!showDataInfo)}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.85rem',
|
||||
height: '32px',
|
||||
transition: 'all 0.2s ease',
|
||||
'& .MuiChip-icon': { color: 'rgba(255, 255, 255, 0.9)', fontSize: '18px' },
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)'
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -338,18 +478,31 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
|
||||
{/* Category Distribution Chips */}
|
||||
{Object.keys(fieldCountByCategory).length > 0 && (
|
||||
<Chip
|
||||
icon={<AutoAwesomeIcon />}
|
||||
label={`${Object.keys(fieldCountByCategory).length} categories`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(156, 39, 176, 0.2)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(156, 39, 176, 0.3)',
|
||||
'& .MuiChip-icon': { color: 'rgba(156, 39, 176, 0.8)' },
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8rem'
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
title={`Your autofilled fields are distributed across ${Object.keys(fieldCountByCategory).length} strategic categories: Business Context, Audience Intelligence, Competitive Intelligence, Content Strategy, and Performance & Analytics.`}
|
||||
arrow
|
||||
>
|
||||
<Chip
|
||||
icon={<AutoAwesomeIcon />}
|
||||
label={`${Object.keys(fieldCountByCategory).length} categories`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(156, 39, 176, 0.25)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(156, 39, 176, 0.4)',
|
||||
'& .MuiChip-icon': { color: 'rgba(186, 104, 200, 0.9)', fontSize: '18px' },
|
||||
fontWeight: 600,
|
||||
fontSize: '0.85rem',
|
||||
height: '32px',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(156, 39, 176, 0.35)',
|
||||
borderColor: 'rgba(156, 39, 176, 0.5)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(156, 39, 176, 0.3)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -377,83 +530,150 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{/* Conditional Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{cacheStatus === 'cached' ? (
|
||||
// Case 1: Data exists in cache - show refresh vs continue options
|
||||
<>
|
||||
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Continue with current autofilled values">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={onContinueWithPresent}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue with Present Values
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : cacheStatus === 'partial' ? (
|
||||
// Case 2: Partial data - show refresh option
|
||||
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||
{/* Action Buttons - Smart, Database, and AI Autofill */}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }}>
|
||||
<Tooltip
|
||||
title="Smart Autofill combines the speed of database autofill with AI personalization. It uses your onboarding data for 18-19 fields and AI analysis for 11-12 additional fields, providing the best of both worlds. Recommended for most users."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
onClick={onSmartAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.95)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
px: 3,
|
||||
py: 1.2,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(102, 126, 234, 1)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.5)'
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.5)',
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Processing...' : 'Smart Autofill (Recommended)'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title="Database Autofill quickly populates 18-19 fields directly from your onboarding data (website analysis, research preferences, API integrations). Fast and free - no AI processing required. Best for users who want quick results from existing data."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<StorageIcon />}
|
||||
onClick={onDatabaseAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
borderWidth: 2,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
px: 3,
|
||||
py: 1.2,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(255, 255, 255, 0.2)'
|
||||
},
|
||||
'&:disabled': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Database Autofill'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title="AI Autofill uses advanced AI analysis to generate personalized strategy fields based on your onboarding data. This provides deeper insights and recommendations but takes longer and uses AI credits. Best for users who want AI-powered strategic insights."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<SmartToyIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
borderWidth: 2,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
px: 3,
|
||||
py: 1.2,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(255, 255, 255, 0.2)'
|
||||
},
|
||||
'&:disabled': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Processing...' : 'AI Autofill'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{cacheStatus === 'cached' && (
|
||||
<Tooltip
|
||||
title="Continue editing your strategy with the current autofilled values. You can review and modify any field before creating your strategy."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={onContinueWithPresent}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
px: 3,
|
||||
py: 1.2,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.9)'
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.35)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
// Case 3: No data - show initial autofill
|
||||
<Tooltip title="Fetch latest data from database and autofill strategy inputs">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRefreshAutofill}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.9)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? 'Autofilling...' : 'Refresh & Autofill Strategy Inputs'}
|
||||
Continue with Present Values
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
Chip,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Grid,
|
||||
Divider,
|
||||
Tooltip,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Code as CodeIcon,
|
||||
Edit as EditIcon
|
||||
} from '@mui/icons-material';
|
||||
import { JsonFieldSchema, FieldDefinition } from '../utils/jsonFieldSchemas';
|
||||
|
||||
interface StructuredJsonFieldProps {
|
||||
fieldId: string;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
schema: JsonFieldSchema;
|
||||
label: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const StructuredJsonField: React.FC<StructuredJsonFieldProps> = ({
|
||||
fieldId,
|
||||
value,
|
||||
onChange,
|
||||
schema,
|
||||
label,
|
||||
error
|
||||
}) => {
|
||||
const [showRawJson, setShowRawJson] = useState(false);
|
||||
const [rawJsonValue, setRawJsonValue] = useState('');
|
||||
|
||||
// Initialize value if empty
|
||||
useEffect(() => {
|
||||
if (!value || (schema.type === 'object' && Object.keys(value).length === 0) ||
|
||||
(schema.type === 'array' && Array.isArray(value) && value.length === 0)) {
|
||||
if (schema.type === 'object') {
|
||||
const initialValue: Record<string, any> = {};
|
||||
if (schema.fields) {
|
||||
Object.keys(schema.fields).forEach(key => {
|
||||
const fieldDef = schema.fields![key];
|
||||
if (fieldDef.type === 'multiselect') {
|
||||
initialValue[key] = [];
|
||||
} else if (fieldDef.type === 'number') {
|
||||
initialValue[key] = '';
|
||||
} else {
|
||||
initialValue[key] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
onChange(initialValue);
|
||||
} else {
|
||||
onChange([]);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update raw JSON when value changes
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
try {
|
||||
setRawJsonValue(JSON.stringify(value, null, 2));
|
||||
} catch (e) {
|
||||
setRawJsonValue('');
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleObjectFieldChange = (key: string, newValue: any) => {
|
||||
const updated = { ...value };
|
||||
updated[key] = newValue;
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleArrayItemAdd = () => {
|
||||
if (schema.type === 'array') {
|
||||
if (schema.itemType === 'object' && schema.itemFields) {
|
||||
const newItem: Record<string, any> = {};
|
||||
Object.keys(schema.itemFields).forEach(key => {
|
||||
const fieldDef = schema.itemFields![key];
|
||||
if (fieldDef.type === 'multiselect') {
|
||||
newItem[key] = [];
|
||||
} else if (fieldDef.type === 'number') {
|
||||
newItem[key] = '';
|
||||
} else {
|
||||
newItem[key] = '';
|
||||
}
|
||||
});
|
||||
onChange([...(value || []), newItem]);
|
||||
} else if (schema.itemType === 'string') {
|
||||
onChange([...(value || []), '']);
|
||||
} else {
|
||||
onChange([...(value || []), '']);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleArrayItemChange = (index: number, newValue: any) => {
|
||||
const updated = [...(value || [])];
|
||||
updated[index] = newValue;
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleArrayItemRemove = (index: number) => {
|
||||
const updated = [...(value || [])];
|
||||
updated.splice(index, 1);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleObjectInArrayChange = (index: number, key: string, newValue: any) => {
|
||||
const updated = [...(value || [])];
|
||||
if (!updated[index]) {
|
||||
updated[index] = {};
|
||||
}
|
||||
updated[index] = { ...updated[index], [key]: newValue };
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const renderField = (fieldKey: string, fieldDef: FieldDefinition, fieldValue: any, onChangeHandler: (val: any) => void) => {
|
||||
switch (fieldDef.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={fieldDef.label}
|
||||
value={fieldValue || ''}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
placeholder={fieldDef.placeholder}
|
||||
required={fieldDef.required}
|
||||
helperText={fieldDef.helperText}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
case 'multiline':
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label={fieldDef.label}
|
||||
value={fieldValue || ''}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
placeholder={fieldDef.placeholder}
|
||||
required={fieldDef.required}
|
||||
helperText={fieldDef.helperText}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<FormControl fullWidth size="small" required={fieldDef.required}>
|
||||
<InputLabel>{fieldDef.label}</InputLabel>
|
||||
<Select
|
||||
value={fieldValue || ''}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
label={fieldDef.label}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Select {fieldDef.label}</em>
|
||||
</MenuItem>
|
||||
{fieldDef.options?.map(option => (
|
||||
<MenuItem key={option} value={option}>{option}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{fieldDef.helperText && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
|
||||
{fieldDef.helperText}
|
||||
</Typography>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
case 'multiselect':
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
|
||||
{fieldDef.label} {fieldDef.required && '*'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1 }}>
|
||||
{fieldDef.options?.map(option => {
|
||||
const isSelected = Array.isArray(fieldValue) && fieldValue.includes(option);
|
||||
return (
|
||||
<Chip
|
||||
key={option}
|
||||
label={option}
|
||||
onClick={() => {
|
||||
const current = Array.isArray(fieldValue) ? [...fieldValue] : [];
|
||||
if (isSelected) {
|
||||
onChangeHandler(current.filter(v => v !== option));
|
||||
} else {
|
||||
onChangeHandler([...current, option]);
|
||||
}
|
||||
}}
|
||||
color={isSelected ? 'primary' : 'default'}
|
||||
variant={isSelected ? 'filled' : 'outlined'}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{fieldDef.helperText && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
|
||||
{fieldDef.helperText}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label={fieldDef.label}
|
||||
value={fieldValue || ''}
|
||||
onChange={(e) => onChangeHandler(e.target.value ? Number(e.target.value) : '')}
|
||||
placeholder={fieldDef.placeholder}
|
||||
required={fieldDef.required}
|
||||
helperText={fieldDef.helperText}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={fieldDef.label}
|
||||
value={fieldValue || ''}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
placeholder={fieldDef.placeholder}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderObjectField = () => {
|
||||
if (schema.type !== 'object' || !schema.fields) return null;
|
||||
|
||||
const objValue = value || {};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{schema.fields && Object.entries(schema.fields).map(([key, fieldDef]) => (
|
||||
<Box key={key}>
|
||||
{renderField(key, fieldDef, objValue[key], (newVal) => handleObjectFieldChange(key, newVal))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderArrayField = () => {
|
||||
if (schema.type !== 'array') return null;
|
||||
|
||||
const arrayValue = Array.isArray(value) ? value : [];
|
||||
|
||||
if (schema.itemType === 'object' && schema.itemFields) {
|
||||
// Array of objects
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{arrayValue.map((item, index) => (
|
||||
<Accordion key={index} defaultExpanded={index === arrayValue.length - 1}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', pr: 2 }}>
|
||||
<Typography variant="body2" fontWeight={500}>
|
||||
{schema.itemLabel || 'Item'} {index + 1}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleArrayItemRemove(index);
|
||||
}}
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{schema.itemFields && Object.entries(schema.itemFields).map(([key, fieldDef]) => (
|
||||
<Box key={key}>
|
||||
{renderField(key, fieldDef, item?.[key], (newVal) => handleObjectInArrayChange(index, key, newVal))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleArrayItemAdd}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Add {schema.itemLabel || 'Item'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
// Array of strings
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{arrayValue.map((item, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={item || ''}
|
||||
onChange={(e) => handleArrayItemChange(index, e.target.value)}
|
||||
placeholder={`Enter ${schema.itemLabel || 'item'}`}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => handleArrayItemRemove(index)}
|
||||
size="small"
|
||||
sx={{ color: 'error.main', mt: 0.5 }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleArrayItemAdd}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Add {schema.itemLabel || 'Item'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Header with toggle */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Tooltip title={showRawJson ? "Switch to form view" : "Switch to JSON view"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowRawJson(!showRawJson)}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
{showRawJson ? <EditIcon fontSize="small" /> : <CodeIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{showRawJson ? (
|
||||
// Raw JSON view
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={6}
|
||||
value={rawJsonValue}
|
||||
onChange={(e) => {
|
||||
setRawJsonValue(e.target.value);
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
onChange(parsed);
|
||||
} catch {
|
||||
// Invalid JSON, don't update
|
||||
}
|
||||
}}
|
||||
placeholder="Enter JSON..."
|
||||
error={!!error}
|
||||
helperText={error || "Edit JSON directly"}
|
||||
sx={{
|
||||
'& .MuiInputBase-input': {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// Structured form view
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{schema.type === 'object' && renderObjectField()}
|
||||
{schema.type === 'array' && renderArrayField()}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StructuredJsonField;
|
||||
@@ -6,8 +6,33 @@ interface UseCategoryReviewProps {
|
||||
setActiveCategory: (category: string | null) => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'strategy_reviewed_categories';
|
||||
|
||||
// Helper functions for localStorage persistence
|
||||
const loadReviewedCategories = (): Set<string> => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const categories = JSON.parse(stored);
|
||||
return new Set(Array.isArray(categories) ? categories : []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load reviewed categories from localStorage:', error);
|
||||
}
|
||||
return new Set();
|
||||
};
|
||||
|
||||
const saveReviewedCategories = (categories: Set<string>) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(categories)));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save reviewed categories to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const useCategoryReview = ({ completionStats, setError, setActiveCategory }: UseCategoryReviewProps) => {
|
||||
const [reviewedCategories, setReviewedCategories] = useState<Set<string>>(new Set());
|
||||
// Load reviewed categories from localStorage on mount
|
||||
const [reviewedCategories, setReviewedCategories] = useState<Set<string>>(() => loadReviewedCategories());
|
||||
const [isMarkingReviewed, setIsMarkingReviewed] = useState(false);
|
||||
const [categoryCompletionMessage, setCategoryCompletionMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -32,7 +57,12 @@ export const useCategoryReview = ({ completionStats, setError, setActiveCategory
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Mark category as reviewed
|
||||
setReviewedCategories(prev => new Set([...Array.from(prev), activeCategory]));
|
||||
setReviewedCategories(prev => {
|
||||
const updated = new Set([...Array.from(prev), activeCategory]);
|
||||
// Persist to localStorage
|
||||
saveReviewedCategories(updated);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Get category name for display
|
||||
const categoryName = activeCategory.split('_').map(word =>
|
||||
|
||||
@@ -0,0 +1,719 @@
|
||||
/**
|
||||
* Schemas for rendering JSON fields as user-friendly forms
|
||||
*/
|
||||
|
||||
export interface JsonFieldSchema {
|
||||
type: 'object' | 'array';
|
||||
fields?: Record<string, FieldDefinition>; // For object type
|
||||
itemType?: 'string' | 'object'; // For array type
|
||||
itemFields?: Record<string, FieldDefinition>; // For array of objects
|
||||
itemLabel?: string; // Label for array items
|
||||
}
|
||||
|
||||
export interface FieldDefinition {
|
||||
type: 'text' | 'multiline' | 'select' | 'multiselect' | 'number';
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
options?: string[]; // For select/multiselect
|
||||
required?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export const JSON_FIELD_SCHEMAS: Record<string, JsonFieldSchema> = {
|
||||
content_preferences: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
preferred_formats: {
|
||||
type: 'multiselect',
|
||||
label: 'Preferred Content Formats',
|
||||
options: ['Blog Posts', 'Articles', 'Videos', 'Infographics', 'Webinars', 'Podcasts', 'Case Studies', 'Whitepapers', 'Social Media Posts', 'Email Newsletters'],
|
||||
required: true,
|
||||
helperText: 'Select the content formats your audience prefers'
|
||||
},
|
||||
content_topics: {
|
||||
type: 'multiselect',
|
||||
label: 'Content Topics',
|
||||
options: ['Industry insights', 'Best practices', 'Case studies', 'How-to guides', 'Product updates', 'Company news', 'Thought leadership', 'Educational content'],
|
||||
helperText: 'Select topics your audience is interested in'
|
||||
},
|
||||
content_style: {
|
||||
type: 'multiselect',
|
||||
label: 'Content Style',
|
||||
options: ['Professional', 'Educational', 'Conversational', 'Technical', 'Inspirational', 'Humorous', 'Authoritative'],
|
||||
helperText: 'Select the tone and style for your content'
|
||||
},
|
||||
content_length: {
|
||||
type: 'select',
|
||||
label: 'Preferred Content Length',
|
||||
options: ['Short (300-500 words)', 'Medium (1000-2000 words)', 'Long (2000+ words)', 'Variable'],
|
||||
helperText: 'Select the typical length for your content'
|
||||
},
|
||||
visual_preferences: {
|
||||
type: 'multiselect',
|
||||
label: 'Visual Preferences',
|
||||
options: ['Infographics', 'Charts', 'Diagrams', 'Images', 'Videos', 'Animations', 'Interactive elements'],
|
||||
helperText: 'Select visual elements to include in content'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
consumption_patterns: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
primary_channels: {
|
||||
type: 'multiselect',
|
||||
label: 'Primary Content Channels',
|
||||
options: ['Website', 'Email', 'Social Media', 'Mobile App', 'Newsletter', 'Blog', 'YouTube', 'Podcast'],
|
||||
helperText: 'Where does your audience consume content?'
|
||||
},
|
||||
preferred_times: {
|
||||
type: 'multiselect',
|
||||
label: 'Preferred Consumption Times',
|
||||
options: ['Morning (6-9 AM)', 'Mid-morning (9-11 AM)', 'Lunch (12-2 PM)', 'Afternoon (2-4 PM)', 'Evening (5-7 PM)', 'Night (7-10 PM)'],
|
||||
helperText: 'When does your audience typically consume content?'
|
||||
},
|
||||
device_preference: {
|
||||
type: 'multiselect',
|
||||
label: 'Device Preference',
|
||||
options: ['Desktop', 'Mobile', 'Tablet', 'Smart TV', 'Smart Speaker'],
|
||||
helperText: 'What devices does your audience use?'
|
||||
},
|
||||
content_length_preference: {
|
||||
type: 'select',
|
||||
label: 'Preferred Content Length',
|
||||
options: ['Short (1-3 min read)', 'Medium (5-10 min read)', 'Long (10+ min read)', 'Variable'],
|
||||
helperText: 'How long does your audience prefer to consume content?'
|
||||
},
|
||||
engagement_pattern: {
|
||||
type: 'text',
|
||||
label: 'Engagement Pattern',
|
||||
placeholder: 'e.g., High engagement on educational content',
|
||||
helperText: 'Describe how your audience typically engages with content'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
audience_pain_points: {
|
||||
type: 'array',
|
||||
itemType: 'string',
|
||||
itemLabel: 'Pain Point'
|
||||
},
|
||||
|
||||
buying_journey: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
awareness: {
|
||||
type: 'multiline',
|
||||
label: 'Awareness Stage',
|
||||
placeholder: 'How do customers first discover your solution?',
|
||||
helperText: 'Describe how customers become aware of your product/service'
|
||||
},
|
||||
consideration: {
|
||||
type: 'multiline',
|
||||
label: 'Consideration Stage',
|
||||
placeholder: 'What factors do customers consider?',
|
||||
helperText: 'Describe what customers evaluate during consideration'
|
||||
},
|
||||
decision: {
|
||||
type: 'multiline',
|
||||
label: 'Decision Stage',
|
||||
placeholder: 'What influences the final purchase decision?',
|
||||
helperText: 'Describe what drives the purchase decision'
|
||||
},
|
||||
retention: {
|
||||
type: 'multiline',
|
||||
label: 'Retention Stage',
|
||||
placeholder: 'How do you keep customers engaged?',
|
||||
helperText: 'Describe ongoing engagement and retention strategies'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
seasonal_trends: {
|
||||
type: 'array',
|
||||
itemType: 'string',
|
||||
itemLabel: 'Seasonal Trend'
|
||||
},
|
||||
|
||||
business_objectives: {
|
||||
type: 'array',
|
||||
itemType: 'string',
|
||||
itemLabel: 'Business Objective'
|
||||
},
|
||||
|
||||
target_metrics: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
primary_metric: {
|
||||
type: 'text',
|
||||
label: 'Primary Metric',
|
||||
placeholder: 'e.g., Website traffic',
|
||||
required: true
|
||||
},
|
||||
target_value: {
|
||||
type: 'number',
|
||||
label: 'Target Value',
|
||||
placeholder: 'e.g., 10000',
|
||||
helperText: 'Your target number for the primary metric'
|
||||
},
|
||||
secondary_metrics: {
|
||||
type: 'multiselect',
|
||||
label: 'Secondary Metrics',
|
||||
options: ['Lead generation', 'Conversion rate', 'Engagement rate', 'Brand awareness', 'Customer retention', 'Revenue', 'ROI'],
|
||||
helperText: 'Additional metrics you want to track'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
performance_metrics: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
traffic: {
|
||||
type: 'number',
|
||||
label: 'Monthly Traffic',
|
||||
placeholder: 'e.g., 10000',
|
||||
helperText: 'Current monthly website traffic'
|
||||
},
|
||||
conversion_rate: {
|
||||
type: 'number',
|
||||
label: 'Conversion Rate (%)',
|
||||
placeholder: 'e.g., 2.5',
|
||||
helperText: 'Current conversion rate percentage'
|
||||
},
|
||||
bounce_rate: {
|
||||
type: 'number',
|
||||
label: 'Bounce Rate (%)',
|
||||
placeholder: 'e.g., 50',
|
||||
helperText: 'Current bounce rate percentage'
|
||||
},
|
||||
avg_session_duration: {
|
||||
type: 'number',
|
||||
label: 'Avg Session Duration (seconds)',
|
||||
placeholder: 'e.g., 150',
|
||||
helperText: 'Average time users spend on site'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
engagement_metrics: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
likes: {
|
||||
type: 'number',
|
||||
label: 'Average Likes',
|
||||
placeholder: 'e.g., 500',
|
||||
helperText: 'Average number of likes per post'
|
||||
},
|
||||
shares: {
|
||||
type: 'number',
|
||||
label: 'Average Shares',
|
||||
placeholder: 'e.g., 50',
|
||||
helperText: 'Average number of shares per post'
|
||||
},
|
||||
comments: {
|
||||
type: 'number',
|
||||
label: 'Average Comments',
|
||||
placeholder: 'e.g., 30',
|
||||
helperText: 'Average number of comments per post'
|
||||
},
|
||||
click_through_rate: {
|
||||
type: 'number',
|
||||
label: 'Click-Through Rate (%)',
|
||||
placeholder: 'e.g., 3.5',
|
||||
helperText: 'Average click-through rate percentage'
|
||||
},
|
||||
time_on_page: {
|
||||
type: 'number',
|
||||
label: 'Average Time on Page (seconds)',
|
||||
placeholder: 'e.g., 180',
|
||||
helperText: 'Average time users spend on a page'
|
||||
},
|
||||
engagement_rate: {
|
||||
type: 'number',
|
||||
label: 'Engagement Rate (%)',
|
||||
placeholder: 'e.g., 5.2',
|
||||
helperText: 'Overall engagement rate percentage'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
top_competitors: {
|
||||
type: 'array',
|
||||
itemType: 'object',
|
||||
itemLabel: 'Competitor',
|
||||
itemFields: {
|
||||
name: {
|
||||
type: 'text',
|
||||
label: 'Competitor Name',
|
||||
placeholder: 'e.g., Company ABC',
|
||||
required: true,
|
||||
helperText: 'Name of the competitor'
|
||||
},
|
||||
website: {
|
||||
type: 'text',
|
||||
label: 'Website URL',
|
||||
placeholder: 'e.g., https://example.com',
|
||||
helperText: 'Competitor website URL'
|
||||
},
|
||||
strength: {
|
||||
type: 'multiline',
|
||||
label: 'Key Strengths',
|
||||
placeholder: 'What are their main strengths?',
|
||||
helperText: 'Describe what makes this competitor strong'
|
||||
},
|
||||
weakness: {
|
||||
type: 'multiline',
|
||||
label: 'Key Weaknesses',
|
||||
placeholder: 'What are their main weaknesses?',
|
||||
helperText: 'Describe areas where this competitor is weaker'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
competitor_content_strategies: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
content_types: {
|
||||
type: 'multiselect',
|
||||
label: 'Content Types They Use',
|
||||
options: ['Blog Posts', 'Videos', 'Webinars', 'Case Studies', 'Whitepapers', 'Infographics', 'Podcasts', 'Social Media', 'Email Campaigns'],
|
||||
helperText: 'What content types do competitors focus on?'
|
||||
},
|
||||
publishing_frequency: {
|
||||
type: 'select',
|
||||
label: 'Publishing Frequency',
|
||||
options: ['Daily', 'Multiple times per week', 'Weekly', 'Bi-weekly', 'Monthly', 'Irregular'],
|
||||
helperText: 'How often do competitors publish content?'
|
||||
},
|
||||
content_themes: {
|
||||
type: 'multiselect',
|
||||
label: 'Content Themes',
|
||||
options: ['Product features', 'Industry insights', 'Customer success', 'Thought leadership', 'Educational', 'Entertainment', 'News and updates'],
|
||||
helperText: 'What themes do competitors focus on?'
|
||||
},
|
||||
distribution_channels: {
|
||||
type: 'multiselect',
|
||||
label: 'Distribution Channels',
|
||||
options: ['Website/Blog', 'LinkedIn', 'Twitter', 'Facebook', 'YouTube', 'Email', 'Newsletter', 'Podcast platforms'],
|
||||
helperText: 'Where do competitors distribute their content?'
|
||||
},
|
||||
engagement_approach: {
|
||||
type: 'multiline',
|
||||
label: 'Engagement Approach',
|
||||
placeholder: 'How do competitors engage with their audience?',
|
||||
helperText: 'Describe how competitors interact with their audience'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
market_gaps: {
|
||||
type: 'array',
|
||||
itemType: 'object',
|
||||
itemLabel: 'Market Gap',
|
||||
itemFields: {
|
||||
gap_description: {
|
||||
type: 'multiline',
|
||||
label: 'Gap Description',
|
||||
placeholder: 'Describe the content gap in the market',
|
||||
required: true,
|
||||
helperText: 'What content need is not being met?'
|
||||
},
|
||||
opportunity: {
|
||||
type: 'multiline',
|
||||
label: 'Opportunity',
|
||||
placeholder: 'How can we fill this gap?',
|
||||
helperText: 'How can your brand capitalize on this gap?'
|
||||
},
|
||||
target_audience: {
|
||||
type: 'text',
|
||||
label: 'Target Audience',
|
||||
placeholder: 'e.g., Small business owners',
|
||||
helperText: 'Who would benefit from content addressing this gap?'
|
||||
},
|
||||
priority: {
|
||||
type: 'select',
|
||||
label: 'Priority',
|
||||
options: ['High', 'Medium', 'Low'],
|
||||
helperText: 'How important is it to address this gap?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
industry_trends: {
|
||||
type: 'array',
|
||||
itemType: 'object',
|
||||
itemLabel: 'Industry Trend',
|
||||
itemFields: {
|
||||
trend_name: {
|
||||
type: 'text',
|
||||
label: 'Trend Name',
|
||||
placeholder: 'e.g., AI-powered content creation',
|
||||
required: true,
|
||||
helperText: 'Name of the industry trend'
|
||||
},
|
||||
description: {
|
||||
type: 'multiline',
|
||||
label: 'Description',
|
||||
placeholder: 'Describe the trend and its impact',
|
||||
helperText: 'What is this trend and why does it matter?'
|
||||
},
|
||||
impact: {
|
||||
type: 'select',
|
||||
label: 'Impact Level',
|
||||
options: ['High', 'Medium', 'Low'],
|
||||
helperText: 'How significant is this trend?'
|
||||
},
|
||||
relevance: {
|
||||
type: 'multiline',
|
||||
label: 'Relevance to Your Brand',
|
||||
placeholder: 'How does this trend relate to your content strategy?',
|
||||
helperText: 'How can you leverage this trend?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
emerging_trends: {
|
||||
type: 'array',
|
||||
itemType: 'object',
|
||||
itemLabel: 'Emerging Trend',
|
||||
itemFields: {
|
||||
trend_name: {
|
||||
type: 'text',
|
||||
label: 'Trend Name',
|
||||
placeholder: 'e.g., Voice search optimization',
|
||||
required: true,
|
||||
helperText: 'Name of the emerging trend'
|
||||
},
|
||||
description: {
|
||||
type: 'multiline',
|
||||
label: 'Description',
|
||||
placeholder: 'Describe the emerging trend',
|
||||
helperText: 'What is this new trend?'
|
||||
},
|
||||
growth_potential: {
|
||||
type: 'select',
|
||||
label: 'Growth Potential',
|
||||
options: ['Very High', 'High', 'Medium', 'Low', 'Unknown'],
|
||||
helperText: 'How likely is this trend to grow?'
|
||||
},
|
||||
early_adoption_benefit: {
|
||||
type: 'multiline',
|
||||
label: 'Early Adoption Benefit',
|
||||
placeholder: 'What are the benefits of adopting this trend early?',
|
||||
helperText: 'Why should you consider this trend now?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
content_mix: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
blog_posts: {
|
||||
type: 'number',
|
||||
label: 'Blog Posts (%)',
|
||||
placeholder: 'e.g., 40',
|
||||
helperText: 'Percentage of content mix for blog posts'
|
||||
},
|
||||
videos: {
|
||||
type: 'number',
|
||||
label: 'Videos (%)',
|
||||
placeholder: 'e.g., 25',
|
||||
helperText: 'Percentage of content mix for videos'
|
||||
},
|
||||
social_media: {
|
||||
type: 'number',
|
||||
label: 'Social Media (%)',
|
||||
placeholder: 'e.g., 20',
|
||||
helperText: 'Percentage of content mix for social media'
|
||||
},
|
||||
email: {
|
||||
type: 'number',
|
||||
label: 'Email (%)',
|
||||
placeholder: 'e.g., 10',
|
||||
helperText: 'Percentage of content mix for email'
|
||||
},
|
||||
other_formats: {
|
||||
type: 'number',
|
||||
label: 'Other Formats (%)',
|
||||
placeholder: 'e.g., 5',
|
||||
helperText: 'Percentage of content mix for other formats'
|
||||
},
|
||||
distribution_strategy: {
|
||||
type: 'multiline',
|
||||
label: 'Distribution Strategy',
|
||||
placeholder: 'Describe how you plan to distribute content across these formats',
|
||||
helperText: 'Explain your content distribution approach'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
optimal_timing: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
blog_posts: {
|
||||
type: 'multiselect',
|
||||
label: 'Best Times for Blog Posts',
|
||||
options: ['Monday Morning', 'Tuesday Morning', 'Wednesday Morning', 'Thursday Morning', 'Friday Morning', 'Monday Afternoon', 'Tuesday Afternoon', 'Wednesday Afternoon', 'Thursday Afternoon', 'Friday Afternoon', 'Weekend'],
|
||||
helperText: 'Select optimal days/times for publishing blog posts'
|
||||
},
|
||||
social_media: {
|
||||
type: 'multiselect',
|
||||
label: 'Best Times for Social Media',
|
||||
options: ['Early Morning (6-9 AM)', 'Mid-Morning (9-11 AM)', 'Lunch (12-2 PM)', 'Afternoon (2-5 PM)', 'Evening (5-8 PM)', 'Night (8-10 PM)'],
|
||||
helperText: 'Select optimal times for social media posts'
|
||||
},
|
||||
email: {
|
||||
type: 'multiselect',
|
||||
label: 'Best Times for Email',
|
||||
options: ['Monday Morning', 'Tuesday Morning', 'Wednesday Morning', 'Thursday Morning', 'Friday Morning', 'Weekend'],
|
||||
helperText: 'Select optimal days/times for sending emails'
|
||||
},
|
||||
videos: {
|
||||
type: 'multiselect',
|
||||
label: 'Best Times for Videos',
|
||||
options: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Weekday Evenings', 'Weekend Mornings'],
|
||||
helperText: 'Select optimal days/times for publishing videos'
|
||||
},
|
||||
timezone: {
|
||||
type: 'text',
|
||||
label: 'Target Timezone',
|
||||
placeholder: 'e.g., EST, PST, GMT',
|
||||
helperText: 'Primary timezone for your audience'
|
||||
},
|
||||
notes: {
|
||||
type: 'multiline',
|
||||
label: 'Timing Notes',
|
||||
placeholder: 'Any additional notes about optimal timing',
|
||||
helperText: 'Additional considerations for content timing'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
quality_metrics: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
readability_score: {
|
||||
type: 'number',
|
||||
label: 'Target Readability Score',
|
||||
placeholder: 'e.g., 60',
|
||||
helperText: 'Target Flesch Reading Ease score (0-100)'
|
||||
},
|
||||
word_count_range: {
|
||||
type: 'text',
|
||||
label: 'Word Count Range',
|
||||
placeholder: 'e.g., 1000-2000',
|
||||
helperText: 'Target word count range for content'
|
||||
},
|
||||
seo_score: {
|
||||
type: 'number',
|
||||
label: 'Target SEO Score',
|
||||
placeholder: 'e.g., 80',
|
||||
helperText: 'Target SEO optimization score (0-100)'
|
||||
},
|
||||
engagement_threshold: {
|
||||
type: 'number',
|
||||
label: 'Engagement Threshold (%)',
|
||||
placeholder: 'e.g., 3',
|
||||
helperText: 'Minimum expected engagement rate'
|
||||
},
|
||||
quality_checklist: {
|
||||
type: 'multiselect',
|
||||
label: 'Quality Checklist Items',
|
||||
options: ['Grammar check', 'Fact verification', 'SEO optimization', 'Visual elements', 'Internal linking', 'External linking', 'CTA placement', 'Mobile optimization', 'Accessibility', 'Brand voice consistency'],
|
||||
helperText: 'Quality standards to check before publishing'
|
||||
},
|
||||
review_process: {
|
||||
type: 'multiline',
|
||||
label: 'Review Process',
|
||||
placeholder: 'Describe your content review and approval process',
|
||||
helperText: 'How is content reviewed before publication?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
editorial_guidelines: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
tone: {
|
||||
type: 'multiselect',
|
||||
label: 'Tone Guidelines',
|
||||
options: ['Professional', 'Conversational', 'Friendly', 'Authoritative', 'Educational', 'Inspirational', 'Humorous', 'Technical'],
|
||||
helperText: 'Select the tone(s) to use in content'
|
||||
},
|
||||
style_guide: {
|
||||
type: 'text',
|
||||
label: 'Style Guide Reference',
|
||||
placeholder: 'e.g., AP Style, Chicago Manual, Custom',
|
||||
helperText: 'Which style guide to follow?'
|
||||
},
|
||||
formatting_rules: {
|
||||
type: 'multiline',
|
||||
label: 'Formatting Rules',
|
||||
placeholder: 'e.g., Use H2 for main sections, bullet points for lists, etc.',
|
||||
helperText: 'Specific formatting requirements'
|
||||
},
|
||||
citation_requirements: {
|
||||
type: 'multiline',
|
||||
label: 'Citation Requirements',
|
||||
placeholder: 'Describe how to cite sources and references',
|
||||
helperText: 'How should sources be cited?'
|
||||
},
|
||||
image_guidelines: {
|
||||
type: 'multiline',
|
||||
label: 'Image Guidelines',
|
||||
placeholder: 'Describe image requirements, alt text, sizing, etc.',
|
||||
helperText: 'Guidelines for using images in content'
|
||||
},
|
||||
language_preferences: {
|
||||
type: 'multiselect',
|
||||
label: 'Language Preferences',
|
||||
options: ['US English', 'UK English', 'Canadian English', 'Australian English', 'Other'],
|
||||
helperText: 'Which variant of English to use?'
|
||||
},
|
||||
prohibited_content: {
|
||||
type: 'multiline',
|
||||
label: 'Prohibited Content',
|
||||
placeholder: 'List content types or topics to avoid',
|
||||
helperText: 'What content should be avoided?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
brand_voice: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
personality_traits: {
|
||||
type: 'multiselect',
|
||||
label: 'Brand Personality Traits',
|
||||
options: ['Trustworthy', 'Innovative', 'Friendly', 'Professional', 'Playful', 'Serious', 'Approachable', 'Expert', 'Bold', 'Humble', 'Confident', 'Empathetic'],
|
||||
helperText: 'Select traits that define your brand voice'
|
||||
},
|
||||
communication_style: {
|
||||
type: 'multiline',
|
||||
label: 'Communication Style',
|
||||
placeholder: 'Describe how your brand communicates (formal, casual, etc.)',
|
||||
helperText: 'How does your brand communicate?'
|
||||
},
|
||||
key_messages: {
|
||||
type: 'multiline',
|
||||
label: 'Key Messages',
|
||||
placeholder: 'List the core messages your brand always conveys',
|
||||
helperText: 'What are your brand\'s core messages?'
|
||||
},
|
||||
do_s: {
|
||||
type: 'multiline',
|
||||
label: 'Do\'s',
|
||||
placeholder: 'What your brand voice should do',
|
||||
helperText: 'Guidelines for what your brand voice should do'
|
||||
},
|
||||
dont_s: {
|
||||
type: 'multiline',
|
||||
label: 'Don\'ts',
|
||||
placeholder: 'What your brand voice should avoid',
|
||||
helperText: 'Guidelines for what your brand voice should avoid'
|
||||
},
|
||||
examples: {
|
||||
type: 'multiline',
|
||||
label: 'Voice Examples',
|
||||
placeholder: 'Provide examples of content that represents your brand voice well',
|
||||
helperText: 'Examples of content that matches your brand voice'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
conversion_rates: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
email_signup: {
|
||||
type: 'number',
|
||||
label: 'Email Signup Rate (%)',
|
||||
placeholder: 'e.g., 2.5',
|
||||
helperText: 'Target email signup conversion rate'
|
||||
},
|
||||
lead_generation: {
|
||||
type: 'number',
|
||||
label: 'Lead Generation Rate (%)',
|
||||
placeholder: 'e.g., 1.8',
|
||||
helperText: 'Target lead generation conversion rate'
|
||||
},
|
||||
content_download: {
|
||||
type: 'number',
|
||||
label: 'Content Download Rate (%)',
|
||||
placeholder: 'e.g., 5.0',
|
||||
helperText: 'Target content download conversion rate'
|
||||
},
|
||||
purchase: {
|
||||
type: 'number',
|
||||
label: 'Purchase Rate (%)',
|
||||
placeholder: 'e.g., 0.5',
|
||||
helperText: 'Target purchase conversion rate'
|
||||
},
|
||||
newsletter_subscription: {
|
||||
type: 'number',
|
||||
label: 'Newsletter Subscription Rate (%)',
|
||||
placeholder: 'e.g., 3.0',
|
||||
helperText: 'Target newsletter subscription rate'
|
||||
},
|
||||
current_performance: {
|
||||
type: 'multiline',
|
||||
label: 'Current Performance',
|
||||
placeholder: 'Describe current conversion rate performance',
|
||||
helperText: 'What are your current conversion rates?'
|
||||
},
|
||||
improvement_goals: {
|
||||
type: 'multiline',
|
||||
label: 'Improvement Goals',
|
||||
placeholder: 'Describe goals for improving conversion rates',
|
||||
helperText: 'What improvements are you targeting?'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
content_roi_targets: {
|
||||
type: 'object',
|
||||
fields: {
|
||||
traffic_roi: {
|
||||
type: 'number',
|
||||
label: 'Traffic ROI Target (%)',
|
||||
placeholder: 'e.g., 150',
|
||||
helperText: 'Target ROI for traffic generation (percentage)'
|
||||
},
|
||||
lead_roi: {
|
||||
type: 'number',
|
||||
label: 'Lead ROI Target (%)',
|
||||
placeholder: 'e.g., 200',
|
||||
helperText: 'Target ROI for lead generation (percentage)'
|
||||
},
|
||||
revenue_roi: {
|
||||
type: 'number',
|
||||
label: 'Revenue ROI Target (%)',
|
||||
placeholder: 'e.g., 300',
|
||||
helperText: 'Target ROI for revenue generation (percentage)'
|
||||
},
|
||||
engagement_roi: {
|
||||
type: 'number',
|
||||
label: 'Engagement ROI Target (%)',
|
||||
placeholder: 'e.g., 120',
|
||||
helperText: 'Target ROI for engagement (percentage)'
|
||||
},
|
||||
measurement_period: {
|
||||
type: 'select',
|
||||
label: 'Measurement Period',
|
||||
options: ['Monthly', 'Quarterly', 'Semi-annually', 'Annually'],
|
||||
helperText: 'How often will ROI be measured?'
|
||||
},
|
||||
calculation_method: {
|
||||
type: 'multiline',
|
||||
label: 'ROI Calculation Method',
|
||||
placeholder: 'Describe how ROI is calculated',
|
||||
helperText: 'How do you calculate content ROI?'
|
||||
},
|
||||
benchmarks: {
|
||||
type: 'multiline',
|
||||
label: 'Industry Benchmarks',
|
||||
placeholder: 'List relevant industry ROI benchmarks',
|
||||
helperText: 'What are the industry benchmarks for comparison?'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Edit as EditIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import StrategyIntelligenceTab from '../components/StrategyIntelligence/StrategyIntelligenceTab';
|
||||
@@ -21,6 +21,7 @@ import { StrategyData } from '../components/StrategyIntelligence/types/strategy.
|
||||
|
||||
const ContentStrategyTab: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Use selective store subscriptions to prevent unnecessary re-renders
|
||||
const strategies = useContentPlanningStore(state => state.strategies);
|
||||
@@ -443,14 +444,14 @@ const ContentStrategyTab: React.FC = () => {
|
||||
|
||||
const handleEditStrategy = () => {
|
||||
setShowOnboarding(false);
|
||||
// Navigate to Create tab to edit strategy
|
||||
// This would typically involve changing the active tab in the parent component
|
||||
// Navigate to Create tab (index 4) to edit strategy
|
||||
navigate('/content-planning', { state: { activeTab: 4 } });
|
||||
};
|
||||
|
||||
const handleCreateNewStrategy = () => {
|
||||
setShowOnboarding(false);
|
||||
// Navigate to Create tab to create new strategy
|
||||
// This would typically involve changing the active tab in the parent component
|
||||
// Navigate to Create tab (index 4) to create new strategy
|
||||
navigate('/content-planning', { state: { activeTab: 4 } });
|
||||
};
|
||||
|
||||
const handleCloseOnboarding = () => {
|
||||
|
||||
@@ -520,12 +520,38 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
|
||||
console.log('Wizard: Completing current step:', currentStepNumber, 'with data:', currentStepData);
|
||||
|
||||
try {
|
||||
await setCurrentStep(currentStepNumber, currentStepData);
|
||||
} catch (error) {
|
||||
console.error('Wizard: Failed to complete step with backend. Aborting progression.', error);
|
||||
setShowProgressMessage(false);
|
||||
setProgressMessage('');
|
||||
const stepResult = await setCurrentStep(currentStepNumber, currentStepData);
|
||||
console.log('Wizard: Step completion result:', stepResult);
|
||||
|
||||
// Check for warnings in the response (legacy support)
|
||||
const responseData = stepResult.response || stepResult;
|
||||
if (responseData.warnings && responseData.warnings.length > 0) {
|
||||
console.warn('Wizard: Step completed with warnings:', responseData.warnings);
|
||||
// Show warnings to user - could add a toast notification or alert here
|
||||
setShowProgressMessage(true);
|
||||
setProgressMessage(`Step completed but with issues: ${responseData.warnings.join(', ')}`);
|
||||
setTimeout(() => {
|
||||
setShowProgressMessage(false);
|
||||
setProgressMessage(`Your data is saved, moving to the next step. Progress is ${Math.round(newProgress)}%`);
|
||||
}, 4000); // Show warnings for longer
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Wizard: BLOCKING ERROR - Failed to complete step with backend. Aborting progression.', error);
|
||||
|
||||
// Handle blocking database errors
|
||||
let errorMessage = 'Failed to complete step. Please try again.';
|
||||
if (error.response?.data?.detail) {
|
||||
errorMessage = error.response.data.detail;
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
// Show blocking error message
|
||||
setShowProgressMessage(true);
|
||||
setProgressMessage(`❌ CRITICAL ERROR: ${errorMessage}`);
|
||||
setLoading(false);
|
||||
|
||||
// Don't proceed to next step on blocking errors
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user