From 34f82c43dd97f8c7a217027c6f83d93a9a2fa25d Mon Sep 17 00:00:00 2001 From: ajaysi Date: Mon, 20 Apr 2026 06:10:54 +0530 Subject: [PATCH] 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 --- backend/app.py | 20 +- .../components/SystemStatusIndicator.tsx | 12 +- .../components/VoiceAvatarPlaceholder.tsx | 18 +- .../PodcastMaker/CreateStep/CreateActions.tsx | 34 +++- .../PodcastMaker/PodcastDashboard.tsx | 51 +---- .../PodcastDashboard/QuerySelection.tsx | 8 +- .../PodcastDashboard/ResearchSummary.tsx | 179 ++++++++++++++---- frontend/src/components/PodcastMaker/types.ts | 1 + .../src/components/shared/VoiceSelector.tsx | 32 ++-- frontend/src/services/podcastApi.ts | 7 + 10 files changed, 231 insertions(+), 131 deletions(-) diff --git a/backend/app.py b/backend/app.py index 669e1bef..89df768b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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", diff --git a/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx b/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx index 64aea511..1da14e26 100644 --- a/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx +++ b/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx @@ -131,6 +131,12 @@ const SystemStatusIndicator: React.FC = ({ 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 = ({ 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); diff --git a/frontend/src/components/OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder.tsx b/frontend/src/components/OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder.tsx index c1185c8a..f651302a 100644 --- a/frontend/src/components/OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder.tsx +++ b/frontend/src/components/OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder.tsx @@ -827,19 +827,13 @@ export const VoiceAvatarPlaceholder: React.FC<{ domainName?: string; onVoiceSet? - Read one of these scripts to capture your voice: + Read this script to capture your voice: - - {[ - "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) => ( - - "{text}" - - ))} - + + + "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." + + )} diff --git a/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx b/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx index cf8290d1..0cdbdc9c 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx @@ -392,30 +392,39 @@ export const CreateActions: React.FC = ({ 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 = ({ reset, submit, can !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} diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx index b3d9c31c..b8962e19 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx @@ -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 = () => { - {/* Progress Stepper */} - {project && workflow.activeStep >= 0 && ( - 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 && ( diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard/QuerySelection.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard/QuerySelection.tsx index a817d883..34edea5c 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard/QuerySelection.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard/QuerySelection.tsx @@ -424,7 +424,13 @@ export const QuerySelection: React.FC = ({ {/* Research Progress Modal */} !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} diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx index 5881268c..bbe80182 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx @@ -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 = ({ return ( - {/* Step Indicator */} - - - - } - > - Analysis - - - - - Research - - - - - Script - - - - - Render - - - - - @@ -285,27 +256,153 @@ export const ResearchSummary: React.FC = ({ ) )} - {/* Expert Quotes */} +{/* Expert Quotes */} - + + + NEW + Expert Quotes {research.expertQuotes && research.expertQuotes.length > 0 ? ( - - {research.expertQuotes.slice(0, 4).map((quote, idx) => ( - - - “{quote.quote}” - - - Source S{quote.source_index} + + {research.expertQuotes.slice(0, 4).map((quote, idx) => { + const sourceUrl = research.sources?.[quote.source_index - 1]?.url; + return ( + + + "{quote.quote}" + + + {sourceUrl ? ( + + ) : ( + + )} + + + ); + })} + + ) : ( + + No expert quotes extracted yet. + + )} + + + {/* Listener CTAs */} + + + + NEW + + Listener CTAs + + {research.listenerCta && research.listenerCta.length > 0 ? ( + + {research.listenerCta.slice(0, 4).map((cta, idx) => ( + + + {cta} ))} ) : ( - No expert quotes extracted yet. + No listener CTAs suggested yet. + + )} + + + {/* Mapped Angles */} + + + + NEW + + Mapped Angles + + {research.mappedAngles && research.mappedAngles.length > 0 ? ( + + {research.mappedAngles.slice(0, 4).map((angle, idx) => ( + + + {angle.title || `Angle ${idx + 1}`} + + + {angle.why || "No rationale provided."} + + + ))} + + ) : ( + + No mapped angles available yet. )} diff --git a/frontend/src/components/PodcastMaker/types.ts b/frontend/src/components/PodcastMaker/types.ts index 9315a133..f09c4a82 100644 --- a/frontend/src/components/PodcastMaker/types.ts +++ b/frontend/src/components/PodcastMaker/types.ts @@ -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; diff --git a/frontend/src/components/shared/VoiceSelector.tsx b/frontend/src/components/shared/VoiceSelector.tsx index 0527c993..e193a25c 100644 --- a/frontend/src/components/shared/VoiceSelector.tsx +++ b/frontend/src/components/shared/VoiceSelector.tsx @@ -982,28 +982,32 @@ export const VoiceSelector: React.FC = ({ startIcon={showVoiceClonePanel ? : redoingClone ? : } endIcon={showVoiceClonePanel ? : } 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 ✨"} diff --git a/frontend/src/services/podcastApi.ts b/frontend/src/services/podcastApi.ts index a86b1b28..5e831b99 100644 --- a/frontend/src/services/podcastApi.ts +++ b/frontend/src/services/podcastApi.ts @@ -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,