Added documentation for the auto-population feature and the analytics integration.

This commit is contained in:
ajaysi
2026-01-17 11:01:10 +05:30
parent 8193cdba67
commit 1db10ccd0f
61 changed files with 6773 additions and 579 deletions

View File

@@ -118,22 +118,22 @@ apiClient.interceptors.request.use(
return Promise.reject(new Error('Authentication not ready. Please wait for sign-in to complete.'));
}
try {
const token = await authTokenGetter();
if (token) {
config.headers = config.headers || {};
(config.headers as any)['Authorization'] = `Bearer ${token}`;
console.log(`[apiClient] ✅ Added auth token to request: ${config.url}`);
} else {
try {
const token = await authTokenGetter();
if (token) {
config.headers = config.headers || {};
(config.headers as any)['Authorization'] = `Bearer ${token}`;
console.log(`[apiClient] ✅ Added auth token to request: ${config.url}`);
} else {
// Token getter returned null - reject request to prevent 401 errors
// ProtectedRoute should ensure user is authenticated before components render
console.error(`[apiClient] ❌ authTokenGetter returned null for ${config.url} - rejecting request`);
console.error(`[apiClient] User ID from localStorage: ${localStorage.getItem('user_id') || 'none'}`);
console.error(`[apiClient] This usually means user is not signed in or token expired. ProtectedRoute should prevent this.`);
return Promise.reject(new Error('Authentication token not available. Please sign in to continue.'));
}
} catch (tokenError) {
console.error(`[apiClient] ❌ Error getting auth token for ${config.url}:`, tokenError);
}
} catch (tokenError) {
console.error(`[apiClient] ❌ Error getting auth token for ${config.url}:`, tokenError);
// Reject request if token getter throws an error
return Promise.reject(new Error('Failed to get authentication token. Please try signing in again.'));
}

View File

@@ -18,6 +18,9 @@ export interface OnboardingStepResponse {
step: number;
data?: any;
validation_errors?: string[];
detail?: string; // Error detail from HTTP responses
message?: string; // Success message
warnings?: string[]; // Warning messages
}
export interface OnboardingSessionResponse {
@@ -50,12 +53,24 @@ export async function getCurrentStep() {
export async function setCurrentStep(step: number, stepData?: any) {
// Complete the current step to move to the next one
console.log('setCurrentStep: Completing step', step, 'with data:', stepData);
const res: AxiosResponse<OnboardingStepResponse> = await apiClient.post(`/api/onboarding/step/${step}/complete`, {
data: stepData || {},
validation_errors: []
});
console.log('setCurrentStep: Backend response:', res.data);
return { step };
try {
const res: AxiosResponse<OnboardingStepResponse> = await apiClient.post(`/api/onboarding/step/${step}/complete`, {
data: stepData || {},
validation_errors: []
});
console.log('setCurrentStep: Backend response:', res.data);
return { step, response: res.data }; // Include the full response data including warnings
} catch (error: any) {
// Handle HTTP errors from the backend
console.error('setCurrentStep: Backend error:', error);
if (error.response?.status >= 400) {
const errorData = error.response.data;
const errorMessage = errorData?.detail || errorData?.message || `Step completion failed with status ${error.response.status}`;
throw new Error(errorMessage);
}
// Re-throw other errors
throw error;
}
}
export async function getApiKeys() {

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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}

View File

@@ -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

View File

@@ -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
}
}}
/>
);

View File

@@ -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>
)}

View File

@@ -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;

View File

@@ -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 =>

View File

@@ -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?'
}
}
}
};

View File

@@ -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 = () => {

View File

@@ -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;
}

View File

@@ -608,10 +608,10 @@ class ContentPlanningAPI {
}
// Clear enhanced strategy streaming/cache for a user (best-effort refresh)
// Note: Endpoint gets user_id from authentication, query params are ignored
async clearEnhancedCache(userId?: number): Promise<any> {
const params: any = {};
if (userId) params.user_id = userId;
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/cache/clear`, null, { params });
// Don't pass user_id as query param - endpoint gets it from authentication
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/cache/clear`, null);
return response.data?.data || response.data;
}
@@ -648,10 +648,20 @@ class ContentPlanningAPI {
}
// Onboarding Data Methods
// Note: Endpoint gets user_id from authentication, query params are ignored
async getOnboardingData(userId?: number): Promise<any> {
return this.handleRequest(async () => {
const params = userId ? { user_id: userId } : {};
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/onboarding-data`, { params });
// Don't pass user_id as query param - endpoint gets it from authentication
const response = await apiClient.get(`${this.baseURL}/enhanced-strategies/onboarding-data`);
return response.data?.data || response.data;
});
}
async smartAutofill(userId?: number): Promise<any> {
return this.handleRequest(async () => {
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/smart-autofill`, null, {
params: userId ? { user_id: userId } : {}
});
return response.data?.data || response.data;
});
}

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { contentPlanningApi } from '../services/contentPlanningApi';
// Global flag to prevent multiple simultaneous auto-population calls
@@ -195,6 +196,7 @@ interface StrategyBuilderStore {
// Auto-Population Actions
autoPopulateFromOnboarding: (forceRefresh?: boolean) => Promise<void>;
smartAutofill: () => Promise<void>;
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
@@ -525,8 +527,16 @@ export const STRATEGIC_INPUT_FIELDS: StrategicInputField[] = [
}
];
// Storage keys for persistence
const STORAGE_KEYS = {
STRATEGY_BUILDER: 'strategy_builder_store',
REVIEWED_CATEGORIES: 'strategy_reviewed_categories'
};
// Strategy Builder Store Implementation
export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) => ({
export const useStrategyBuilderStore = create<StrategyBuilderStore>()(
persist(
(set, get) => ({
// Initial State
strategies: [],
currentStrategy: null,
@@ -702,20 +712,23 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
// Add a longer delay to prevent rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
set({ loading: true });
// Clear error state when starting new autofill operation
set({ loading: true, error: null });
console.log('🔄 Starting auto-population from onboarding data...');
// Optionally clear backend caches to force fresh values
// Note: Cache clear gets user_id from authentication, no need to pass it
if (forceRefresh) {
try {
await contentPlanningApi.clearEnhancedCache(1);
await contentPlanningApi.clearEnhancedCache();
} catch (e) {
console.warn('Cache clear failed (non-blocking):', e);
}
}
// Fetch onboarding data to auto-populate fields
// Note: Endpoint gets user_id from authentication, no need to pass it
const response = await contentPlanningApi.getOnboardingData();
// Enhanced logging for autofill data
@@ -751,22 +764,29 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
}))
});
// Validate AI generation success
// Validate response meta (for database autofill, ai_used will be false)
const meta = response.meta || {};
console.log('🤖 AI Generation Meta:', {
console.log('📊 Autofill Meta:', {
aiUsed: meta.ai_used,
aiOverridesCount: meta.ai_overrides_count,
dataSource: meta.data_source,
error: meta.error,
processingTime: meta.processing_time
});
if (meta.ai_used === false || meta.ai_overrides_count === 0) {
console.log('❌ AI generation failed - no real AI values produced');
throw new Error(meta.error || 'AI generation failed to produce strategy fields. Please try again.');
// Database autofill does NOT use AI - only validate if AI was expected
// For database autofill, we expect ai_used: false, which is correct
if (meta.ai_used === false && meta.data_source === 'database') {
console.log('✅ Database autofill successful (no AI used):', Object.keys(fields).length, 'fields');
// Continue processing - database autofill is valid
} else if (meta.ai_used === false && meta.error) {
// Only throw error if AI was expected but failed
console.log('❌ Autofill failed:', meta.error);
throw new Error(meta.error || 'Autofill failed. Please try again.');
} else if (meta.ai_used === true) {
console.log('✅ AI autofill successful:', Object.keys(fields).length, 'fields');
}
console.log('✅ AI generation successful:', Object.keys(fields).length, 'fields');
// Transform the fields object to extract values for formData
const fieldValues: Record<string, any> = {};
const autoPopulatedFields: Record<string, any> = {};
@@ -870,6 +890,9 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
// Store the autofill completion time
sessionStorage.setItem('lastAutofillTime', new Date().toISOString());
// Persist autofill data to localStorage (handled by zustand persist middleware)
console.log('💾 Autofill data persisted to localStorage');
} catch (error: any) {
console.error('❌ Auto-population error:', error);
const errorMessage = error.message || 'Failed to auto-populate from onboarding';
@@ -949,5 +972,197 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
completion_percentage: completionPercentage,
category_completion: categoryCompletion
};
},
smartAutofill: async () => {
// Global protection against multiple simultaneous calls
if (isAutoPopulating) {
console.log('⏸️ Smart autofill skipped - already running globally');
return;
}
isAutoPopulating = true;
try {
// Skip if already loading
if (get().loading) {
console.log('⏸️ Smart autofill skipped - already loading');
return;
}
// Skip if auto-population is blocked
if (get().autoPopulationBlocked) {
console.log('⏸️ Smart autofill skipped - blocked due to previous errors');
return;
}
// Add a delay to prevent rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
// Clear error state when starting new autofill operation
set({ loading: true, error: null });
console.log('🚀 Starting smart autofill (DB + AI combined)...');
// Call smart autofill endpoint (combines DB + AI)
const response = await contentPlanningApi.smartAutofill(1);
// Enhanced logging for smart autofill data
console.log('📊 Smart Autofill Response Structure:', {
hasResponse: !!response,
responseKeys: response ? Object.keys(response) : [],
fieldsCount: response?.fields ? Object.keys(response.fields).length : 0,
sourcesCount: response?.sources ? Object.keys(response.sources).length : 0,
inputDataPointsCount: response?.input_data_points ? Object.keys(response.input_data_points).length : 0,
hasMeta: !!response?.meta
});
// Validate response structure
if (!response) {
throw new Error('Invalid response structure from backend');
}
// Extract field values and sources
const fields = response.fields || {};
const sources = response.sources || {};
const inputDataPoints = response.input_data_points || {};
// Log detailed field information
console.log('🎯 Smart Autofill Field Details:', {
totalFields: Object.keys(fields).length,
fieldIds: Object.keys(fields),
sampleFieldData: Object.keys(fields).slice(0, 3).map(id => ({
id,
hasValue: !!fields[id]?.value,
hasPersonalization: !!fields[id]?.personalization_data,
hasConfidence: !!fields[id]?.confidence_score,
valueType: typeof fields[id]?.value
}))
});
// Validate smart autofill success
const meta = response.meta || {};
console.log('🤖 Smart Autofill Meta:', {
aiUsed: meta.ai_used,
aiOverridesCount: meta.ai_overrides_count,
dbFieldsCount: meta.db_fields_count,
aiFieldsCount: meta.ai_fields_count,
totalFields: meta.total_fields,
dataSource: meta.data_source,
error: meta.error,
processingTime: meta.processing_time_ms
});
// Check if we have any fields generated
if (Object.keys(fields).length === 0) {
console.log('❌ No fields found in smart autofill response');
set({
loading: false,
error: 'Smart autofill failed to produce strategy fields. Please try again.',
autoPopulatedFields: {},
personalizationData: {},
dataSources: {},
inputDataPoints: {}
});
return;
}
console.log('✅ Smart autofill successful:', Object.keys(fields).length, 'fields');
// Transform the fields object to extract values for formData
const fieldValues: Record<string, any> = {};
const autoPopulatedFields: Record<string, any> = {};
const personalizationData: Record<string, any> = {};
const confidenceScores: Record<string, number> = {};
// Process fields from backend
let processedFields = 0;
let skippedFields = 0;
let fieldsWithPersonalization = 0;
let fieldsWithConfidence = 0;
Object.keys(fields).forEach(fieldId => {
const fieldData = fields[fieldId];
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
const value = fieldData.value;
// Store field value
fieldValues[fieldId] = value;
autoPopulatedFields[fieldId] = {
source: fieldData.source || sources[fieldId] || 'smart_autofill',
timestamp: new Date().toISOString(),
method: 'smart_autofill' // Combined DB + AI
};
// Store personalization data if available
if (fieldData.personalization_data) {
personalizationData[fieldId] = fieldData.personalization_data;
fieldsWithPersonalization++;
}
// Store confidence score if available
if (fieldData.confidence_score !== undefined) {
confidenceScores[fieldId] = fieldData.confidence_score;
fieldsWithConfidence++;
}
processedFields++;
} else {
skippedFields++;
}
});
console.log('📊 Smart Autofill Processing Summary:', {
processedFields,
skippedFields,
fieldsWithPersonalization,
fieldsWithConfidence,
dbFieldsCount: meta.db_fields_count,
aiFieldsCount: meta.ai_fields_count
});
// Update store with populated fields
set({
formData: { ...get().formData, ...fieldValues },
autoPopulatedFields,
dataSources: sources,
inputDataPoints,
personalizationData,
confidenceScores,
loading: false,
error: null
});
console.log('✅ Smart autofill completed successfully');
} catch (error: any) {
console.error('❌ Smart autofill error:', error);
set({
loading: false,
error: error.message || 'Smart autofill failed. Please try again.',
autoPopulationBlocked: true // Block further attempts
});
} finally {
isAutoPopulating = false;
}
}
}));
}),
{
name: STORAGE_KEYS.STRATEGY_BUILDER,
// Only persist user-editable data, not loading/error states
partialize: (state) => ({
// Persist form data (user edits)
formData: state.formData,
formErrors: state.formErrors,
// Persist autofill data
autoPopulatedFields: state.autoPopulatedFields,
dataSources: state.dataSources,
inputDataPoints: state.inputDataPoints,
personalizationData: state.personalizationData,
confidenceScores: state.confidenceScores,
// Don't persist loading, error, saving states
}),
}
)
);