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

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

View File

@@ -17,12 +17,14 @@ import {
Warning
} from '@mui/icons-material';
import OnboardingButton from '../common/OnboardingButton';
import { useNavigate } from 'react-router-dom';
import { getApiKeys, completeOnboarding, getOnboardingSummary, getWebsiteAnalysisData, getResearchPreferencesData, setCurrentStep } from '../../../api/onboarding';
import { SetupSummary, CapabilitiesOverview, AgentTeamSection } from './components';
import { FinalStepProps, OnboardingData, Capability } from './types';
import { getAgentTeam, type AgentTeamCatalogEntry } from '../../../api/agentsTeam';
const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }) => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -297,22 +299,9 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
localStorage.setItem('onboarding_active_step', String(stepsLengthFallback()));
} catch {}
// Navigate directly to dashboard without calling onContinue
// This bypasses the wizard flow and goes straight to the dashboard
console.log('FinalStep: Navigating to dashboard...');
console.log('FinalStep: Setting window.location.href to /dashboard');
// Try multiple navigation methods to ensure redirect works
try {
window.location.href = '/dashboard';
console.log('FinalStep: window.location.href set successfully');
} catch (navError) {
console.error('FinalStep: window.location.href failed:', navError);
console.log('FinalStep: Trying alternative navigation method...');
window.location.assign('/dashboard');
}
console.log('FinalStep: Navigation initiated');
// Navigate directly to dashboard using React Router
console.log('FinalStep: Navigating to dashboard with react-router navigate("/dashboard")');
navigate('/dashboard', { replace: true });
} catch (e: any) {
console.error('FinalStep: Error completing onboarding:', e);
console.error('FinalStep: Error details:', {
@@ -528,26 +517,27 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
onClick={handleLaunch}
startIcon={<Rocket />}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
fontSize: '1.125rem',
fontWeight: 600,
px: 4,
py: 2,
borderRadius: 2,
textTransform: 'none',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
background: 'linear-gradient(135deg, #0f172a 0%, #312e81 40%, #4f46e5 100%)',
fontSize: '1.125rem',
fontWeight: 600,
px: 4,
py: 2,
borderRadius: 999,
textTransform: 'none',
boxShadow: '0 10px 28px rgba(15,23,42,0.45)',
letterSpacing: 0.2,
'&:hover': {
background: 'linear-gradient(135deg, #020617 0%, #1e1b4b 40%, #4338ca 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 14px 36px rgba(15,23,42,0.55)',
},
'&:disabled': {
background: 'rgba(148,163,184,0.4)',
color: 'rgba(15,23,42,0.6)',
boxShadow: 'none',
transform: 'none',
}
}}
>
Launch Alwrity & Complete Setup
</Button>
@@ -555,12 +545,16 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
{/* Help Text */}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
This will complete your onboarding and launch Alwrity with your configured settings.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Star sx={{ fontSize: 16 }} />
Ready to create amazing content with AI-powered assistance
<Typography
variant="body2"
color="text.secondary"
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}
>
<Star sx={{ fontSize: 16, color: '#fbbf24' }} />
Your SIF Agent Framework is ready to orchestrate your marketing.
</Typography>
</Box>
</React.Fragment>

View File

@@ -22,6 +22,7 @@ import {
Chip,
Stack,
Divider,
Tooltip,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import GroupIcon from "@mui/icons-material/Group";
@@ -30,6 +31,7 @@ import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
import SaveIcon from "@mui/icons-material/Save";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import {
aiOptimizeAgentProfile,
@@ -242,18 +244,168 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
};
return (
<Paper sx={{ mt: 3, p: 3, borderRadius: 3 }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<GroupIcon />
<Typography variant="h6" sx={{ fontWeight: 700 }}>
Meet {websiteName || "Your"} AI Marketing Team
</Typography>
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
These agents work together to help you plan, execute, and improve your digital marketing. Tools and responsibilities are locked for safety and reliability.
</Typography>
<Paper
elevation={0}
sx={{
mt: 3,
p: 3,
borderRadius: 4,
border: "1px solid #e2e8f0",
bgcolor: "#ffffff",
color: "#0f172a",
boxShadow: "0 1px 2px rgba(15,23,42,0.04)",
"& .MuiTypography-root": {
color: "#111827 !important",
WebkitTextFillColor: "#111827",
},
"& .MuiTypography-body2": {
color: "#4b5563 !important",
},
"& .MuiTypography-caption": {
color: "#6b7280 !important",
},
"& .MuiFormLabel-root": {
color: "#4b5563 !important",
},
"& .MuiFormLabel-root.Mui-focused": {
color: "#4f46e5 !important",
},
"& .MuiInputBase-input": {
color: "#111827 !important",
},
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff !important",
color: "#111827 !important",
},
"& .MuiAccordionDetails-root": {
bgcolor: "#ffffff !important",
},
}}
>
<Box
sx={{
mb: 3,
p: 2.5,
borderRadius: 3,
background: "linear-gradient(135deg, #0f172a 0%, #312e81 40%, #4f46e5 100%)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 2,
boxShadow: "0 12px 30px rgba(15,23,42,0.45)",
"& .MuiTypography-root": {
color: "#e5e7eb !important",
WebkitTextFillColor: "#e5e7eb",
},
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: "999px",
bgcolor: "rgba(129,140,248,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#e5e7eb",
}}
>
<GroupIcon />
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="h6" sx={{ fontWeight: 800, letterSpacing: 0.2 }}>
Meet {websiteName || "Your"} AI Marketing Team
</Typography>
<Typography variant="body2" sx={{ opacity: 0.92 }}>
Enterprise-grade autonomous agents orchestrated by ALwrity&apos;s SIF framework to run your marketing.
</Typography>
</Box>
</Box>
<Tooltip
title="Semantic Intelligence Framework™ Alwrity's orchestration layer for autonomous marketing agents."
arrow
placement="left"
>
<Chip
size="small"
label="SIF Agent Framework™"
sx={{
borderRadius: "999px",
border: "1px solid rgba(191,219,254,0.9)",
bgcolor: "rgba(15,23,42,0.75)",
color: "#e5e7eb",
fontWeight: 600,
letterSpacing: 0.4,
textTransform: "uppercase",
}}
/>
</Tooltip>
</Box>
<Stack spacing={1.5}>
<Box
sx={{
mb: 2,
px: 0.5,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexWrap: "wrap",
gap: 1.5,
}}
>
<Stack direction="row" spacing={1} flexWrap="wrap" alignItems="center">
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.6 }}>
Agent roles
</Typography>
<Chip
size="small"
label="Lead"
sx={{
height: 22,
borderRadius: "999px",
bgcolor: "#eef2ff",
color: "#312e81",
fontWeight: 600,
}}
/>
<Chip
size="small"
label="Strategist"
sx={{
height: 22,
borderRadius: "999px",
bgcolor: "#ecfdf5",
color: "#047857",
fontWeight: 600,
}}
/>
<Chip
size="small"
label="Analyst"
sx={{
height: 22,
borderRadius: "999px",
bgcolor: "#eff6ff",
color: "#1d4ed8",
fontWeight: 600,
}}
/>
</Stack>
<Stack direction="row" spacing={2} alignItems="center">
<Stack direction="row" spacing={0.75} alignItems="center">
<Box sx={{ width: 8, height: 8, borderRadius: "999px", bgcolor: "#22c55e" }} />
<Typography variant="caption">Enabled</Typography>
</Stack>
<Stack direction="row" spacing={0.75} alignItems="center">
<Box sx={{ width: 8, height: 8, borderRadius: "999px", bgcolor: "#e5e7eb" }} />
<Typography variant="caption">Disabled</Typography>
</Stack>
</Stack>
</Box>
<Stack spacing={2}>
{agents.map((agent) => {
const displayName = resolveDisplayName(agent, websiteName);
const scheduleText = formatSchedule(agent.profile?.schedule ?? agent.defaults?.schedule);
@@ -261,66 +413,173 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
const warnings = draft ? lintDraft(agent, draft) : [];
return (
<Accordion key={agent.agent_key} disableGutters elevation={0} sx={{ borderRadius: 2, border: "1px solid rgba(0,0,0,0.08)" }}>
<Accordion
key={agent.agent_key}
disableGutters
elevation={0}
sx={{
borderRadius: 2,
border: "1px solid #e2e8f0",
bgcolor: "#f9fafb",
"&:before": { display: "none" },
transition: "all 160ms ease",
"&:hover": {
borderColor: "#4f46e5",
boxShadow: "0 8px 24px rgba(15,23,42,0.12)",
transform: "translateY(-1px)",
},
"&.Mui-expanded": {
borderColor: "#4f46e5",
boxShadow: "0 12px 30px rgba(15,23,42,0.16)",
bgcolor: "#ffffff",
},
}}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 2 }}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2 }} noWrap>
<Typography
variant="subtitle1"
sx={{ fontWeight: 700, lineHeight: 1.2, color: "#0f172a" }}
noWrap
>
{displayName}
</Typography>
<Typography variant="body2" color="text.secondary" noWrap>
<Typography
variant="body2"
sx={{ color: "#64748b" }}
noWrap
>
{agent.role || agent.agent_key} {scheduleText}
</Typography>
</Box>
<Stack direction="row" spacing={1} sx={{ flexShrink: 0 }}>
<Chip size="small" icon={<LockIcon />} label="Tools locked" variant="outlined" />
<Chip size="small" icon={<LockIcon />} label="Responsibilities locked" variant="outlined" />
<Tooltip title="System tools this agent can call while executing your strategy." arrow>
<Chip
size="small"
icon={<LockIcon />}
label="Tools locked"
variant="outlined"
sx={{
fontWeight: 500,
borderColor: "#cbd5e1",
bgcolor: "#e5edff",
color: "#1e293b",
}}
/>
</Tooltip>
<Tooltip title="High-level responsibilities are predefined for safety and reliability." arrow>
<Chip
size="small"
icon={<LockIcon />}
label="Responsibilities locked"
variant="outlined"
sx={{
fontWeight: 500,
borderColor: "#cbd5e1",
bgcolor: "#e5edff",
color: "#1e293b",
}}
/>
</Tooltip>
</Stack>
</Box>
</AccordionSummary>
<AccordionDetails>
<Stack spacing={2}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Button
size="small"
variant="outlined"
startIcon={<AutoFixHighIcon />}
disabled={aiBusyKey === agent.agent_key}
onClick={() => handleAiOptimize(agent)}
sx={{ textTransform: "none" }}
<Tooltip
title="Let ALwrity refine this agent's prompts and schedule based on your brand context."
arrow
>
AI Optimize
</Button>
<Button
size="small"
variant="outlined"
startIcon={<VisibilityIcon />}
disabled={previewBusyKey === agent.agent_key}
onClick={() => handlePreview(agent)}
sx={{ textTransform: "none" }}
<span>
<Button
size="small"
variant="outlined"
startIcon={<AutoFixHighIcon />}
disabled={aiBusyKey === agent.agent_key}
onClick={() => handleAiOptimize(agent)}
sx={{
textTransform: "none",
borderColor: "#4f46e5",
color: "#4f46e5",
"&:hover": {
borderColor: "#4338ca",
background: "rgba(79,70,229,0.04)",
},
}}
>
AI Optimize
</Button>
</span>
</Tooltip>
<Tooltip
title="Preview how this agent would respond using the current configuration."
arrow
>
Preview
</Button>
<Button
size="small"
variant="contained"
startIcon={<SaveIcon />}
disabled={!draft || savingKey === agent.agent_key}
onClick={() => handleSave(agent)}
sx={{ textTransform: "none" }}
<span>
<Button
size="small"
variant="outlined"
startIcon={<VisibilityIcon />}
disabled={previewBusyKey === agent.agent_key}
onClick={() => handlePreview(agent)}
sx={{
textTransform: "none",
borderColor: "#0f172a",
color: "#0f172a",
"&:hover": {
borderColor: "#111827",
background: "rgba(15,23,42,0.04)",
},
}}
>
Preview
</Button>
</span>
</Tooltip>
<Tooltip
title="Persist this agent's configuration for future sessions."
arrow
>
Save
</Button>
<Button
size="small"
variant="text"
startIcon={<RestartAltIcon />}
disabled={savingKey === agent.agent_key}
onClick={() => handleReset(agent)}
sx={{ textTransform: "none" }}
<span>
<Button
size="small"
variant="contained"
startIcon={<SaveIcon />}
disabled={!draft || savingKey === agent.agent_key}
onClick={() => handleSave(agent)}
sx={{
textTransform: "none",
background: "linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)",
boxShadow: "0 4px 12px rgba(79,70,229,0.35)",
"&:hover": {
background: "linear-gradient(135deg, #4338ca 0%, #6d28d9 100%)",
boxShadow: "0 6px 18px rgba(79,70,229,0.45)",
},
}}
>
Save
</Button>
</span>
</Tooltip>
<Tooltip
title="Revert this agent to its recommended default settings."
arrow
>
Reset
</Button>
<span>
<Button
size="small"
variant="text"
startIcon={<RestartAltIcon />}
disabled={savingKey === agent.agent_key}
onClick={() => handleReset(agent)}
sx={{ textTransform: "none" }}
>
Reset
</Button>
</span>
</Tooltip>
</Box>
{warnings.length > 0 && (
@@ -367,9 +626,34 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
<Divider />
{draft && (
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>
<Box
sx={{
mt: 1,
p: 2.5,
borderRadius: 2,
border: "1px dashed #e5e7eb",
bgcolor: "#f9fafb",
}}
>
<Typography
variant="subtitle2"
sx={{
fontWeight: 700,
mb: 1.5,
display: "flex",
alignItems: "center",
gap: 0.75,
}}
>
<EditOutlinedIcon sx={{ fontSize: 18, color: "#4f46e5" }} />
Editable settings
<Typography
component="span"
variant="caption"
sx={{ ml: 0.75, color: "#6b7280" }}
>
Adjust how this agent behaves for your workspace.
</Typography>
</Typography>
<Stack spacing={2}>
<TextField
@@ -377,6 +661,17 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
value={draft.display_name}
onChange={(e) => setDraftField(agent.agent_key, { display_name: e.target.value })}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
<FormControlLabel
control={
@@ -388,7 +683,20 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
label="Enabled"
/>
<FormControl fullWidth>
<FormControl
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
>
<InputLabel>Schedule</InputLabel>
<Select
label="Schedule"
@@ -418,12 +726,34 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
})
}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
<TextField
label="Time (HH:MM)"
value={draft.schedule?.time || ""}
onChange={(e) => setDraftField(agent.agent_key, { schedule: { ...(draft.schedule || {}), time: e.target.value } })}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
</Stack>
)}
@@ -434,6 +764,17 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
value={draft.schedule?.time || ""}
onChange={(e) => setDraftField(agent.agent_key, { schedule: { ...(draft.schedule || {}), time: e.target.value } })}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
)}
@@ -444,6 +785,17 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
multiline
minRows={6}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
<TextField
label="Task prompt template"
@@ -452,6 +804,17 @@ const AgentTeamSection: React.FC<Props> = ({ websiteName, agents, contextCard })
multiline
minRows={6}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
bgcolor: "#ffffff",
"& fieldset": { borderColor: "#e5e7eb" },
"&:hover fieldset": { borderColor: "#4f46e5" },
"&.Mui-focused fieldset": {
borderColor: "#4f46e5",
boxShadow: "0 0 0 1px rgba(79,70,229,0.25)",
},
},
}}
/>
</Stack>
</Box>

View File

@@ -17,6 +17,10 @@ import {
Chip
} from '@mui/material';
import {
ArrowForward as ArrowForwardIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
PlayArrow as PlayArrowIcon,
// Social Media Icons
Facebook as FacebookIcon,
Twitter as TwitterIcon,
@@ -31,10 +35,13 @@ import {
Google as GoogleIcon,
Analytics as AnalyticsIcon,
// UI Icons
Psychology as PsychologyIcon,
AutoAwesome as AutoAwesomeIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon
} from '@mui/icons-material';
import { motion } from 'framer-motion';
// Import refactored components
import EmailSection from './common/EmailSection';
@@ -53,6 +60,7 @@ interface IntegrationsStepProps {
onContinue: () => void;
updateHeaderContent: (content: { title: string; description: string }) => void;
onValidationChange?: (isValid: boolean) => void;
onDataChange?: (data: any) => void;
}
interface IntegrationPlatform {
@@ -68,7 +76,7 @@ interface IntegrationPlatform {
isEnabled: boolean;
}
const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateHeaderContent, onValidationChange }) => {
const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateHeaderContent, onValidationChange, onDataChange }) => {
const { user } = useUser();
const [email, setEmail] = useState<string>('');
@@ -102,7 +110,7 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
const { connected: wordpressConnected, sites: wordpressSites } = useWordPressOAuth();
// Bing OAuth hook
const { connected: bingConnected, sites: bingSites, connect: connectBing } = useBingOAuth();
const { connected: bingConnected, sites: bingSites, connect: connectBing, refreshStatus: refreshBingStatus } = useBingOAuth();
// Initialize integrations data
const [integrations] = useState<IntegrationPlatform[]>([
@@ -257,6 +265,17 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
}
}, [wordpressConnected, wordpressSites, connectedPlatforms, setConnectedPlatforms, invalidateAnalyticsCache]);
useEffect(() => {
(async () => {
try {
await refreshBingStatus();
} catch (e) {
console.error('Failed to refresh Bing status:', e);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle Bing connection status changes
useEffect(() => {
@@ -354,6 +373,65 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
return sites;
}, [wixConnected, wixSites, wordpressConnected, wordpressSites]);
useEffect(() => {
if (!onDataChange) {
return;
}
const websiteIntegrations = {
wix: wixConnected ? wixSites.map(s => ({ url: s.blog_url, name: 'Wix Site' })) : [],
wordpress: wordpressConnected ? wordpressSites.map(s => ({ url: s.blog_url, name: 'WordPress Site' })) : [],
primaryWebsite: primarySite || null,
};
const analyticsIntegrations = {
gsc: {
connected: connectedPlatforms.includes('gsc'),
sites: (gscSites || []).map((site: any) => ({
siteUrl: site.siteUrl || site.site_url || '',
})),
},
bing: {
connected: connectedPlatforms.includes('bing') || !!bingConnected,
sites: (bingSites || []).map((site: any) => ({
siteUrl: site.siteUrl || site.site_url || '',
})),
},
};
const socialIntegrations = {
facebook: connectedPlatforms.includes('facebook'),
twitter: connectedPlatforms.includes('twitter'),
linkedin: connectedPlatforms.includes('linkedin'),
instagram: connectedPlatforms.includes('instagram'),
youtube: connectedPlatforms.includes('youtube'),
tiktok: connectedPlatforms.includes('tiktok'),
pinterest: connectedPlatforms.includes('pinterest'),
};
onDataChange({
integrations: {
primaryWebsite: websiteIntegrations.primaryWebsite,
websitePlatforms: websiteIntegrations,
analyticsPlatforms: analyticsIntegrations,
socialPlatforms: socialIntegrations,
connectedPlatforms,
updatedAt: new Date().toISOString(),
},
});
}, [
onDataChange,
primarySite,
wixConnected,
wixSites,
wordpressConnected,
wordpressSites,
gscSites,
bingConnected,
bingSites,
connectedPlatforms,
]);
// Default to first site
useEffect(() => {
if (availableSites.length > 0 && !primarySite) {
@@ -379,6 +457,30 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
}
}, [availableSites.length, primarySite, onValidationChange]);
const [walkthroughStep, setWalkthroughStep] = useState<number>(0);
const walkthroughTitles: string[] = [
'Connect your platforms',
'We cache your insights',
'Agents analyze weekly',
'We propose clear fixes',
'You review and publish',
];
const walkthroughDescriptions: string[] = [
'Link Google Search Console and Bing to unlock search signals for your site.',
'We safely store key metrics so recommendations are quick and quotafriendly.',
'SIF agents look for lowCTR pages, strikingdistance wins, declines, and overlaps.',
'Youll see simple suggestions: better titles/meta, refreshes, and consolidations.',
'Pick what you like and publish; we keep the rhythm going week after week.',
];
const walkthroughLabels: string[] = ['Step 1 of 5', 'Step 2 of 5', 'Step 3 of 5', 'Step 4 of 5', 'Step 5 of 5'];
useEffect(() => {
const id = setInterval(() => {
setWalkthroughStep(prev => (prev + 1) % walkthroughTitles.length);
}, 4500);
return () => clearInterval(id);
}, [walkthroughTitles.length]);
return (
<Box sx={{ width: '100%', maxWidth: '100%', p: { xs: 1, sm: 2, md: 3 } }}>
{/* Email Address Section */}
@@ -594,6 +696,281 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
{/* Coming Soon Section */}
<ComingSoonSection />
{/* Recommendation Panel */}
<Fade in timeout={1500}>
<div>
<Paper
elevation={2}
sx={{
mt: 2.5,
p: { xs: 2, md: 2.5 },
borderRadius: 2,
background: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
border: '1px solid #e2e8f0'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}>
<AutoAwesomeIcon sx={{ color: '#7c3aed' }} />
<Typography variant="h6" sx={{ fontWeight: 700, color: '#111827' }}>
How ALwritys SIF Agents Help You Every Week
</Typography>
</Box>
<Typography variant="body2" sx={{ color: '#334155', mb: 1.5 }}>
Your connected analytics power a helpful weekly routine. Our SIF agent framework reads real search signals and proposes simple, highimpact actions for your content—no jargon, just clear next steps.
</Typography>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', mb: 2 }}>
<Chip icon={<AnalyticsIcon />} label="LowCTR pages" sx={{ bgcolor: '#eef2ff', color: '#312e81', fontWeight: 600 }} />
<Chip icon={<AnalyticsIcon />} label="Strikingdistance wins" sx={{ bgcolor: '#ecfeff', color: '#075985', fontWeight: 600 }} />
<Chip icon={<AnalyticsIcon />} label="Declining queries" sx={{ bgcolor: '#f0fdf4', color: '#14532d', fontWeight: 600 }} />
<Chip icon={<AnalyticsIcon />} label="Cannibalization fixes" sx={{ bgcolor: '#fff7ed', color: '#7c2d12', fontWeight: 600 }} />
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap', mb: 2 }}>
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }}>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #e5e7eb', minWidth: 210, textAlign: 'center', bgcolor: '#f9fafb' }}>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 1 }}>
GSC & Bing Metrics
</Typography>
<AnalyticsIcon sx={{ color: '#2563eb' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 1 }}>
Clicks, impressions, CTR, positions
</Typography>
</Paper>
</motion.div>
<ArrowForwardIcon sx={{ color: '#64748b' }} />
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.05 }}>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #e5e7eb', minWidth: 210, textAlign: 'center', bgcolor: '#f9fafb' }}>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 1 }}>
SIF Agents
</Typography>
<PsychologyIcon sx={{ color: '#7c3aed' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 1 }}>
Turns signals into clear suggestions
</Typography>
</Paper>
</motion.div>
<ArrowForwardIcon sx={{ color: '#64748b' }} />
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.1 }}>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #e5e7eb', minWidth: 210, textAlign: 'center', bgcolor: '#f9fafb' }}>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 1 }}>
Suggested Actions
</Typography>
<AutoAwesomeIcon sx={{ color: '#059669' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 1 }}>
Better titles/meta, refreshes, consolidations
</Typography>
</Paper>
</motion.div>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' }, gap: 2 }}>
<motion.div initial={{ opacity: 0, x: -12 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.45 }}>
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 2,
border: '1px solid #e5e7eb',
bgcolor: '#f9fafb',
}}
>
<Typography variant="subtitle2" sx={{ color: '#111827', fontWeight: 700, mb: 1 }}>
Who does what
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.75 }}>
<Chip size="small" label="SEO Agent" sx={{ bgcolor: '#eef2ff', color: '#312e81', fontWeight: 700 }} />
<Typography variant="body2" sx={{ color: '#334155' }}>
Finds lowCTR pages and strikingdistance queries; suggests title/meta fixes and refreshes.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip size="small" label="Content Agent" sx={{ bgcolor: '#ecfeff', color: '#075985', fontWeight: 700 }} />
<Typography variant="body2" sx={{ color: '#334155' }}>
Recommends consolidation and internal links from cannibalization; queues refresh topics.
</Typography>
</Box>
</Paper>
</motion.div>
<motion.div initial={{ opacity: 0, x: 12 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.45 }}>
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 2,
border: '1px solid #e5e7eb',
bgcolor: '#f9fafb',
}}
>
<Typography variant="subtitle2" sx={{ color: '#111827', fontWeight: 700, mb: 1 }}>
What you get
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.75 }}>
<CheckCircleIcon sx={{ color: '#16a34a' }} />
<Typography variant="body2" sx={{ color: '#334155' }}>
Clear, bitesize fixes that improve visibility and clicks.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.75 }}>
<CheckCircleIcon sx={{ color: '#16a34a' }} />
<Typography variant="body2" sx={{ color: '#334155' }}>
A weekly rhythm that keeps content fresh and organized.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleIcon sx={{ color: '#16a34a' }} />
<Typography variant="body2" sx={{ color: '#334155' }}>
Caching protects your quota; agents use cached insights, not direct API calls.
</Typography>
</Box>
</Paper>
</motion.div>
</Box>
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
<Paper
elevation={0}
sx={{
p: 1.75,
borderRadius: 2,
border: '1px solid #e5e7eb',
bgcolor: '#f9fafb',
}}
>
<Typography variant="subtitle2" sx={{ color: '#111827', fontWeight: 700, mb: 1 }}>
Full Flow at a Glance
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, flexWrap: 'wrap' }}>
<Paper
elevation={0}
sx={{
p: 1.25,
borderRadius: 2,
border: '1px solid #e5e7eb',
minWidth: 150,
textAlign: 'center',
bgcolor: '#ffffff',
}}
>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 0.5 }}>
1. Connect
</Typography>
<AnalyticsIcon sx={{ color: '#2563eb' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 0.5 }}>
GSC & Bing
</Typography>
</Paper>
<ArrowForwardIcon sx={{ color: '#64748b' }} />
<Paper
elevation={0}
sx={{
p: 1.25,
borderRadius: 2,
border: '1px solid #e5e7eb',
minWidth: 150,
textAlign: 'center',
bgcolor: '#ffffff',
}}
>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 0.5 }}>
2. Cache
</Typography>
<AutoAwesomeIcon sx={{ color: '#0891b2' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 0.5 }}>
Fast, quotasafe
</Typography>
</Paper>
<ArrowForwardIcon sx={{ color: '#64748b' }} />
<Paper
elevation={0}
sx={{
p: 1.25,
borderRadius: 2,
border: '1px solid #e5e7eb',
minWidth: 150,
textAlign: 'center',
bgcolor: '#ffffff',
}}
>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 0.5 }}>
3. Analyze
</Typography>
<PsychologyIcon sx={{ color: '#7c3aed' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 0.5 }}>
SIF agents
</Typography>
</Paper>
<ArrowForwardIcon sx={{ color: '#64748b' }} />
<Paper
elevation={0}
sx={{
p: 1.25,
borderRadius: 2,
border: '1px solid #e5e7eb',
minWidth: 150,
textAlign: 'center',
bgcolor: '#ffffff',
}}
>
<Typography variant="caption" sx={{ color: '#334155', fontWeight: 700, display: 'block', mb: 0.5 }}>
4. Suggest
</Typography>
<AutoAwesomeIcon sx={{ color: '#059669' }} />
<Typography variant="body2" sx={{ color: '#334155', mt: 0.5 }}>
Clear fixes
</Typography>
</Paper>
</Box>
</Paper>
<Paper
elevation={0}
sx={{
p: 1.75,
borderRadius: 2,
border: '1px solid #e5e7eb',
bgcolor: '#f9fafb',
}}
>
<Typography variant="subtitle2" sx={{ color: '#111827', fontWeight: 700, mb: 1 }}>
Guided Walkthrough
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mb: 1.5 }}>
<Chip
icon={<PlayArrowIcon />}
label="Auto walkthrough"
sx={{ bgcolor: '#eef2ff', color: '#111827', fontWeight: 700 }}
/>
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600 }}>
{walkthroughLabels[walkthroughStep]}
</Typography>
</Box>
<Box sx={{ position: 'relative', minHeight: 120 }}>
<motion.div
key={`walk-${walkthroughStep}`}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.35 }}
>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px dashed #cbd5e1', bgcolor: '#f8fafc' }}>
<Typography variant="body2" sx={{ color: '#334155', fontWeight: 600, mb: 0.5 }}>
{walkthroughTitles[walkthroughStep]}
</Typography>
<Typography variant="body2" sx={{ color: '#475569' }}>
{walkthroughDescriptions[walkthroughStep]}
</Typography>
</Paper>
</motion.div>
</Box>
</Paper>
</Box>
</Box>
</Paper>
</div>
</Fade>
{/* Success Toast */}
<Snackbar
open={showToast}
@@ -613,4 +990,4 @@ const IntegrationsStep: React.FC<IntegrationsStepProps> = ({ onContinue, updateH
);
};
export default IntegrationsStep;
export default IntegrationsStep;

View File

@@ -67,7 +67,7 @@ interface QualityMetrics {
type PersonalizationTab = 'text' | 'image' | 'audio';
const PersonalizationStep: React.FC<PersonalizationStepProps> = ({
onContinue,
onContinue: _onContinue,
updateHeaderContent,
onValidationChange,
onDataChange,
@@ -80,7 +80,6 @@ const PersonalizationStep: React.FC<PersonalizationStepProps> = ({
// AI Generation state (Ported from PersonaStep)
const [generationStep, setGenerationStep] = useState<string>('analyzing');
const [isGenerating, setIsGenerating] = useState(false);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
@@ -94,7 +93,7 @@ const PersonalizationStep: React.FC<PersonalizationStepProps> = ({
// UI state
const [showPreview, setShowPreview] = useState(false);
const [expandedAccordion, setExpandedAccordion] = useState<string | false>('core');
const [hasCheckedCache, setHasCheckedCache] = useState(false);
const [, setHasCheckedCache] = useState(false);
const [configurationOptions, setConfigurationOptions] = useState<any>(null);
// Asset Status State
@@ -417,26 +416,6 @@ const PersonalizationStep: React.FC<PersonalizationStepProps> = ({
generatePersonas();
};
const handleContinue = useCallback(() => {
if (corePersona && platformPersonas && qualityMetrics) {
if (!brandAvatarSet || !voiceCloneSet) {
setError('Please generate and set your Brand Avatar and Voice Clone before continuing.');
return;
}
const personaData = {
corePersona,
platformPersonas,
qualityMetrics,
selectedPlatforms,
stepType: 'personalization',
completedAt: new Date().toISOString()
};
onContinue(personaData);
} else {
setError('Missing persona data. Please generate your brand voice first.');
}
}, [corePersona, platformPersonas, qualityMetrics, selectedPlatforms, onContinue, brandAvatarSet, voiceCloneSet]);
useEffect(() => {
const hasValidData = !!(corePersona && platformPersonas && Object.keys(platformPersonas).length > 0 && qualityMetrics);
const isComplete = !isGenerating && hasValidData && generationStep === 'preview' && brandAvatarSet && voiceCloneSet;

View File

@@ -9,6 +9,7 @@ import {
} from '@mui/material';
import { createAvatarVideoAsync } from '../../../../api/videoStudioApi';
import { useVideoGenerationPolling } from '../../../../hooks/usePolling';
import { fetchMediaBlobUrl } from '../../../../utils/fetchMediaBlobUrl';
import { VideoCameraFront, SkipNext, PlayArrow, InfoOutlined, Close as CloseIcon, HelpOutline, Refresh, RestartAlt, Undo } from '@mui/icons-material';
import { VideoGenerationLoader } from '../../../shared/VideoGenerationLoader';
import { OperationButton } from '../../../shared/OperationButton';
@@ -29,6 +30,7 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
const [success, setSuccess] = useState<string | null>(null);
const [model, setModel] = useState<'infinitetalk' | 'hunyuan-avatar'>('infinitetalk');
const [showCapabilities, setShowCapabilities] = useState(false);
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | null>(null);
const STORAGE_KEY = 'test_persona_video_url';
const STORAGE_BACKUP_KEY = 'test_persona_video_url_backup';
@@ -135,9 +137,29 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
setGeneratedVideoUrl(null);
try {
// 1. Fetch blobs from URLs (works for data URIs too)
const avatarBlob = await fetch(avatarUrl).then(r => r.blob());
const voiceBlob = await fetch(voiceUrl).then(r => r.blob());
let avatarBlob: Blob;
try {
const avatarBlobUrl = await fetchMediaBlobUrl(avatarUrl);
if (avatarBlobUrl) {
avatarBlob = await fetch(avatarBlobUrl).then(r => r.blob());
} else {
avatarBlob = await fetch(avatarUrl).then(r => r.blob());
}
} catch {
avatarBlob = await fetch(avatarUrl).then(r => r.blob());
}
let voiceBlob: Blob;
try {
const voiceBlobUrl = await fetchMediaBlobUrl(voiceUrl);
if (voiceBlobUrl) {
voiceBlob = await fetch(voiceBlobUrl).then(r => r.blob());
} else {
voiceBlob = await fetch(voiceUrl).then(r => r.blob());
}
} catch {
voiceBlob = await fetch(voiceUrl).then(r => r.blob());
}
// 2. Create Files
const avatarFile = new File([avatarBlob], "avatar.png", { type: avatarBlob.type });
@@ -175,6 +197,68 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
}, 100);
};
useEffect(() => {
if (!avatarUrl) {
setAvatarBlobUrl(null);
return;
}
if (avatarUrl.startsWith('data:') || avatarUrl.startsWith('blob:')) {
setAvatarBlobUrl(null);
return;
}
const isInternal =
avatarUrl.includes('/api/podcast/') ||
avatarUrl.includes('/api/youtube/') ||
avatarUrl.includes('/api/story/') ||
(avatarUrl.startsWith('/') && !avatarUrl.startsWith('//'));
if (!isInternal) {
setAvatarBlobUrl(null);
return;
}
let isMounted = true;
const currentAvatarUrl = avatarUrl;
const loadAvatarBlob = async () => {
try {
const blobUrl = await fetchMediaBlobUrl(currentAvatarUrl);
if (!isMounted || avatarUrl !== currentAvatarUrl) {
if (blobUrl && blobUrl.startsWith('blob:')) {
URL.revokeObjectURL(blobUrl);
}
return;
}
setAvatarBlobUrl(prev => {
if (prev && prev !== blobUrl && prev.startsWith('blob:')) {
URL.revokeObjectURL(prev);
}
return blobUrl;
});
} catch {
if (isMounted && avatarUrl === currentAvatarUrl) {
setAvatarBlobUrl(null);
}
}
};
loadAvatarBlob();
return () => {
isMounted = false;
setAvatarBlobUrl(prev => {
if (prev && prev.startsWith('blob:')) {
URL.revokeObjectURL(prev);
}
return null;
});
};
}, [avatarUrl]);
const CapabilitiesModal = () => (
<Dialog
open={showCapabilities}
@@ -429,7 +513,7 @@ export const TestPersonaModal: React.FC<TestPersonaModalProps> = ({
{/* Avatar Preview */}
<Box sx={{ position: 'relative' }}>
<Avatar
src={avatarUrl}
src={avatarBlobUrl || avatarUrl}
sx={{ width: 140, height: 140, border: '4px solid #ffffff', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
/>
<Box sx={{ position: 'absolute', bottom: 0, right: 0, bgcolor: '#10b981', color: 'white', p: 0.5, borderRadius: '50%', border: '2px solid white' }}>

View File

@@ -29,6 +29,7 @@ import {
// Extracted components
import { AnalysisResultsDisplay, AnalysisProgressDisplay } from './WebsiteStep/components';
import type { StyleAnalysis } from './WebsiteStep/components/AnalysisResultsDisplay';
// Import API client for saving
import { apiClient } from '../../api/client';
@@ -48,104 +49,6 @@ interface WebsiteStepProps {
onValidationChange?: (isValid: boolean) => void;
}
interface StyleAnalysis {
id?: number;
writing_style?: {
tone: string;
voice: string;
complexity: string;
engagement_level: string;
brand_personality?: string;
formality_level?: string;
emotional_appeal?: string;
};
content_characteristics?: {
sentence_structure: string;
vocabulary_level: string;
paragraph_organization: string;
content_flow: string;
readability_score?: string;
content_density?: string;
visual_elements_usage?: string;
};
target_audience?: {
demographics: string[];
expertise_level: string;
industry_focus: string;
geographic_focus: string;
psychographic_profile?: string;
pain_points?: string[];
motivations?: string[];
};
content_type?: {
primary_type: string;
secondary_types: string[];
purpose: string;
call_to_action: string;
conversion_focus?: string;
educational_value?: string;
};
brand_analysis?: {
brand_voice: string;
brand_values: string[];
brand_positioning: string;
competitive_differentiation: string;
trust_signals: string[];
authority_indicators: string[];
};
content_strategy_insights?: {
strengths: string[];
weaknesses: string[];
opportunities: string[];
threats: string[];
recommended_improvements: string[];
content_gaps: string[];
};
recommended_settings?: {
writing_tone: string;
target_audience: string;
content_type: string;
creativity_level: string;
geographic_location: string;
industry_context?: string;
brand_alignment?: string;
};
guidelines?: {
tone_recommendations: string[];
structure_guidelines: string[];
vocabulary_suggestions: string[];
engagement_tips: string[];
audience_considerations: string[];
brand_alignment?: string[];
seo_optimization?: string[];
conversion_optimization?: string[];
};
best_practices?: string[];
avoid_elements?: string[];
content_strategy?: string;
ai_generation_tips?: string[];
competitive_advantages?: string[];
content_calendar_suggestions?: string[];
style_patterns?: {
sentence_length: string;
vocabulary_patterns: string[];
rhetorical_devices: string[];
paragraph_structure: string;
transition_phrases: string[];
};
patterns?: {
sentence_length: string;
vocabulary_patterns: string[];
rhetorical_devices: string[];
paragraph_structure: string;
transition_phrases: string[];
};
style_consistency?: string;
unique_elements?: string[];
seo_audit?: any;
sitemap_analysis?: any;
}
interface AnalysisProgress {
step: number;
message: string;
@@ -189,6 +92,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(null);
const [analysisWarning, setAnalysisWarning] = useState<string | null>(null);
const [analysis, setAnalysis] = useState<StyleAnalysis | null>(null);
const [crawlResult, setCrawlResult] = useState<any>(null);
const [existingAnalysis, setExistingAnalysis] = useState<ExistingAnalysis | null>(null);
@@ -290,6 +194,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
setDomainName(result.domainName || '');
setAnalysis(result.analysis);
setCrawlResult(result.crawlResult);
setAnalysisWarning(result.warning || null);
setSuccess('Loaded previous analysis successfully!');
}
return result;
@@ -298,6 +203,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
const handleAnalyze = async () => {
setError(null);
setSuccess(null);
setAnalysisWarning(null);
setLoading(true);
setAnalysis(null);
setCrawlResult(null);
@@ -330,6 +236,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
setDomainName(analysisResult.domainName || '');
setAnalysis(analysisResult.analysis);
setCrawlResult(analysisResult.crawlResult);
setAnalysisWarning(analysisResult.warning || null);
// Store in localStorage for Step 3 (Competitor Analysis)
localStorage.setItem('website_url', fixedUrl);
@@ -404,6 +311,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
if (analysisResult.success) {
setDomainName(analysisResult.domainName || '');
setAnalysis(analysisResult.analysis);
setAnalysisWarning(analysisResult.warning || null);
if (analysisResult.warning) {
setSuccess(`Website style analysis completed successfully! Note: ${analysisResult.warning}`);
@@ -754,6 +662,7 @@ const WebsiteStep: React.FC<WebsiteStepProps> = ({ onContinue, updateHeaderConte
useAnalysisForGenAI={useAnalysisForGenAI}
onUseAnalysisChange={setUseAnalysisForGenAI}
onAnalysisUpdate={handleAnalysisUpdate}
warning={analysisWarning || undefined}
onSave={() => saveAnalysis(analysis)}
/>
</Box>

View File

@@ -64,7 +64,18 @@ import { useOnboardingStyles } from '../../common/useOnboardingStyles';
import { apiClient } from '../../../../api/client';
interface StyleAnalysis {
export interface StyleAnalysis {
id?: number;
guidelines?: {
tone_recommendations?: string[];
structure_guidelines?: string[];
vocabulary_suggestions?: string[];
engagement_tips?: string[];
audience_considerations?: string[];
brand_alignment?: string[];
seo_optimization?: string[];
conversion_optimization?: string[];
} | null;
writing_style?: {
tone: string;
voice: string;
@@ -132,6 +143,7 @@ interface AnalysisResultsDisplayProps {
onUseAnalysisChange: (use: boolean) => void;
crawlResult?: any;
onAnalysisUpdate?: (updatedAnalysis: StyleAnalysis) => void;
warning?: string;
onSave?: () => void;
}
@@ -142,12 +154,17 @@ const AnalysisResultsDisplay: React.FC<AnalysisResultsDisplayProps> = ({
onUseAnalysisChange,
crawlResult,
onAnalysisUpdate,
warning,
onSave
}) => {
const styles = useOnboardingStyles();
const [isCrawlExpanded, setIsCrawlExpanded] = useState(false);
const [isEditable, setIsEditable] = useState(false);
const warningParts = warning ? warning.split('|').map(part => part.trim()).filter(Boolean) : [];
const guidelineWarning = warningParts.find(part => part.toLowerCase().startsWith('guidelines generation failed'));
const sitemapWarning = warningParts.find(part => part.toLowerCase().startsWith('sitemap analysis failed'));
// Helper to handle section updates
const handleSectionUpdate = (section: string, fieldPath: string, value: any) => {
if (!onAnalysisUpdate) return;
@@ -383,17 +400,25 @@ const AnalysisResultsDisplay: React.FC<AnalysisResultsDisplayProps> = ({
{renderBrandAnalysisSection(analysis)}
</Box>
{/* Style Guidelines Section */}
<Box sx={{ mt: 4 }}>
<SectionHeader
title="Style Guidelines"
icon={<AutoAwesomeIcon />}
/>
<EnhancedGuidelinesSection
guidelines={analysis.style_guidelines}
domainName={domainName}
/>
</Box>
{(analysis.guidelines || guidelineWarning) && (
<Box sx={{ mt: 4 }}>
<SectionHeader
title="Style Guidelines"
icon={<AutoAwesomeIcon />}
/>
{guidelineWarning && (
<Alert severity="warning" sx={{ mb: 2 }}>
{guidelineWarning}
</Alert>
)}
{analysis.guidelines && (
<EnhancedGuidelinesSection
guidelines={analysis.guidelines}
domainName={domainName}
/>
)}
</Box>
)}
{/* SEO Audit Section */}
<Box sx={{ mt: 4 }}>
@@ -408,12 +433,16 @@ const AnalysisResultsDisplay: React.FC<AnalysisResultsDisplayProps> = ({
/>
</Box>
{/* Sitemap Analysis Section */}
<Box sx={{ mt: 4 }}>
<SectionHeader
title="Sitemap Analysis"
icon={<LinkIcon />}
/>
{sitemapWarning && (
<Alert severity="warning" sx={{ mb: 2 }}>
{sitemapWarning}
</Alert>
)}
<SitemapAnalysisSection
sitemapAnalysis={analysis.sitemap_analysis}
domainName={domainName}

View File

@@ -36,7 +36,7 @@ interface Guidelines {
}
interface EnhancedGuidelinesSectionProps {
guidelines: Guidelines;
guidelines?: Guidelines | null;
domainName: string;
}
@@ -46,6 +46,10 @@ const EnhancedGuidelinesSection: React.FC<EnhancedGuidelinesSectionProps> = ({
}) => {
const styles = useOnboardingStyles();
if (!guidelines) {
return null;
}
return (
<Box sx={styles.analysisSection}>
<Typography variant="h5" sx={styles.analysisSectionHeader} gutterBottom>

View File

@@ -108,6 +108,7 @@ export const loadExistingAnalysis = async (analysisId: number, website: string):
analysis?: any;
domainName?: string;
crawlResult?: any;
warning?: string;
error?: string;
}> => {
try {
@@ -115,13 +116,12 @@ export const loadExistingAnalysis = async (analysisId: number, website: string):
const result = response.data;
if (result.success && result.analysis) {
// Extract domain name for personalization
const extractedDomain = extractDomainName(website);
// Database structure: flat fields at top level
// Need to combine them into the format expected by UI
const comprehensiveAnalysis = {
// Top-level style analysis fields from database
id: result.analysis.id,
writing_style: result.analysis.writing_style,
content_characteristics: result.analysis.content_characteristics,
target_audience: result.analysis.target_audience,
@@ -151,7 +151,8 @@ export const loadExistingAnalysis = async (analysisId: number, website: string):
success: true,
analysis: comprehensiveAnalysis,
domainName: extractedDomain,
crawlResult: result.analysis.crawl_result
crawlResult: result.analysis.crawl_result,
warning: result.analysis.warning_message
};
}
return {
@@ -212,6 +213,7 @@ export const performAnalysis = async (
// Combine all analysis data into a comprehensive object
const comprehensiveAnalysis = {
id: result.analysis_id,
...result.style_analysis,
seo_audit: result.seo_audit,
sitemap_analysis: result.crawl_result?.sitemap_analysis,

View File

@@ -654,6 +654,20 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
}
}
// Special handling for IntegrationsStep (step 4)
if (activeStep === 4) {
const currentData = stepDataRef.current || {};
if (!currentStepData && currentData && typeof currentData === 'object') {
if (currentData.integrations) {
currentStepData = {
integrations: currentData.integrations,
};
} else {
currentStepData = currentData;
}
}
}
// Store step data in state
if (currentStepData) {
setStepData(currentStepData);
@@ -681,7 +695,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// Complete the current step (activeStep + 1 because steps are 1-indexed)
const currentStepNumber = activeStep + 1;
const stepWasCompleted = currentStepData && typeof currentStepData === 'object' && (
const hasCoreStepData = currentStepData && typeof currentStepData === 'object' && (
currentStepData.website ||
currentStepData.businessData ||
currentStepData.competitors ||
@@ -692,6 +706,10 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
currentStepData.qualityMetrics
);
const hasIntegrationsData = !!(currentStepData && typeof currentStepData === 'object' && currentStepData.integrations);
const stepWasCompleted = hasCoreStepData || hasIntegrationsData;
console.log('Wizard: Step completion check:', {
currentStepNumber,
hasData: !!currentStepData,
@@ -881,6 +899,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
onContinue={handleNext}
updateHeaderContent={updateHeaderContent}
onValidationChange={(isValid: boolean) => handleStepValidationChange(4, isValid)}
onDataChange={handleStepDataChange}
/>,
<FinalStep key="final" onContinue={handleComplete} updateHeaderContent={updateHeaderContent} />
];
@@ -901,6 +920,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
return (
<Box
className="light-theme-container"
sx={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',