diff --git a/backend/alwrity_utils/router_manager.py b/backend/alwrity_utils/router_manager.py index 3b7c2f0c..ab514343 100644 --- a/backend/alwrity_utils/router_manager.py +++ b/backend/alwrity_utils/router_manager.py @@ -16,7 +16,7 @@ CORE_ROUTER_REGISTRY = [ {"name": "component_logic", "module": "api.component_logic", "attr": "router", "features": {"all", "core"}}, {"name": "subscription", "module": "api.subscription", "attr": "router", "features": {"all", "core", "podcast", "blog-writer", "youtube"}}, {"name": "step3_research", "module": "api.onboarding_utils.step3_routes", "attr": "router", "features": {"all", "core"}}, - {"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "features": {"all", "core"}}, + {"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "features": {"all", "core", "podcast"}}, {"name": "step4_persona", "module": "api.onboarding_utils.step4_persona_routes_optimized", "attr": "router", "features": {"all", "core"}}, {"name": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "features": {"all", "core", "seo"}}, {"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "features": {"all", "core"}}, @@ -116,10 +116,6 @@ class RouterManager: if "all" in enabled_features: return True - # Skip core routers in podcast-only mode (they require non-podcast features) - if enabled_features == {"podcast"}: - return False - # If no required features specified, include by default if not required_features: return True diff --git a/frontend/src/components/PodcastMaker/AnalysisPanel/AnalysisTabNav.tsx b/frontend/src/components/PodcastMaker/AnalysisPanel/AnalysisTabNav.tsx index 993cf0a3..74811147 100644 --- a/frontend/src/components/PodcastMaker/AnalysisPanel/AnalysisTabNav.tsx +++ b/frontend/src/components/PodcastMaker/AnalysisPanel/AnalysisTabNav.tsx @@ -26,20 +26,28 @@ export const ANALYSIS_TABS: TabConfig[] = [ const getTabButtonStyles = (isActive: boolean) => ({ background: isActive ? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" - : "transparent", - color: isActive ? "#fff" : "#64748b", - border: isActive ? "none" : "1px solid rgba(0,0,0,0.1)", + : "rgba(255, 255, 255, 0.8)", + color: isActive ? "#fff" : "#475569", + border: isActive ? "none" : "1px solid rgba(102, 126, 234, 0.2)", borderRadius: 2, - px: 2, - py: 1, - fontSize: "0.75rem", + px: 2.5, + py: 1.25, + fontSize: "0.8125rem", fontWeight: 600, textTransform: "none" as const, transition: "all 0.2s ease", + boxShadow: isActive + ? "0 4px 12px rgba(102, 126, 234, 0.35)" + : "0 2px 4px rgba(0, 0, 0, 0.04)", "&:hover": { background: isActive ? "linear-gradient(135deg, #764ba2 0%, #667eea 100%)" - : "rgba(102,126,234,0.08)", + : "rgba(102, 126, 234, 0.12)", + border: isActive ? "none" : "1px solid rgba(102, 126, 234, 0.35)", + boxShadow: isActive + ? "0 6px 16px rgba(102, 126, 234, 0.4)" + : "0 4px 8px rgba(102, 126, 234, 0.15)", + transform: "translateY(-1px)", }, }); @@ -50,18 +58,27 @@ interface AnalysisTabNavProps { export const AnalysisTabNav: React.FC = ({ activeTab, onTabChange }) => { return ( - - {ANALYSIS_TABS.map((tab) => ( - - ))} - + + + {ANALYSIS_TABS.map((tab) => ( + + ))} + + ); }; diff --git a/frontend/src/components/PodcastMaker/CreateModal.tsx b/frontend/src/components/PodcastMaker/CreateModal.tsx index d6bba11b..a24b8250 100644 --- a/frontend/src/components/PodcastMaker/CreateModal.tsx +++ b/frontend/src/components/PodcastMaker/CreateModal.tsx @@ -8,7 +8,6 @@ import { getLatestBrandAvatar } from "../../api/brandAssets"; import { VoiceSelector } from "../shared/VoiceSelector"; // Imported Components -import { CreateHeader } from "./CreateStep/CreateHeader"; import { TopicUrlInput, TOPIC_PLACEHOLDERS } from "./CreateStep/TopicUrlInput"; import { PodcastConfiguration } from "./CreateStep/PodcastConfiguration"; import { AvatarSelector } from "./CreateStep/AvatarSelector"; @@ -280,7 +279,13 @@ export const CreateModal: React.FC = ({ onCreate, open, defaul brandAvatarBlobUrl // Brand avatar blob URL ); - const canSubmit = Boolean(topicInput.trim() && hasAvatar); + // Check if all required inputs are provided + const hasTopic = Boolean(topicInput.trim()); + const hasVoice = Boolean(selectedVoiceId); + const hasDuration = Boolean(duration > 0 && duration <= 10); + const hasSpeakers = Boolean(speakers >= 1 && speakers <= 2); + + const canSubmit = Boolean(hasTopic && hasAvatar && hasVoice && hasDuration && hasSpeakers); const submit = async () => { if (!canSubmit || isSubmitting) return; @@ -521,14 +526,6 @@ export const CreateModal: React.FC = ({ onCreate, open, defaul }} > - - = ({ onCreate, open, defaul placeholderIndex={placeholderIndex} loading={enhancingTopic} loadingMessage={enhanceTopicMessage} + estimatedCost={estimatedCost} + duration={duration} + speakers={speakers} + knobs={knobs} /> diff --git a/frontend/src/components/PodcastMaker/CreateStep/AvatarSelector.tsx b/frontend/src/components/PodcastMaker/CreateStep/AvatarSelector.tsx index 3ecc1088..212a81a3 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/AvatarSelector.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/AvatarSelector.tsx @@ -69,7 +69,21 @@ export const AvatarSelector: React.FC = ({ }} > - + + + 3 + = ({ sx={{ p: { xs: 1, sm: 1.5 }, borderRadius: 1.5, - background: alpha("#f8fafc", 0.8), - border: "1px solid rgba(15, 23, 42, 0.1)", + background: alpha("#f0f4ff", 0.6), + border: "1px solid rgba(99, 102, 241, 0.2)", }} > - - - Take a Selfie - - - Capture a photo using your device camera and use "Make Presentable" to enhance it into a professional presenter using AI. - - - - - - Camera access required for selfie capture - + + + + Capture a photo using your device camera and use "Make Presentable" to enhance it. Camera access required. + + )} @@ -570,30 +570,16 @@ export const AvatarSelector: React.FC = ({ sx={{ p: { xs: 1, sm: 1.5 }, borderRadius: 1.5, - background: alpha("#f8fafc", 0.8), - border: "1px solid rgba(15, 23, 42, 0.1)", + background: alpha("#f0f4ff", 0.6), + border: "1px solid rgba(99, 102, 241, 0.2)", }} > - - - Upload Your Photo - - - Upload a new photo and use "Make Presentable" to enhance it into a professional presenter using AI. - - - - - - Supported formats: JPG, PNG, WebP (max 5MB) - + + + + Upload a photo and use "Make Presentable" to enhance it into a professional presenter. Supported: JPG, PNG, WebP (max 5MB) + + )} diff --git a/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx b/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx index 5fbe8d76..d5248d93 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/CreateActions.tsx @@ -225,7 +225,7 @@ export const CreateActions: React.FC = ({ reset, submit, can disabled={!canSubmit || isSubmitting} loading={isSubmitting} startIcon={} - tooltip={!canSubmit ? "Enter an idea/URL and add a podcast avatar to continue" : "We'll start AI analysis after this click"} + tooltip={!canSubmit ? "Complete all steps: 1) Enter topic/URL, 2) Configure duration & speakers, 3) Add avatar, 4) Select voice" : "We'll start AI analysis after this click"} > {isSubmitting ? "Analyzing..." : "Analyze & Continue"} diff --git a/frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx b/frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx index 5423409d..820914d4 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx @@ -29,32 +29,53 @@ export const CreateHeader: React.FC = ({ estimatedCost, }) => { return ( - - - - - - - + + + + + + + Create New Podcast Episode @@ -106,122 +127,122 @@ export const CreateHeader: React.FC = ({ - - - - - - - - - - - 1 ? "s" : ""}`} - size="small" - sx={{ - background: alpha("#0f172a", 0.06), - color: "#0f172a", - fontWeight: 600, - border: "1px solid rgba(15, 23, 42, 0.12)", - fontSize: "0.75rem", - height: 26, - cursor: "help", - }} - /> - - - - Estimated Cost Breakdown: - - - • Audio Generation: ${estimatedCost.ttsCost}
- • Avatar Creation: ${estimatedCost.avatarCost}
- • Video Rendering: ${estimatedCost.videoCost}
- • Research: ${estimatedCost.researchCost}
- - Total: ${estimatedCost.total} - - - Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs.bitrate === "hd" ? "HD" : "standard"} quality - -
-
- } - arrow - placement="top" - componentsProps={{ - tooltip: { - sx: { - bgcolor: "#0f172a", - color: "#ffffff", - maxWidth: 280, - fontSize: "0.875rem", - p: 1.5, - boxShadow: "0 4px 12px rgba(0,0,0,0.15)", - }, - }, - arrow: { - sx: { +
+ + + + + + + + + 1 ? "s" : ""}`} + size="small" + sx={{ + background: alpha("#0f172a", 0.06), + color: "#0f172a", + fontWeight: 600, + border: "1px solid rgba(15, 23, 42, 0.12)", + fontSize: "0.75rem", + height: 26, + cursor: "help", + }} + /> + + + + Estimated Cost Breakdown: + + + • Audio Generation: ${estimatedCost.ttsCost}
+ • Avatar Creation: ${estimatedCost.avatarCost}
+ • Video Rendering: ${estimatedCost.videoCost}
+ • Research: ${estimatedCost.researchCost}
+ + Total: ${estimatedCost.total} + + + Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs.bitrate === "hd" ? "HD" : "standard"} quality + +
+ + } + arrow + placement="top" + componentsProps={{ + tooltip: { + sx: { + bgcolor: "#0f172a", + color: "#ffffff", + maxWidth: 280, + fontSize: "0.875rem", + p: 1.5, + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + }, + }, + arrow: { + sx: { + color: "#0f172a", + }, }, - }, - }} - > - } - label={`Est. $${estimatedCost.total}`} - size="small" - sx={{ - background: "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)", - color: "#059669", - fontWeight: 600, - border: "1px solid rgba(16, 185, 129, 0.2)", - fontSize: "0.75rem", - height: 26, - cursor: "help", }} - /> -
+ > + } + label={`Est. $${estimatedCost.total}`} + size="small" + sx={{ + background: "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)", + color: "#059669", + fontWeight: 600, + border: "1px solid rgba(16, 185, 129, 0.2)", + fontSize: "0.75rem", + height: 26, + cursor: "help", + }} + /> + +
-
+ ); }; diff --git a/frontend/src/components/PodcastMaker/CreateStep/PodcastConfiguration.tsx b/frontend/src/components/PodcastMaker/CreateStep/PodcastConfiguration.tsx index db891c33..5d338b1b 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/PodcastConfiguration.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/PodcastConfiguration.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Stack, Box, Typography, TextField, ToggleButton, ToggleButtonGroup, alpha } from "@mui/material"; -import { Person as PersonIcon, Group as GroupIcon } from "@mui/icons-material"; +import { Person as PersonIcon, Group as GroupIcon, Settings as SettingsIcon } from "@mui/icons-material"; interface PodcastConfigurationProps { duration: number; @@ -35,22 +35,72 @@ export const PodcastConfiguration: React.FC = ({ flex: { xs: "1 1 auto", lg: "0 0 320px" }, width: { xs: "100%", lg: "320px" }, p: 3, - borderRadius: 2, - background: alpha("#f8fafc", 0.5), - border: "1px solid rgba(15, 23, 42, 0.06)", + borderRadius: 3, + background: "#ffffff", + border: "1px solid rgba(102, 126, 234, 0.15)", height: "100%", display: "flex", flexDirection: "column", + boxShadow: "0 4px 20px rgba(102, 126, 234, 0.08)", + position: "relative", + overflow: "hidden", + "&::before": { + content: '""', + position: "absolute", + top: 0, + left: 0, + right: 0, + height: "3px", + background: "linear-gradient(90deg, #667eea 0%, #764ba2 100%)", + }, }} > - - Basic Configuration - + + + 2 + + + + + + Basic Configuration + + {/* Duration Input */} - + Duration (minutes) = ({ fullWidth sx={{ "& .MuiOutlinedInput-root": { - backgroundColor: "#ffffff", - border: "1px solid rgba(15, 23, 42, 0.12)", + backgroundColor: "#f8fafc", + border: "2px solid rgba(102, 126, 234, 0.2)", borderRadius: 2, transition: "all 0.2s", "&:hover": { - borderColor: "rgba(102, 126, 234, 0.6)", + borderColor: "rgba(102, 126, 234, 0.4)", + boxShadow: "0 2px 8px rgba(102, 126, 234, 0.1)", }, "&.Mui-focused": { borderColor: "#667eea", - boxShadow: "0 0 0 3px rgba(102, 126, 234, 0.1)", + boxShadow: "0 0 0 4px rgba(102, 126, 234, 0.1)", + backgroundColor: "#ffffff", }, }, "& .MuiOutlinedInput-input": { - color: "#0f172a", + color: "#1e293b", fontWeight: 600, - fontSize: "0.9375rem", + fontSize: "1rem", }, "& .MuiFormHelperText-root": { color: duration > 10 ? "#dc2626" : "#64748b", fontSize: "0.75rem", - mt: 0.75, + mt: 1, + fontWeight: 500, }, }} /> @@ -92,7 +145,7 @@ export const PodcastConfiguration: React.FC = ({ {/* Speakers Toggle */} - + Number of Speakers = ({ fullWidth size="small" sx={{ - backgroundColor: "#ffffff", - border: "1px solid rgba(15, 23, 42, 0.12)", + backgroundColor: "#f8fafc", + border: "2px solid rgba(102, 126, 234, 0.2)", borderRadius: 2, p: 0.5, "& .MuiToggleButton-root": { @@ -116,14 +169,15 @@ export const PodcastConfiguration: React.FC = ({ py: 1, transition: "all 0.2s ease", "&:hover": { - backgroundColor: alpha("#64748b", 0.05), + backgroundColor: alpha("#667eea", 0.08), }, "&.Mui-selected": { - backgroundColor: alpha("#667eea", 0.1), - color: "#667eea", + background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + color: "#ffffff", fontWeight: 600, + boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)", "&:hover": { - backgroundColor: alpha("#667eea", 0.15), + background: "linear-gradient(135deg, #764ba2 0%, #667eea 100%)", }, }, }, @@ -142,7 +196,7 @@ export const PodcastConfiguration: React.FC = ({ - + {speakers === 1 ? "Single host format" : "Host and guest conversation"} diff --git a/frontend/src/components/PodcastMaker/CreateStep/TopicUrlInput.tsx b/frontend/src/components/PodcastMaker/CreateStep/TopicUrlInput.tsx index 35b4eb62..06b4479a 100644 --- a/frontend/src/components/PodcastMaker/CreateStep/TopicUrlInput.tsx +++ b/frontend/src/components/PodcastMaker/CreateStep/TopicUrlInput.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha } from "@mui/material"; -import { AutoAwesome as AutoAwesomeIcon } from "@mui/icons-material"; +import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha, Stack, Chip } from "@mui/material"; +import { AutoAwesome as AutoAwesomeIcon, AttachMoney as AttachMoneyIcon } from "@mui/icons-material"; +import { Knobs } from "../types"; export const TOPIC_PLACEHOLDERS = [ "Industry insights: Latest trends in AI for Content Marketing", @@ -20,6 +21,16 @@ interface TopicUrlInputProps { placeholderIndex: number; loading?: boolean; loadingMessage?: string; + estimatedCost?: { + ttsCost: number; + avatarCost: number; + videoCost: number; + researchCost: number; + total: number; + }; + duration?: number; + speakers?: number; + knobs?: Knobs; } export const TopicUrlInput: React.FC = ({ @@ -31,23 +42,137 @@ export const TopicUrlInput: React.FC = ({ placeholderIndex, loading = false, loadingMessage, + estimatedCost, + duration = 1, + speakers = 1, + knobs, }) => { return ( - - Topic Idea or Blog URL - + + + + 1 + + + + + + Enter Podcast Topic or Blog URL + + + + {estimatedCost && ( + + + Estimated Cost Breakdown: + + + • Audio Generation: ${estimatedCost.ttsCost}
+ • Avatar Creation: ${estimatedCost.avatarCost}
+ • Video Rendering: ${estimatedCost.videoCost}
+ • Research: ${estimatedCost.researchCost}
+ + Total: ${estimatedCost.total} + + + Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs?.bitrate === "hd" ? "HD" : "standard"} quality + +
+
+ } + arrow + placement="top" + componentsProps={{ + tooltip: { + sx: { + bgcolor: "#0f172a", + color: "#ffffff", + maxWidth: 280, + fontSize: "0.875rem", + p: 1.5, + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + }, + }, + arrow: { + sx: { + color: "#0f172a", + }, + }, + }} + > + } + label={`Est. $${estimatedCost.total}`} + size="small" + sx={{ + background: "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)", + color: "#059669", + fontWeight: 600, + border: "1px solid rgba(16, 185, 129, 0.2)", + fontSize: "0.75rem", + height: 26, + cursor: "help", + }} + /> + + )} +
= ({ inputProps={{ sx: { "&::placeholder": { color: "#94a3b8", opacity: 1 }, - color: "#0f172a", + color: "#1e293b", }, }} value={value} @@ -78,35 +203,41 @@ export const TopicUrlInput: React.FC = ({ } sx={{ "& .MuiOutlinedInput-root": { - backgroundColor: "#ffffff", - border: "1.5px solid rgba(15, 23, 42, 0.12)", + backgroundColor: "#f8fafc", + border: "2px solid rgba(102, 126, 234, 0.2)", borderRadius: 2, + fontSize: "1rem", + transition: "all 0.2s ease", "&:hover": { backgroundColor: "#ffffff", borderColor: "rgba(102, 126, 234, 0.4)", + boxShadow: "0 2px 8px rgba(102, 126, 234, 0.1)", }, "&.Mui-focused": { backgroundColor: "#ffffff", - borderColor: isUrl ? "#10b981" : "#667eea", // Green for URL, Blue for Topic + borderColor: isUrl ? "#10b981" : "#667eea", borderWidth: 2, + boxShadow: isUrl + ? "0 0 0 4px rgba(16, 185, 129, 0.1)" + : "0 0 0 4px rgba(102, 126, 234, 0.1)", }, }, "& .MuiOutlinedInput-input": { - fontSize: "0.9375rem", - lineHeight: 1.6, - color: "#0f172a", - fontWeight: 400, - }, - "& .MuiInputBase-input::placeholder": { - color: "#94a3b8", - opacity: 1, - fontWeight: 400, + fontSize: "1rem", + lineHeight: 1.7, + color: "#1e293b", + fontWeight: 500, + "&::placeholder": { + color: "#64748b", + opacity: 1, + fontWeight: 400, + }, }, "& .MuiFormHelperText-root": { color: isUrl ? "#059669" : "#64748b", fontSize: "0.8125rem", - fontWeight: 400, - mt: 0.75, + fontWeight: 500, + mt: 1, }, }} /> @@ -114,7 +245,7 @@ export const TopicUrlInput: React.FC = ({ {/* Enhance topic with AI button - appears when user types (and not a URL) */} {showAIDetailsButton && !isUrl && ( - + @@ -283,14 +387,17 @@ export const VoiceSelector: React.FC = ({ )} - - System Voices + + System Voices {voiceOptions.filter(v => !v.isCustom).map((voice) => ( - + + + + {voice.name}} secondary={voice.personality?.split(' - ')[0]} /> @@ -299,23 +406,146 @@ export const VoiceSelector: React.FC = ({ {selectedVoice?.personality && ( - - {selectedVoice.personality} - + + + Voice Personality: {selectedVoice.personality} + + )} {showVoiceClone && !voiceClone?.success && ( - - - - - Don't see your voice? Go to Onboarding → Voice Cloning to create your custom voice clone. - - + + + + + + + + {voiceCreated && ( + + + + + Voice Clone Created Successfully! + + + + + Your custom voice clone is ready. Would you like to use this voice for your podcast? + + + setUseCreatedVoice(e.target.checked)} + sx={{ + color: "#10b981", + "&.Mui-checked": { color: "#10b981" }, + }} + /> + } + label={ + + Use this voice for my podcast + + } + /> + + + + + + + + + )} + + )} ); }; -export default VoiceSelector; +export default VoiceSelector; \ No newline at end of file