feat: podcast demo mode with ALWRITY_ENABLED_FEATURES support

- Add ALWRITY_ENABLED_FEATURES env var for feature gating
- Podcast-only mode: skip LLM bootstrap, scheduler, persona services
- Enhance video generation prompt with scene context, analysis, narration
- Add voice cloning support via custom_voice_id in WaveSpeed
- Add text-to-speech for research results (browser speechSynthesis)
- Fix render queue to sync images from script phase
- Add WaveSpeed LLM pricing (gpt-oss-120b)
- Fix podcast bible generation error handling
- Refactor RouterManager for feature-based router loading
This commit is contained in:
ajaysi
2026-04-03 06:59:59 +05:30
parent c52b1eabc9
commit 63bb937796
58 changed files with 3568 additions and 1597 deletions

View File

@@ -1,11 +1,32 @@
import React, { useState, useEffect } from "react";
import { Stack, Alert, Typography, alpha, IconButton, Collapse } from "@mui/material";
import {
Stack,
Alert,
Typography,
alpha,
Collapse,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemIcon,
ListItemText,
CircularProgress,
Box,
LinearProgress,
} from "@mui/material";
import {
Info as InfoIcon,
Refresh as RefreshIcon,
AutoAwesome as AutoAwesomeIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
CheckCircle as CheckCircleIcon,
Analytics as AnalyticsIcon,
Title as TitleIcon,
ListAlt as ListAltIcon,
Psychology as PsychologyIcon,
RecordVoiceOver as RecordVoiceOverIcon,
} from "@mui/icons-material";
import { PrimaryButton, SecondaryButton } from "../ui";
@@ -16,64 +37,191 @@ interface CreateActionsProps {
isSubmitting: boolean;
}
export const CreateActions: React.FC<CreateActionsProps> = ({
reset,
submit,
canSubmit,
isSubmitting,
}) => {
// ============================================================================
// Constants & Data
// ============================================================================
const ANALYSIS_FEATURES = [
{ icon: <AnalyticsIcon />, text: "Target audience & content type analysis" },
{ icon: <ListAltIcon />, text: "5 high-impact keywords for discoverability" },
{ icon: <TitleIcon />, text: "3 catchy episode title suggestions" },
{ icon: <PsychologyIcon />, text: "2 detailed episode outlines with segments" },
{ icon: <RecordVoiceOverIcon />, text: "4-6 research queries for AI-powered research" },
{ icon: <CheckCircleIcon />, text: "Episode hook, key takeaways & listener CTA" },
];
const ANALYSIS_PROGRESS_STEPS = [
"Analyzing target audience & content type",
"Generating keywords & title suggestions",
"Creating episode outlines",
"Generating research queries",
"Creating hook, takeaways & CTA",
];
const INFO_BANNER_TEXT =
"Podcast avatar Image is required. Brand avatar is default. You can choose from asset library or upload your picture. If not, AI Avatar will be generated automatically.";
// ============================================================================
// Styles
// ============================================================================
const styles = {
dialog: {
background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
border: "1px solid rgba(167, 139, 250, 0.3)",
borderRadius: 3,
},
infoAlert: {
background: alpha("#f0f4ff", 0.6),
border: "1px solid rgba(99, 102, 241, 0.15)",
borderRadius: 2,
boxShadow: "0 1px 3px rgba(99, 102, 241, 0.08)",
},
progressDot: {
width: 6,
height: 6,
borderRadius: "50%",
bgcolor: "#a78bfa",
},
dialogContent: {
color: "rgba(255,255,255,0.8)",
minHeight: 200,
py: 3,
},
};
// ============================================================================
// Sub-Components
// ============================================================================
const InfoBanner: React.FC<{ showInfo: boolean; setShowInfo: (v: boolean) => void }> = ({
showInfo,
setShowInfo,
}) => (
<Collapse in={showInfo}>
<Alert
severity="info"
icon={<InfoIcon sx={{ color: "#6366f1", fontSize: "1.125rem" }} />}
onClose={() => setShowInfo(false)}
sx={styles.infoAlert}
>
<Typography variant="body2" sx={{ fontSize: "0.875rem", color: "#475569", lineHeight: 1.6, fontWeight: 400 }}>
{INFO_BANNER_TEXT}
</Typography>
</Alert>
</Collapse>
);
const ShowTipsLink: React.FC<{ onClick: () => void }> = ({ onClick }) => (
<Stack direction="row" alignItems="center" spacing={1}>
<InfoIcon sx={{ fontSize: 16, color: "#6366f1" }} />
<Typography variant="caption" sx={{ color: "#6366f1", cursor: "pointer", "&:hover": { textDecoration: "underline" } }} onClick={onClick}>
Show tips
</Typography>
</Stack>
);
const AnalysisProgressView: React.FC = () => (
<Stack spacing={3} alignItems="center" sx={styles.dialogContent} justifyContent="center">
<Box sx={{ position: "relative", display: "flex", alignItems: "center", justifyContent: "center" }}>
<CircularProgress size={80} thickness={3} sx={{ color: "#a78bfa" }} />
<Box sx={{ position: "absolute", display: "flex", flexDirection: "column", alignItems: "center" }}>
<AutoAwesomeIcon sx={{ color: "#a78bfa", fontSize: 32 }} />
</Box>
</Box>
<Typography variant="h6" sx={{ color: "#fff", textAlign: "center" }}>
Analyzing Your Podcast Idea
</Typography>
<LinearProgress
sx={{
width: "100%",
height: 8,
borderRadius: 4,
bgcolor: "rgba(255,255,255,0.1)",
"& .MuiLinearProgress-bar": { bgcolor: "#a78bfa", borderRadius: 4 },
}}
/>
<Stack spacing={1} sx={{ width: "100%" }}>
<Typography variant="body2" sx={{ color: "rgba(255,255,255,0.7)", textAlign: "center" }}>
This may take a few moments...
</Typography>
<Stack spacing={0.5} alignItems="flex-start" sx={{ pl: 2 }}>
{ANALYSIS_PROGRESS_STEPS.map((step, idx) => (
<Typography key={idx} variant="caption" sx={{ color: "rgba(255,255,255,0.5)", display: "flex", alignItems: "center", gap: 0.5 }}>
<Box sx={styles.progressDot} /> {step}
</Typography>
))}
</Stack>
</Stack>
</Stack>
);
const WhatYoullGetView: React.FC = () => (
<>
<Typography variant="body2" sx={{ mb: 2, color: "rgba(255,255,255,0.7)" }}>
Click "Start Analysis" to begin AI-powered podcast planning. Here's what we'll generate for you:
</Typography>
<List>
{ANALYSIS_FEATURES.map((feature, index) => (
<ListItem key={index} sx={{ px: 0, py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 36, color: "#a78bfa" }}>{feature.icon}</ListItemIcon>
<ListItemText
primary={feature.text}
primaryTypographyProps={{ sx: { color: "rgba(255,255,255,0.9)", fontSize: "0.9rem" } }}
/>
</ListItem>
))}
</List>
</>
);
// ============================================================================
// Main Component
// ============================================================================
export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, canSubmit, isSubmitting }) => {
const [showInfo, setShowInfo] = useState(true);
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
const [analysisStarted, setAnalysisStarted] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowInfo(false);
}, 8000);
const timer = setTimeout(() => setShowInfo(false), 8000);
return () => clearTimeout(timer);
}, []);
// Close modal when analysis completes
useEffect(() => {
if (!isSubmitting && analysisStarted) {
setShowAnalysisModal(false);
setAnalysisStarted(false);
}
}, [isSubmitting, analysisStarted]);
const handleSubmitClick = () => {
if (canSubmit && !isSubmitting) setShowAnalysisModal(true);
};
const handleStartAnalysis = () => {
setAnalysisStarted(true);
submit();
};
const showProgressInModal = showAnalysisModal && (analysisStarted || isSubmitting);
return (
<Stack spacing={2}>
{/* Collapsible Info Banner */}
<Collapse in={showInfo}>
<Alert
severity="info"
icon={<InfoIcon sx={{ color: "#6366f1", fontSize: "1.125rem" }} />}
onClose={() => setShowInfo(false)}
sx={{
background: alpha("#f0f4ff", 0.6),
border: "1px solid rgba(99, 102, 241, 0.15)",
borderRadius: 2,
boxShadow: "0 1px 3px rgba(99, 102, 241, 0.08)",
"& .MuiAlert-message": {
width: "100%",
},
}}
>
<Typography variant="body2" sx={{ fontSize: "0.875rem", color: "#475569", lineHeight: 1.6, fontWeight: 400 }}>
Podcast avatar Image is required. Brand avatar is default. You can choose from asset library or upload your picture. If not, AI Avatar will be generated automatically.
</Typography>
</Alert>
</Collapse>
{!showInfo && (
<Stack direction="row" alignItems="center" spacing={1}>
<InfoIcon sx={{ fontSize: 16, color: "#6366f1" }} />
<Typography
variant="caption"
sx={{ color: "#6366f1", cursor: "pointer", "&:hover": { textDecoration: "underline" } }}
onClick={() => setShowInfo(true)}
>
Show tips
</Typography>
</Stack>
)}
<InfoBanner showInfo={showInfo} setShowInfo={setShowInfo} />
{!showInfo && <ShowTipsLink onClick={() => setShowInfo(true)} />}
<Stack direction="row" justifyContent="flex-end" spacing={1}>
<SecondaryButton onClick={reset} startIcon={<RefreshIcon />}>
Reset
</SecondaryButton>
<PrimaryButton
onClick={submit}
onClick={handleSubmitClick}
disabled={!canSubmit || isSubmitting}
loading={isSubmitting}
startIcon={<AutoAwesomeIcon />}
@@ -82,6 +230,43 @@ export const CreateActions: React.FC<CreateActionsProps> = ({
{isSubmitting ? "Analyzing..." : "Analyze & Continue"}
</PrimaryButton>
</Stack>
<Dialog
open={showAnalysisModal}
onClose={() => !isSubmitting && setShowAnalysisModal(false)}
maxWidth="sm"
fullWidth
PaperProps={{ sx: styles.dialog }}
>
<DialogTitle sx={{ color: "#fff", display: "flex", alignItems: "center", gap: 1 }}>
{isSubmitting ? (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<CircularProgress size={24} sx={{ color: "#a78bfa" }} />
Analyzing Your Podcast Idea
</Box>
) : (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<AutoAwesomeIcon sx={{ color: "#a78bfa" }} />
What You'll Get
</Box>
)}
</DialogTitle>
<DialogContent sx={styles.dialogContent}>
{showProgressInModal ? <AnalysisProgressView /> : <WhatYoullGetView />}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
{showProgressInModal ? null : (
<>
<SecondaryButton onClick={() => setShowAnalysisModal(false)}>Cancel</SecondaryButton>
<PrimaryButton onClick={handleStartAnalysis} startIcon={<AutoAwesomeIcon />}>
Start Analysis
</PrimaryButton>
</>
)}
</DialogActions>
</Dialog>
</Stack>
);
};