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:
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user