Production fixes: modal stays open, gradient UI, source links, stepper cleanup

- Fixed progress modals closing prematurely during analysis/research
- Enhanced Create Your Voice Clone button with gradient styling
- Light gradient themes for Expert Quotes, Listener CTAs, Mapped Angles
- Made source reference chips clickable with links in new tab
- Removed duplicate stepper (kept in Header only)
- Skip api-stats endpoint in podcast-only mode
- Combined 3 voice scripts into 1 example
- Added force-include step4_assets router in podcast mode
This commit is contained in:
ajaysi
2026-04-20 06:10:54 +05:30
parent 95edd7d470
commit 34f82c43dd
10 changed files with 231 additions and 131 deletions

View File

@@ -424,12 +424,28 @@ if PODCAST_ONLY_DEMO_MODE:
# In podcast-only mode, include only podcast-enabled routers from core registry
from alwrity_utils.router_manager import CORE_ROUTER_REGISTRY
podcast_routers = [r for r in CORE_ROUTER_REGISTRY if "podcast" in r.get("features", set())]
for entry in podcast_routers:
logger.info(f"[PODCAST-ONLY] Found {len(podcast_routers)} podcast routers: {[r['name'] for r in podcast_routers]}")
# Force include step4_assets for voice cloning
step4_entry = next((r for r in CORE_ROUTER_REGISTRY if r.get("name") == "step4_assets"), None)
if step4_entry:
try:
logger.info(f"[PODCAST-ONLY] Forcing load of step4_assets for voice cloning")
router = router_manager._load_router_from_registry(step4_entry)
router_manager.include_router_safely(router, step4_entry["name"], step4_entry.get("include_kwargs"))
except Exception as e:
logger.error(f"[PODCAST-ONLY] Failed to mount step4_assets: {e}")
# Load other podcast routers
for entry in podcast_routers:
if entry.get("name") == "step4_assets":
continue # Already loaded above
try:
logger.info(f"[PODCAST-ONLY] Loading router: {entry['name']}")
router = router_manager._load_router_from_registry(entry)
router_manager.include_router_safely(router, entry["name"], entry.get("include_kwargs"))
except Exception as e:
logger.warning(f"{entry['name']} router not mounted: {e}")
logger.error(f"[PODCAST-ONLY] Failed to mount {entry.get('name', 'unknown')}: {e}")
router_group_status["modular_core"] = {
"mounted": True,
"reason": "Podcast routers only in podcast-only mode",

View File

@@ -131,6 +131,12 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
};
const fetchDetailedStats = async () => {
// Skip detailed stats in podcast-only mode (endpoint not available)
if (isPodcastOnlyDemoMode()) {
setChartData([]);
return;
}
try {
const response = await apiClient.get('/api/content-planning/monitoring/api-stats');
const result = response?.data;
@@ -176,8 +182,10 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
useEffect(() => {
fetchStatus();
// Prime cache performance occasionally even when dashboard is closed
fetchDetailedStats();
// Skip detailed stats in podcast-only mode
if (!isPodcastOnlyDemoMode()) {
fetchDetailedStats();
}
// Refresh every 120 seconds
const interval = setInterval(fetchStatus, 120000);

View File

@@ -827,19 +827,13 @@ export const VoiceAvatarPlaceholder: React.FC<{ domainName?: string; onVoiceSet?
<Box sx={{ width: '100%' }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: '#4B5563', mb: 1, display: 'block', fontSize: '0.7rem' }}>
Read one of these scripts to capture your voice:
Read this script to capture your voice:
</Typography>
<Stack spacing={1}>
{[
"Hi, I'm excited to use AI to scale my content creation. This voice clone will help me stay consistent across all my channels.",
"At our company, we value transparency and innovation. We strive to deliver the best solutions for our clients every single day.",
"Imagine a world where creativity knows no bounds. Where your ideas can take flight and reach millions of people instantly."
].map((text, i) => (
<Paper key={i} elevation={0} sx={{ p: 1, bgcolor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: '#7C3AED', bgcolor: '#F9FAFB', transform: 'translateY(-1px)' } }}>
<Typography variant="body2" sx={{ fontSize: '0.75rem', color: '#374151', lineHeight: 1.4, fontStyle: 'italic' }}>"{text}"</Typography>
</Paper>
))}
</Stack>
<Paper elevation={0} sx={{ p: 2, bgcolor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: '#7C3AED', bgcolor: '#F9FAFB', transform: 'translateY(-1px)' } }}>
<Typography variant="body2" sx={{ fontSize: '0.8rem', color: '#374151', lineHeight: 1.5, fontStyle: 'italic' }}>
"Hi, I'm excited to use AI to scale my content creation. This voice clone will help me stay consistent across all my channels. At our company, we value transparency and innovation, and we strive to deliver the best solutions for our clients every single day. Imagine a world where creativity knows no bounds, where your ideas can take flight and reach millions of people instantly."
</Typography>
</Paper>
</Box>
</Stack>
)}

View File

@@ -392,30 +392,39 @@ export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, can
return () => clearTimeout(timer);
}, []);
// Close modal when analysis completes OR when there's an error
// Close modal only AFTER analysis fully completes (wait for project/analysis to be set)
// Use a ref to track previous isSubmitting to detect the transition from true to false
const prevIsSubmittingRef = useRef(isSubmitting);
const [analysisCompleteRef, setAnalysisCompleteRef] = useState(false);
useEffect(() => {
// Detect transition from submitting to not submitting (analysis complete)
// Track if analysis transitioned from true to false (completed)
const wasSubmitting = prevIsSubmittingRef.current;
const nowNotSubmitting = !isSubmitting;
if (showAnalysisModal && analysisStarted && wasSubmitting && nowNotSubmitting) {
console.warn('[CreateActions] Analysis complete — closing modal and clearing announcement');
// Only close modal if:
// 1. Modal is still shown
// 2. analysisStarted is true
// 3. isSubmitting transitioned from true to false
// 4. AND we're not showing an error
if (showAnalysisModal && analysisStarted && wasSubmitting && nowNotSubmitting && !error) {
// Mark analysis as complete and close after a delay
setAnalysisCompleteRef(true);
console.warn('[CreateActions] Analysis complete — will close modal after delay');
setTimeout(() => {
setShowAnalysisModal(false);
onAnnouncementClear?.();
}, 100);
}, 500);
}
// Update ref for next render
prevIsSubmittingRef.current = isSubmitting;
// If there's an error, also ensure modal is usable
// If there's an error, keep modal open so user can see error message
if (error && showAnalysisModal) {
console.warn('[CreateActions] Error detected:', error);
console.warn('[CreateActions] Error detected — keeping modal open:', error);
}
}, [isSubmitting, showAnalysisModal, analysisStarted, onAnnouncementClear, error]);
}, [isSubmitting, showAnalysisModal, analysisStarted, onAnnouncementClear, error, analysisCompleteRef]);
// Sequential progress - increment every few seconds
useEffect(() => {
@@ -472,7 +481,14 @@ export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, can
<Dialog
open={showAnalysisModal}
onClose={() => !isSubmitting && setShowAnalysisModal(false)}
disableEscapeKeyDown={isSubmitting}
onClose={(event, reason) => {
// Only allow closing if NOT submitting and analysis hasn't started
// This prevents modal from closing when user clicks outside while analysis runs
if (!isSubmitting && !analysisStarted) {
setShowAnalysisModal(false);
}
}}
maxWidth="sm"
fullWidth
fullScreen={isMobile}

View File

@@ -12,7 +12,6 @@ import { ProjectList } from "./ProjectList";
import { PreflightBlockDialog } from "./PreflightBlockDialog";
import {
Header,
ProgressStepper,
EstimateCard,
QuerySelection,
ResearchSummary,
@@ -175,55 +174,7 @@ const PodcastDashboard: React.FC = () => {
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
{/* Progress Stepper */}
{project && workflow.activeStep >= 0 && (
<ProgressStepper
activeStep={workflow.activeStep}
completedSteps={[
...(analysis ? [0] : []), // Analysis step
...(research ? [1] : []), // Research step
...(scriptData ? [2] : []), // Script step
...(scriptData && renderJobs.length > 0 ? [3] : []), // Render step (if script exists and has jobs)
]}
onStepClick={(stepIndex) => {
// Navigate to the clicked step
// Step indices: 0 = Analysis, 1 = Research, 2 = Script, 3 = Render
if (stepIndex === 0) {
// Navigate to Analysis
setShowScriptEditor(false);
setShowRenderQueue(false);
setCurrentStep('analysis');
} else if (stepIndex === 1) {
// Navigate to Research
if (!analysis) {
workflow.setAnnouncement("Complete Analysis first to access Research.");
return;
}
setShowScriptEditor(false);
setShowRenderQueue(false);
setCurrentStep('research');
} else if (stepIndex === 2) {
// Navigate to Script
if (!research) {
workflow.setAnnouncement("Complete Research first to access Script Editor.");
return;
}
setShowRenderQueue(false);
setShowScriptEditor(true);
setCurrentStep('script');
} else if (stepIndex === 3) {
// Navigate to Render
if (!scriptData) {
workflow.setAnnouncement("Generate and approve script first to access Render Queue.");
return;
}
setShowScriptEditor(false);
setShowRenderQueue(true);
setCurrentStep('render');
}
}}
/>
)}
{/* Progress stepper is in Header - keeping UI clean */}
{/* Resume Alert */}
{workflow.showResumeAlert && project && (

View File

@@ -424,7 +424,13 @@ export const QuerySelection: React.FC<QuerySelectionProps> = ({
{/* Research Progress Modal */}
<Dialog
open={showResearchModal}
onClose={() => !isResearching && setShowResearchModal(false)}
disableEscapeKeyDown={isResearching}
onClose={(event, reason) => {
// Only allow closing if NOT researching and research hasn't started
if (!isResearching && !researchStarted) {
setShowResearchModal(false);
}
}}
maxWidth="sm"
fullWidth
fullScreen={isMobile}

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useCallback } from "react";
import { Stack, Typography, Chip, Divider, Box, alpha, Paper, Stepper, Step, StepLabel, CircularProgress } from "@mui/material";
import { Stack, Typography, Chip, Divider, Box, alpha, Paper, CircularProgress } from "@mui/material";
import {
Insights as InsightsIcon,
Search as SearchIcon,
@@ -7,7 +7,6 @@ import {
Article as ArticleIcon,
AutoAwesome as AutoAwesomeIcon,
ArrowForward as ArrowForwardIcon,
CheckCircle as CheckCircleIcon,
} from "@mui/icons-material";
import { Research, ResearchInsight } from "../types";
import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui";
@@ -55,34 +54,6 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
return (
<GlassyCard sx={glassyCardSx}>
<Stack spacing={3}>
{/* Step Indicator */}
<Box sx={{ mb: 1 }}>
<Stepper activeStep={1} alternativeLabel>
<Step completed>
<StepLabel
StepIconComponent={() => <CheckCircleIcon sx={{ color: "#22c55e", fontSize: 24 }} />}
>
Analysis
</StepLabel>
</Step>
<Step active>
<StepLabel>
Research
</StepLabel>
</Step>
<Step>
<StepLabel>
Script
</StepLabel>
</Step>
<Step>
<StepLabel>
Render
</StepLabel>
</Step>
</Stepper>
</Box>
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={2}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, color: "#0f172a", fontWeight: 700 }}>
@@ -285,27 +256,153 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
)
)}
{/* Expert Quotes */}
{/* Expert Quotes */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700 }}>
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
px: 1.5, py: 0.5,
borderRadius: 2,
background: 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)',
color: '#fff',
fontSize: '0.75rem',
fontWeight: 700
}}>
NEW
</Box>
Expert Quotes
</Typography>
{research.expertQuotes && research.expertQuotes.length > 0 ? (
<Stack spacing={1}>
{research.expertQuotes.slice(0, 4).map((quote, idx) => (
<Paper key={`${quote.source_index}-${idx}`} elevation={0} sx={{ p: 1.5, border: "1px solid rgba(0,0,0,0.06)", borderRadius: 2 }}>
<Typography variant="body2" sx={{ color: "#334155", lineHeight: 1.55 }}>
{quote.quote}
</Typography>
<Typography variant="caption" sx={{ color: "#64748b", display: "block", mt: 0.5 }}>
Source S{quote.source_index}
<Stack spacing={1.5}>
{research.expertQuotes.slice(0, 4).map((quote, idx) => {
const sourceUrl = research.sources?.[quote.source_index - 1]?.url;
return (
<Paper key={`${quote.source_index}-${idx}`} elevation={0} sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%)',
border: '1px solid rgba(139, 92, 246, 0.15)'
}}>
<Typography variant="body2" sx={{ color: "#1E1B4B", lineHeight: 1.65, fontStyle: 'italic', fontWeight: 500 }}>
"{quote.quote}"
</Typography>
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 0.5 }}>
{sourceUrl ? (
<Chip
label={`Source S${quote.source_index}`}
size="small"
clickable
component="a"
href={sourceUrl}
target="_blank"
rel="noopener noreferrer"
sx={{
background: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
color: '#fff',
fontWeight: 600,
fontSize: '0.7rem',
cursor: 'pointer',
'&:hover': {
background: 'linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)',
},
}}
/>
) : (
<Chip
label={`Source S${quote.source_index}`}
size="small"
sx={{
background: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
color: '#fff',
fontWeight: 600,
fontSize: '0.7rem',
}}
/>
)}
</Box>
</Paper>
);
})}
</Stack>
) : (
<Typography variant="body2" sx={{ color: "#64748b" }}>
No expert quotes extracted yet.
</Typography>
)}
</Box>
{/* Listener CTAs */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
px: 1.5, py: 0.5,
borderRadius: 2,
background: 'linear-gradient(135deg, #10B981 0%, #14B8A6 100%)',
color: '#fff',
fontSize: '0.75rem',
fontWeight: 700
}}>
NEW
</Box>
Listener CTAs
</Typography>
{research.listenerCta && research.listenerCta.length > 0 ? (
<Stack spacing={1.5}>
{research.listenerCta.slice(0, 4).map((cta, idx) => (
<Paper key={`cta-${idx}`} elevation={0} sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #ECFDF5 0%, #D1FAE5 100%)',
border: '1px solid rgba(16, 185, 129, 0.15)'
}}>
<Typography variant="body2" sx={{ color: "#064E3B", lineHeight: 1.65, fontWeight: 500 }}>
{cta}
</Typography>
</Paper>
))}
</Stack>
) : (
<Typography variant="body2" sx={{ color: "#64748b" }}>
No expert quotes extracted yet.
No listener CTAs suggested yet.
</Typography>
)}
</Box>
{/* Mapped Angles */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
px: 1.5, py: 0.5,
borderRadius: 2,
background: 'linear-gradient(135deg, #0EA5E9 0%, #06B6D4 100%)',
color: '#fff',
fontSize: '0.75rem',
fontWeight: 700
}}>
NEW
</Box>
Mapped Angles
</Typography>
{research.mappedAngles && research.mappedAngles.length > 0 ? (
<Stack spacing={1.5}>
{research.mappedAngles.slice(0, 4).map((angle, idx) => (
<Paper key={`angle-${idx}`} elevation={0} sx={{
p: 2,
borderRadius: 2,
background: 'linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%)',
border: '1px solid rgba(14, 165, 233, 0.15)'
}}>
<Typography variant="subtitle2" sx={{ color: "#0C4A6E", fontWeight: 700, mb: 0.5 }}>
{angle.title || `Angle ${idx + 1}`}
</Typography>
<Typography variant="body2" sx={{ color: "#075985", lineHeight: 1.65 }}>
{angle.why || "No rationale provided."}
</Typography>
</Paper>
))}
</Stack>
) : (
<Typography variant="body2" sx={{ color: "#64748b" }}>
No mapped angles available yet.
</Typography>
)}
</Box>

View File

@@ -47,6 +47,7 @@ export type Research = {
summary: string;
keyInsights: ResearchInsight[];
factCards: Fact[];
sources?: { title: string; url: string; excerpt?: string }[];
mappedAngles: {
title: string;
why: string;

View File

@@ -982,28 +982,32 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
startIcon={showVoiceClonePanel ? <ExpandLess /> : redoingClone ? <RestartAlt /> : <AutoAwesome />}
endIcon={showVoiceClonePanel ? <ExpandLess /> : <ExpandMore />}
sx={{
py: 1.5,
px: 2,
py: 2,
px: 3,
width: "100%",
background: showVoiceClonePanel
? "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)"
: "linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%)",
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
: "linear-gradient(135deg, #8B5CF6 0%, #EC4899 50%, #F59E0B 100%)",
border: showVoiceClonePanel
? "1px solid rgba(102, 126, 234, 0.3)"
: "1px dashed rgba(102, 126, 234, 0.4)",
borderRadius: 2,
color: "#667eea",
fontWeight: 600,
? "1px solid rgba(102, 126, 234, 0.5)"
: "none",
borderRadius: 2.5,
color: "#fff",
fontWeight: 700,
textTransform: "none",
fontSize: "0.875rem",
fontSize: "0.95rem",
boxShadow: showVoiceClonePanel
? "0 4px 15px rgba(102, 126, 234, 0.35)"
: "0 4px 20px rgba(139, 92, 246, 0.4), 0 0 30px rgba(236, 72, 153, 0.2)",
"&:hover": {
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%)",
borderColor: "#667eea",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
background: "linear-gradient(135deg, #7C3AED 0%, #9333EA 50%, #D97706 100%)",
boxShadow: "0 6px 25px rgba(139, 92, 246, 0.5)",
transform: "translateY(-1px)",
},
transition: "all 0.3s ease",
}}
>
{redoingClone ? "Redo Voice Clone" : showVoiceClonePanel ? "Hide Voice Cloning" : "Create Your Voice Clone"}
{redoingClone ? "Redo Voice Clone" : showVoiceClonePanel ? "Hide Voice Cloning" : "Create Your Voice Clone"}
</Button>
<Collapse in={showVoiceClonePanel}>

View File

@@ -213,10 +213,17 @@ const mapExaResearchResponse = (response: any): Research => {
mappedFactIds: angle.mapped_fact_ids || angle.mappedFactIds || []
}));
const sources = (response.sources || []).map((source: any) => ({
title: source.title || "",
url: source.url || "",
excerpt: source.excerpt || source.highlights?.[0] || ""
}));
return {
summary,
keyInsights,
factCards,
sources,
mappedAngles,
expertQuotes,
listenerCta,