Feat: Podcast maker UI improvements and voice clone panel

- Add podcast feature to step4_assets router for podcast mode
- Enhance analysis tab navigation with gradient styling
- Move cost estimate display to TopicUrlInput component
- Add voice clone panel with toggle and preview functionality
- Improve podcast dashboard header with gradient background
- Add step indicator and improved styling to TopicUrlInput
- Update AvatarSelector with refined styling
- Enhance PodcastConfiguration with better layout
- Improve Header component with gradient and shadow effects
This commit is contained in:
ajaysi
2026-04-07 06:41:53 +05:30
parent 3f1d5cbb09
commit 23bf709c10
10 changed files with 793 additions and 328 deletions

View File

@@ -16,7 +16,7 @@ CORE_ROUTER_REGISTRY = [
{"name": "component_logic", "module": "api.component_logic", "attr": "router", "features": {"all", "core"}}, {"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": "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": "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": "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": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "features": {"all", "core", "seo"}},
{"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "features": {"all", "core"}}, {"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "features": {"all", "core"}},
@@ -116,10 +116,6 @@ class RouterManager:
if "all" in enabled_features: if "all" in enabled_features:
return True 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 no required features specified, include by default
if not required_features: if not required_features:
return True return True

View File

@@ -26,20 +26,28 @@ export const ANALYSIS_TABS: TabConfig[] = [
const getTabButtonStyles = (isActive: boolean) => ({ const getTabButtonStyles = (isActive: boolean) => ({
background: isActive background: isActive
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" ? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
: "transparent", : "rgba(255, 255, 255, 0.8)",
color: isActive ? "#fff" : "#64748b", color: isActive ? "#fff" : "#475569",
border: isActive ? "none" : "1px solid rgba(0,0,0,0.1)", border: isActive ? "none" : "1px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2, borderRadius: 2,
px: 2, px: 2.5,
py: 1, py: 1.25,
fontSize: "0.75rem", fontSize: "0.8125rem",
fontWeight: 600, fontWeight: 600,
textTransform: "none" as const, textTransform: "none" as const,
transition: "all 0.2s ease", 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": { "&:hover": {
background: isActive background: isActive
? "linear-gradient(135deg, #764ba2 0%, #667eea 100%)" ? "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<AnalysisTabNavProps> = ({ activeTab, onTabChange }) => { export const AnalysisTabNav: React.FC<AnalysisTabNavProps> = ({ activeTab, onTabChange }) => {
return ( return (
<Stack direction="row" flexWrap="wrap" gap={1}> <Box
{ANALYSIS_TABS.map((tab) => ( sx={{
<Button background: "linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, rgba(118, 75, 162, 0.04) 100%)",
key={tab.id} borderRadius: 2.5,
onClick={() => onTabChange(tab.id)} p: 1.5,
startIcon={tab.icon} border: "1px solid rgba(102, 126, 234, 0.1)",
sx={getTabButtonStyles(activeTab === tab.id)} }}
> >
{tab.label} <Stack direction="row" flexWrap="wrap" gap={1}>
</Button> {ANALYSIS_TABS.map((tab) => (
))} <Button
</Stack> key={tab.id}
onClick={() => onTabChange(tab.id)}
startIcon={tab.icon}
sx={getTabButtonStyles(activeTab === tab.id)}
>
{tab.label}
</Button>
))}
</Stack>
</Box>
); );
}; };

View File

@@ -8,7 +8,6 @@ import { getLatestBrandAvatar } from "../../api/brandAssets";
import { VoiceSelector } from "../shared/VoiceSelector"; import { VoiceSelector } from "../shared/VoiceSelector";
// Imported Components // Imported Components
import { CreateHeader } from "./CreateStep/CreateHeader";
import { TopicUrlInput, TOPIC_PLACEHOLDERS } from "./CreateStep/TopicUrlInput"; import { TopicUrlInput, TOPIC_PLACEHOLDERS } from "./CreateStep/TopicUrlInput";
import { PodcastConfiguration } from "./CreateStep/PodcastConfiguration"; import { PodcastConfiguration } from "./CreateStep/PodcastConfiguration";
import { AvatarSelector } from "./CreateStep/AvatarSelector"; import { AvatarSelector } from "./CreateStep/AvatarSelector";
@@ -280,7 +279,13 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
brandAvatarBlobUrl // Brand avatar blob URL 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 () => { const submit = async () => {
if (!canSubmit || isSubmitting) return; if (!canSubmit || isSubmitting) return;
@@ -521,14 +526,6 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
}} }}
> >
<Stack spacing={3.5}> <Stack spacing={3.5}>
<CreateHeader
subscription={subscription}
duration={duration}
speakers={speakers}
knobs={knobs}
estimatedCost={estimatedCost}
/>
<Stack direction={{ xs: "column", md: "row" }} spacing={3} alignItems="stretch"> <Stack direction={{ xs: "column", md: "row" }} spacing={3} alignItems="stretch">
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<TopicUrlInput <TopicUrlInput
@@ -540,6 +537,10 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
placeholderIndex={placeholderIndex} placeholderIndex={placeholderIndex}
loading={enhancingTopic} loading={enhancingTopic}
loadingMessage={enhanceTopicMessage} loadingMessage={enhanceTopicMessage}
estimatedCost={estimatedCost}
duration={duration}
speakers={speakers}
knobs={knobs}
/> />
</Box> </Box>

View File

@@ -69,7 +69,21 @@ export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
}} }}
> >
<Stack direction={{ xs: "column", sm: "row" }} spacing={{ xs: 1, sm: 1.5 }} alignItems={{ xs: "flex-start", sm: "center" }} sx={{ mb: 2 }}> <Stack direction={{ xs: "column", sm: "row" }} spacing={{ xs: 1, sm: 1.5 }} alignItems={{ xs: "flex-start", sm: "center" }} sx={{ mb: 2 }}>
<Stack direction="row" spacing={1.5} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
<Box
sx={{
width: 24,
height: 24,
borderRadius: "50%",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
}}
>
<Typography sx={{ color: "#fff", fontSize: "0.75rem", fontWeight: 700 }}>3</Typography>
</Box>
<Box <Box
sx={{ sx={{
width: 36, width: 36,
@@ -439,30 +453,16 @@ export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
sx={{ sx={{
p: { xs: 1, sm: 1.5 }, p: { xs: 1, sm: 1.5 },
borderRadius: 1.5, borderRadius: 1.5,
background: alpha("#f8fafc", 0.8), background: alpha("#f0f4ff", 0.6),
border: "1px solid rgba(15, 23, 42, 0.1)", border: "1px solid rgba(99, 102, 241, 0.2)",
}} }}
> >
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.875rem" }, fontWeight: 600, mb: 0.5, display: "flex", alignItems: "center", gap: 0.5 }}> <Stack direction="row" alignItems="center" spacing={0.5}>
<PhotoCameraIcon fontSize="small" sx={{ color: "#64748b" }} /> <PhotoCameraIcon fontSize="small" sx={{ color: "#667eea" }} />
Take a Selfie <Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, fontWeight: 600 }}>
</Typography> Capture a photo using your device camera and use "Make Presentable" to enhance it. Camera access required.
<Typography variant="body2" sx={{ color: "#475569", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, lineHeight: 1.6 }}> </Typography>
Capture a photo using your device camera and use <strong>"Make Presentable"</strong> to enhance it into a professional presenter using AI. </Stack>
</Typography>
</Box>
<Box
sx={{
p: { xs: 1, sm: 1.5 },
borderRadius: 1.5,
background: alpha("#f0f4ff", 0.5),
border: "1px solid rgba(99, 102, 241, 0.15)",
}}
>
<Typography variant="caption" sx={{ color: "#6366f1", fontSize: { xs: "0.7rem", sm: "0.8125rem" }, fontWeight: 500, display: "flex", alignItems: "center", gap: 0.5 }}>
<InfoIcon fontSize="inherit" />
Camera access required for selfie capture
</Typography>
</Box> </Box>
</Stack> </Stack>
)} )}
@@ -570,30 +570,16 @@ export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
sx={{ sx={{
p: { xs: 1, sm: 1.5 }, p: { xs: 1, sm: 1.5 },
borderRadius: 1.5, borderRadius: 1.5,
background: alpha("#f8fafc", 0.8), background: alpha("#f0f4ff", 0.6),
border: "1px solid rgba(15, 23, 42, 0.1)", border: "1px solid rgba(99, 102, 241, 0.2)",
}} }}
> >
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.875rem" }, fontWeight: 600, mb: 0.5, display: "flex", alignItems: "center", gap: 0.5 }}> <Stack direction="row" alignItems="center" spacing={0.5}>
<CloudUploadIcon fontSize="small" sx={{ color: "#64748b" }} /> <CloudUploadIcon fontSize="small" sx={{ color: "#667eea" }} />
Upload Your Photo <Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, fontWeight: 600 }}>
</Typography> Upload a photo and use "Make Presentable" to enhance it into a professional presenter. Supported: JPG, PNG, WebP (max 5MB)
<Typography variant="body2" sx={{ color: "#475569", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, lineHeight: 1.6 }}> </Typography>
Upload a new photo and use <strong>"Make Presentable"</strong> to enhance it into a professional presenter using AI. </Stack>
</Typography>
</Box>
<Box
sx={{
p: { xs: 1, sm: 1.5 },
borderRadius: 1.5,
background: alpha("#f0f4ff", 0.5),
border: "1px solid rgba(99, 102, 241, 0.15)",
}}
>
<Typography variant="caption" sx={{ color: "#6366f1", fontSize: { xs: "0.7rem", sm: "0.8125rem" }, fontWeight: 500, display: "flex", alignItems: "center", gap: 0.5 }}>
<InfoIcon fontSize="inherit" />
Supported formats: JPG, PNG, WebP (max 5MB)
</Typography>
</Box> </Box>
</Stack> </Stack>
)} )}

View File

@@ -225,7 +225,7 @@ export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, can
disabled={!canSubmit || isSubmitting} disabled={!canSubmit || isSubmitting}
loading={isSubmitting} loading={isSubmitting}
startIcon={<AutoAwesomeIcon />} startIcon={<AutoAwesomeIcon />}
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"} {isSubmitting ? "Analyzing..." : "Analyze & Continue"}
</PrimaryButton> </PrimaryButton>

View File

@@ -29,32 +29,53 @@ export const CreateHeader: React.FC<CreateHeaderProps> = ({
estimatedCost, estimatedCost,
}) => { }) => {
return ( return (
<Stack direction="row" spacing={2} alignItems="flex-start" justifyContent="space-between" flexWrap="wrap" gap={2}> <Box
<Stack direction="row" spacing={2} alignItems="flex-start" sx={{ flex: 1, minWidth: { xs: "100%", md: "60%" } }}> sx={{
<Box width: "100%",
sx={{ background: "linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%)",
width: 48, borderRadius: 3,
height: 48, p: { xs: 2, md: 2.5 },
borderRadius: 2, border: "1px solid rgba(102, 126, 234, 0.15)",
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)", position: "relative",
display: "flex", overflow: "hidden",
alignItems: "center", "&::before": {
justifyContent: "center", content: '""',
flexShrink: 0, position: "absolute",
}} top: 0,
> left: 0,
<AutoAwesomeIcon sx={{ color: "#667eea", fontSize: "1.75rem" }} /> right: 0,
</Box> height: "3px",
<Box sx={{ flex: 1 }}> background: "linear-gradient(90deg, #667eea 0%, #764ba2 50%, #f093fb 100%)",
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}> },
}}
>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}>
<Stack direction="row" spacing={1.5} alignItems="center">
<Box
sx={{
width: 40,
height: 40,
borderRadius: 2,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)",
}}
>
<AutoAwesomeIcon sx={{ color: "#fff", fontSize: "1.25rem" }} />
</Box>
<Stack direction="row" spacing={1} alignItems="center">
<Typography <Typography
variant="h5" variant="h6"
sx={{ sx={{
color: "#0f172a", background: "linear-gradient(135deg, #1e293b 0%, #334155 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
fontWeight: 700, fontWeight: 700,
fontSize: { xs: "1.5rem", md: "1.75rem" }, fontSize: { xs: "1.125rem", md: "1.25rem" },
letterSpacing: "-0.02em", letterSpacing: "-0.02em",
lineHeight: 1.2,
}} }}
> >
Create New Podcast Episode Create New Podcast Episode
@@ -106,122 +127,122 @@ export const CreateHeader: React.FC<CreateHeaderProps> = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Stack> </Stack>
</Box> </Stack>
</Stack> <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ alignItems: "center" }}>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ alignItems: "center" }}> <Tooltip
<Tooltip title={`Your current subscription plan: ${subscription?.tier || "free"}. Upgrade for more features.`}
title={`Your current subscription plan: ${subscription?.tier || "free"}. Upgrade for more features.`} arrow
arrow placement="top"
placement="top" >
> <Chip
<Chip label={`Plan: ${subscription?.tier || "free"}`}
label={`Plan: ${subscription?.tier || "free"}`} size="small"
size="small" sx={{
sx={{ background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)", color: "#667eea",
color: "#667eea", fontWeight: 600,
fontWeight: 600, border: "1px solid rgba(102, 126, 234, 0.2)",
border: "1px solid rgba(102, 126, 234, 0.2)", fontSize: "0.75rem",
fontSize: "0.75rem", height: 26,
height: 26, cursor: "help",
cursor: "help", }}
}} />
/> </Tooltip>
</Tooltip> <Tooltip
<Tooltip title={`Podcast duration: ${duration} minutes. Maximum duration is 10 minutes. Recommended: 5-10 minutes for best results.`}
title={`Podcast duration: ${duration} minutes. Maximum duration is 10 minutes. Recommended: 5-10 minutes for best results.`} arrow
arrow placement="top"
placement="top" >
> <Chip
<Chip label={`Duration: ${duration} min`}
label={`Duration: ${duration} min`} size="small"
size="small" sx={{
sx={{ background: alpha("#0f172a", 0.06),
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",
}}
/>
</Tooltip>
<Tooltip
title={`Number of speakers: ${speakers}. Supports 1-2 speakers. Each additional speaker adds avatar generation cost.`}
arrow
placement="top"
>
<Chip
label={`${speakers} speaker${speakers > 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",
}}
/>
</Tooltip>
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost Breakdown:
</Typography>
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
Audio Generation: ${estimatedCost.ttsCost}<br />
Avatar Creation: ${estimatedCost.avatarCost}<br />
Video Rendering: ${estimatedCost.videoCost}<br />
Research: ${estimatedCost.researchCost}<br />
<Typography variant="body2" sx={{ fontWeight: 600, mt: 0.5, pt: 0.5, borderTop: "1px solid rgba(255,255,255,0.2)" }}>
Total: ${estimatedCost.total}
</Typography>
<Typography variant="caption" sx={{ fontSize: "0.75rem", opacity: 0.9, mt: 0.5, display: "block" }}>
Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs.bitrate === "hd" ? "HD" : "standard"} quality
</Typography>
</Typography>
</Box>
}
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", color: "#0f172a",
fontWeight: 600,
border: "1px solid rgba(15, 23, 42, 0.12)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
/>
</Tooltip>
<Tooltip
title={`Number of speakers: ${speakers}. Supports 1-2 speakers. Each additional speaker adds avatar generation cost.`}
arrow
placement="top"
>
<Chip
label={`${speakers} speaker${speakers > 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",
}}
/>
</Tooltip>
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost Breakdown:
</Typography>
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
Audio Generation: ${estimatedCost.ttsCost}<br />
Avatar Creation: ${estimatedCost.avatarCost}<br />
Video Rendering: ${estimatedCost.videoCost}<br />
Research: ${estimatedCost.researchCost}<br />
<Typography variant="body2" sx={{ fontWeight: 600, mt: 0.5, pt: 0.5, borderTop: "1px solid rgba(255,255,255,0.2)" }}>
Total: ${estimatedCost.total}
</Typography>
<Typography variant="caption" sx={{ fontSize: "0.75rem", opacity: 0.9, mt: 0.5, display: "block" }}>
Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs.bitrate === "hd" ? "HD" : "standard"} quality
</Typography>
</Typography>
</Box>
}
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",
},
}, },
},
}}
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
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",
}} }}
/> >
</Tooltip> <Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
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",
}}
/>
</Tooltip>
</Stack>
</Stack> </Stack>
</Stack> </Box>
); );
}; };

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Stack, Box, Typography, TextField, ToggleButton, ToggleButtonGroup, alpha } from "@mui/material"; 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 { interface PodcastConfigurationProps {
duration: number; duration: number;
@@ -35,22 +35,72 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
flex: { xs: "1 1 auto", lg: "0 0 320px" }, flex: { xs: "1 1 auto", lg: "0 0 320px" },
width: { xs: "100%", lg: "320px" }, width: { xs: "100%", lg: "320px" },
p: 3, p: 3,
borderRadius: 2, borderRadius: 3,
background: alpha("#f8fafc", 0.5), background: "#ffffff",
border: "1px solid rgba(15, 23, 42, 0.06)", border: "1px solid rgba(102, 126, 234, 0.15)",
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", 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%)",
},
}} }}
> >
<Typography variant="subtitle2" sx={{ mb: 2.5, color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}> <Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2.5 }}>
Basic Configuration <Box
</Typography> sx={{
width: 24,
height: 24,
borderRadius: "50%",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
}}
>
<Typography sx={{ color: "#fff", fontSize: "0.75rem", fontWeight: 700 }}>2</Typography>
</Box>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
}}
>
<SettingsIcon sx={{ color: "#667eea", fontSize: "1rem" }} />
</Box>
<Typography
variant="subtitle1"
sx={{
color: "#0f172a",
fontWeight: 700,
fontSize: "1rem",
letterSpacing: "-0.01em",
}}
>
Basic Configuration
</Typography>
</Stack>
<Stack spacing={3}> <Stack spacing={3}>
{/* Duration Input */} {/* Duration Input */}
<Box> <Box>
<Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 500 }}> <Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 600, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Duration (minutes) Duration (minutes)
</Typography> </Typography>
<TextField <TextField
@@ -64,27 +114,30 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
fullWidth fullWidth
sx={{ sx={{
"& .MuiOutlinedInput-root": { "& .MuiOutlinedInput-root": {
backgroundColor: "#ffffff", backgroundColor: "#f8fafc",
border: "1px solid rgba(15, 23, 42, 0.12)", border: "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2, borderRadius: 2,
transition: "all 0.2s", transition: "all 0.2s",
"&:hover": { "&: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": { "&.Mui-focused": {
borderColor: "#667eea", 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": { "& .MuiOutlinedInput-input": {
color: "#0f172a", color: "#1e293b",
fontWeight: 600, fontWeight: 600,
fontSize: "0.9375rem", fontSize: "1rem",
}, },
"& .MuiFormHelperText-root": { "& .MuiFormHelperText-root": {
color: duration > 10 ? "#dc2626" : "#64748b", color: duration > 10 ? "#dc2626" : "#64748b",
fontSize: "0.75rem", fontSize: "0.75rem",
mt: 0.75, mt: 1,
fontWeight: 500,
}, },
}} }}
/> />
@@ -92,7 +145,7 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
{/* Speakers Toggle */} {/* Speakers Toggle */}
<Box> <Box>
<Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 500 }}> <Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 600, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Number of Speakers Number of Speakers
</Typography> </Typography>
<ToggleButtonGroup <ToggleButtonGroup
@@ -102,8 +155,8 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
fullWidth fullWidth
size="small" size="small"
sx={{ sx={{
backgroundColor: "#ffffff", backgroundColor: "#f8fafc",
border: "1px solid rgba(15, 23, 42, 0.12)", border: "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2, borderRadius: 2,
p: 0.5, p: 0.5,
"& .MuiToggleButton-root": { "& .MuiToggleButton-root": {
@@ -116,14 +169,15 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
py: 1, py: 1,
transition: "all 0.2s ease", transition: "all 0.2s ease",
"&:hover": { "&:hover": {
backgroundColor: alpha("#64748b", 0.05), backgroundColor: alpha("#667eea", 0.08),
}, },
"&.Mui-selected": { "&.Mui-selected": {
backgroundColor: alpha("#667eea", 0.1), background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "#667eea", color: "#ffffff",
fontWeight: 600, fontWeight: 600,
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)",
"&:hover": { "&:hover": {
backgroundColor: alpha("#667eea", 0.15), background: "linear-gradient(135deg, #764ba2 0%, #667eea 100%)",
}, },
}, },
}, },
@@ -142,7 +196,7 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
</Stack> </Stack>
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
<Typography variant="caption" sx={{ display: "block", mt: 0.75, color: "#64748b", fontSize: "0.75rem" }}> <Typography variant="caption" sx={{ display: "block", mt: 1, color: "#64748b", fontSize: "0.75rem", fontWeight: 500 }}>
{speakers === 1 ? "Single host format" : "Host and guest conversation"} {speakers === 1 ? "Single host format" : "Host and guest conversation"}
</Typography> </Typography>
</Box> </Box>

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha } from "@mui/material"; import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha, Stack, Chip } from "@mui/material";
import { AutoAwesome as AutoAwesomeIcon } from "@mui/icons-material"; import { AutoAwesome as AutoAwesomeIcon, AttachMoney as AttachMoneyIcon } from "@mui/icons-material";
import { Knobs } from "../types";
export const TOPIC_PLACEHOLDERS = [ export const TOPIC_PLACEHOLDERS = [
"Industry insights: Latest trends in AI for Content Marketing", "Industry insights: Latest trends in AI for Content Marketing",
@@ -20,6 +21,16 @@ interface TopicUrlInputProps {
placeholderIndex: number; placeholderIndex: number;
loading?: boolean; loading?: boolean;
loadingMessage?: string; loadingMessage?: string;
estimatedCost?: {
ttsCost: number;
avatarCost: number;
videoCost: number;
researchCost: number;
total: number;
};
duration?: number;
speakers?: number;
knobs?: Knobs;
} }
export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
@@ -31,23 +42,137 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
placeholderIndex, placeholderIndex,
loading = false, loading = false,
loadingMessage, loadingMessage,
estimatedCost,
duration = 1,
speakers = 1,
knobs,
}) => { }) => {
return ( return (
<Box <Box
sx={{ sx={{
p: 3, p: 3,
borderRadius: 2, borderRadius: 3,
background: alpha("#f8fafc", 0.5), background: "#ffffff",
border: "1px solid rgba(15, 23, 42, 0.06)", border: "1px solid rgba(102, 126, 234, 0.15)",
height: "100%", // Fill height of parent height: "100%",
display: "flex", display: "flex",
flexDirection: "column", 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%)",
},
}} }}
> >
<Box flex={1} display="flex" flexDirection="column"> <Box flex={1} display="flex" flexDirection="column">
<Typography variant="subtitle2" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}> <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
Topic Idea or Blog URL <Stack direction="row" alignItems="center" spacing={1}>
</Typography> <Box
sx={{
width: 24,
height: 24,
borderRadius: "50%",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
}}
>
<Typography sx={{ color: "#fff", fontSize: "0.75rem", fontWeight: 700 }}>1</Typography>
</Box>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
}}
>
<AutoAwesomeIcon sx={{ color: "#667eea", fontSize: "1rem" }} />
</Box>
<Typography
variant="subtitle1"
sx={{
color: "#0f172a",
fontWeight: 700,
fontSize: "1rem",
letterSpacing: "-0.01em",
}}
>
Enter Podcast Topic or Blog URL
</Typography>
</Stack>
{estimatedCost && (
<Tooltip
title={
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost Breakdown:
</Typography>
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
Audio Generation: ${estimatedCost.ttsCost}<br />
Avatar Creation: ${estimatedCost.avatarCost}<br />
Video Rendering: ${estimatedCost.videoCost}<br />
Research: ${estimatedCost.researchCost}<br />
<Typography variant="body2" sx={{ fontWeight: 600, mt: 0.5, pt: 0.5, borderTop: "1px solid rgba(255,255,255,0.2)" }}>
Total: ${estimatedCost.total}
</Typography>
<Typography variant="caption" sx={{ fontSize: "0.75rem", opacity: 0.9, mt: 0.5, display: "block" }}>
Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs?.bitrate === "hd" ? "HD" : "standard"} quality
</Typography>
</Typography>
</Box>
}
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",
},
},
}}
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
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",
}}
/>
</Tooltip>
)}
</Stack>
<Tooltip <Tooltip
title={ title={
isUrl isUrl
@@ -65,7 +190,7 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
inputProps={{ inputProps={{
sx: { sx: {
"&::placeholder": { color: "#94a3b8", opacity: 1 }, "&::placeholder": { color: "#94a3b8", opacity: 1 },
color: "#0f172a", color: "#1e293b",
}, },
}} }}
value={value} value={value}
@@ -78,35 +203,41 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
} }
sx={{ sx={{
"& .MuiOutlinedInput-root": { "& .MuiOutlinedInput-root": {
backgroundColor: "#ffffff", backgroundColor: "#f8fafc",
border: "1.5px solid rgba(15, 23, 42, 0.12)", border: "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2, borderRadius: 2,
fontSize: "1rem",
transition: "all 0.2s ease",
"&:hover": { "&:hover": {
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
borderColor: "rgba(102, 126, 234, 0.4)", borderColor: "rgba(102, 126, 234, 0.4)",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.1)",
}, },
"&.Mui-focused": { "&.Mui-focused": {
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
borderColor: isUrl ? "#10b981" : "#667eea", // Green for URL, Blue for Topic borderColor: isUrl ? "#10b981" : "#667eea",
borderWidth: 2, 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": { "& .MuiOutlinedInput-input": {
fontSize: "0.9375rem", fontSize: "1rem",
lineHeight: 1.6, lineHeight: 1.7,
color: "#0f172a", color: "#1e293b",
fontWeight: 400, fontWeight: 500,
}, "&::placeholder": {
"& .MuiInputBase-input::placeholder": { color: "#64748b",
color: "#94a3b8", opacity: 1,
opacity: 1, fontWeight: 400,
fontWeight: 400, },
}, },
"& .MuiFormHelperText-root": { "& .MuiFormHelperText-root": {
color: isUrl ? "#059669" : "#64748b", color: isUrl ? "#059669" : "#64748b",
fontSize: "0.8125rem", fontSize: "0.8125rem",
fontWeight: 400, fontWeight: 500,
mt: 0.75, mt: 1,
}, },
}} }}
/> />
@@ -114,7 +245,7 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
{/* Enhance topic with AI button - appears when user types (and not a URL) */} {/* Enhance topic with AI button - appears when user types (and not a URL) */}
{showAIDetailsButton && !isUrl && ( {showAIDetailsButton && !isUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1, flexDirection: "column", alignItems: "flex-end", gap: 0.6 }}> <Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5, flexDirection: "column", alignItems: "flex-end", gap: 0.6 }}>
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
@@ -133,7 +264,8 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
fontWeight: 600, fontWeight: 600,
borderRadius: 2.5, borderRadius: 2.5,
color: "#f8fbff", color: "#f8fbff",
px: 1.8, px: 2,
py: 0.75,
border: "1px solid rgba(148, 211, 255, 0.6)", border: "1px solid rgba(148, 211, 255, 0.6)",
background: "linear-gradient(120deg, #0ea5e9 0%, #2563eb 55%, #1d4ed8 100%)", background: "linear-gradient(120deg, #0ea5e9 0%, #2563eb 55%, #1d4ed8 100%)",
boxShadow: "0 8px 18px rgba(37, 99, 235, 0.28), inset 0 1px 0 rgba(255,255,255,0.22)", boxShadow: "0 8px 18px rgba(37, 99, 235, 0.28), inset 0 1px 0 rgba(255,255,255,0.22)",

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Stack, Typography } from "@mui/material"; import { Stack, Typography, Box } from "@mui/material";
import { import {
Mic as MicIcon, Mic as MicIcon,
Info as InfoIcon, Info as InfoIcon,
@@ -19,7 +19,27 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Stack sx={{ width: "100%", minWidth: 0 }} spacing={1.5}> <Box
sx={{
width: "100%",
minWidth: 0,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%)",
borderRadius: 3,
p: { xs: 2, md: 2.5 },
border: "1px solid rgba(102, 126, 234, 0.15)",
position: "relative",
overflow: "hidden",
"&::before": {
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "3px",
background: "linear-gradient(90deg, #667eea 0%, #764ba2 50%, #f093fb 100%)",
},
}}
>
<Stack <Stack
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
@@ -27,20 +47,35 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
flexWrap="wrap" flexWrap="wrap"
gap={2} gap={2}
> >
<Typography <Stack direction="row" alignItems="center" gap={1.5}>
variant="h3" <Box
sx={{ sx={{
color: "#1e293b", width: 44,
fontWeight: 800, height: 44,
display: "flex", borderRadius: 2,
alignItems: "center", background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
gap: 1.5, display: "flex",
fontSize: { xs: "1.5rem", md: "2rem" }, alignItems: "center",
}} justifyContent: "center",
> boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)",
<MicIcon fontSize="large" sx={{ color: "#667eea" }} /> }}
AI Podcast Maker >
</Typography> <MicIcon sx={{ color: "#fff", fontSize: 24 }} />
</Box>
<Typography
variant="h5"
sx={{
background: "linear-gradient(135deg, #1e293b 0%, #334155 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
fontWeight: 700,
fontSize: { xs: "1.25rem", md: "1.5rem" },
letterSpacing: "-0.02em",
}}
>
ALwrity Podcast Maker
</Typography>
</Stack>
<Stack <Stack
direction="row" direction="row"
spacing={1} spacing={1}
@@ -114,14 +149,7 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
</PrimaryButton> </PrimaryButton>
</Stack> </Stack>
</Stack> </Stack>
<Typography </Box>
variant="body2"
color="text.secondary"
sx={{ display: { xs: "none", sm: "block" } }}
>
Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration
</Typography>
</Stack>
); );
}; };

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from "react"; import React, { useState, useEffect, useMemo, useCallback } from "react";
import { import {
Box, Box,
Typography, Typography,
@@ -15,17 +15,23 @@ import {
IconButton, IconButton,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
Collapse,
FormControlLabel,
Checkbox,
Divider,
} from "@mui/material"; } from "@mui/material";
import { import {
Mic, Mic,
PlayArrow, PlayArrow,
Pause, Pause,
CloudUpload,
HelpOutline, HelpOutline,
AutoAwesome, AutoAwesome,
CheckCircle, CheckCircle,
ExpandLess,
ExpandMore,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { getLatestVoiceClone, VoiceCloneResponse } from "../../api/brandAssets"; import { getLatestVoiceClone, VoiceCloneResponse } from "../../api/brandAssets";
import { VoiceAvatarPlaceholder } from "../OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder";
export type VoiceOption = { export type VoiceOption = {
id: string; id: string;
@@ -75,6 +81,23 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
const [voiceClone, setVoiceClone] = useState<VoiceCloneResponse | null>(null); const [voiceClone, setVoiceClone] = useState<VoiceCloneResponse | null>(null);
const [loadingVoiceClone, setLoadingVoiceClone] = useState(false); const [loadingVoiceClone, setLoadingVoiceClone] = useState(false);
const [playingPreview, setPlayingPreview] = useState<string | null>(null); const [playingPreview, setPlayingPreview] = useState<string | null>(null);
const [showVoiceClonePanel, setShowVoiceClonePanel] = useState(false);
const [voiceCreated, setVoiceCreated] = useState(false);
const [useCreatedVoice, setUseCreatedVoice] = useState(true);
const fetchVoiceClone = async () => {
try {
setLoadingVoiceClone(true);
const result = await getLatestVoiceClone();
setVoiceClone(result);
return result;
} catch (error) {
console.error("Failed to fetch voice clone:", error);
return null;
} finally {
setLoadingVoiceClone(false);
}
};
const voiceOptions = useMemo(() => { const voiceOptions = useMemo(() => {
const options: VoiceOption[] = [...PREDEFINED_VOICES]; const options: VoiceOption[] = [...PREDEFINED_VOICES];
@@ -101,19 +124,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
useEffect(() => { useEffect(() => {
if (!showVoiceClone) return; if (!showVoiceClone) return;
const fetchVoiceClone = async () => {
try {
setLoadingVoiceClone(true);
const result = await getLatestVoiceClone();
setVoiceClone(result);
} catch (error) {
console.error("Failed to fetch voice clone:", error);
} finally {
setLoadingVoiceClone(false);
}
};
fetchVoiceClone(); fetchVoiceClone();
}, [showVoiceClone]); }, [showVoiceClone]);
@@ -154,6 +164,30 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
const isVoiceCloneSelected = value === VOICE_CLONE_ID || const isVoiceCloneSelected = value === VOICE_CLONE_ID ||
(voiceClone?.success && voiceClone.custom_voice_id && value === voiceClone.custom_voice_id); (voiceClone?.success && voiceClone.custom_voice_id && value === voiceClone.custom_voice_id);
const handleVoiceSet = useCallback(() => {
setVoiceCreated(true);
setUseCreatedVoice(true);
}, []);
const handleDoneWithVoice = useCallback(() => {
if (useCreatedVoice) {
fetchVoiceClone();
}
setShowVoiceClonePanel(false);
setVoiceCreated(false);
}, [useCreatedVoice]);
const handleTogglePanel = useCallback(() => {
if (showVoiceClonePanel) {
setShowVoiceClonePanel(false);
setVoiceCreated(false);
} else {
setShowVoiceClonePanel(true);
setVoiceCreated(false);
setUseCreatedVoice(true);
}
}, [showVoiceClonePanel]);
if (compact) { if (compact) {
return ( return (
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
@@ -183,18 +217,65 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
} }
return ( return (
<Box> <Box
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}> sx={{
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}> p: 3,
Voice borderRadius: 3,
background: "#ffffff",
border: "1px solid rgba(102, 126, 234, 0.15)",
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%)",
},
}}
>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}>
<Box
sx={{
width: 24,
height: 24,
borderRadius: "50%",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
}}
>
<Typography sx={{ color: "#fff", fontSize: "0.75rem", fontWeight: 700 }}>4</Typography>
</Box>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
}}
>
<Mic fontSize="small" sx={{ color: "#667eea" }} />
</Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: "#0f172a", fontSize: "1rem" }}>
Voice Selection
</Typography> </Typography>
<Tooltip title="Choose a system voice or your custom cloned voice" arrow> <Tooltip title="Choose a system voice or your custom cloned voice" arrow>
<IconButton size="small" sx={{ color: "rgba(0,0,0,0.5)" }}> <IconButton size="small" sx={{ color: "#94a3b8", "&:hover": { color: "#667eea" } }}>
<HelpOutline fontSize="small" /> <HelpOutline fontSize="small" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{showVoiceClone && loadingVoiceClone && ( {showVoiceClone && loadingVoiceClone && (
<CircularProgress size={16} sx={{ ml: 1 }} /> <CircularProgress size={16} sx={{ ml: 1, color: "#667eea" }} />
)} )}
</Stack> </Stack>
@@ -210,8 +291,8 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
); );
return ( return (
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
<Mic fontSize="small" sx={{ color: voice?.isCustom ? "#667eea" : "inherit" }} /> <Mic fontSize="small" sx={{ color: voice?.isCustom ? "#667eea" : "#64748b" }} />
<Typography>{voice?.name}</Typography> <Typography sx={{ fontWeight: 500 }}>{voice?.name}</Typography>
{voice?.isCustom && ( {voice?.isCustom && (
<Chip <Chip
label="Cloned" label="Cloned"
@@ -221,22 +302,44 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
color: "#667eea", color: "#667eea",
height: 20, height: 20,
fontSize: "0.7rem", fontSize: "0.7rem",
fontWeight: 600,
}} }}
/> />
)} )}
</Stack> </Stack>
); );
}} }}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#f8fafc",
border: "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2,
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: "#667eea",
boxShadow: "0 0 0 4px rgba(102, 126, 234, 0.1)",
},
},
}}
MenuProps={{ MenuProps={{
PaperProps: { PaperProps: {
sx: { sx: {
maxHeight: 400, maxHeight: 400,
boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
border: "1px solid rgba(102, 126, 234, 0.15)",
borderRadius: 2,
}, },
}, },
}} }}
> >
{showVoiceClone && voiceClone?.success && voiceClone.custom_voice_id && ( {showVoiceClone && voiceClone?.success && voiceClone.custom_voice_id && (
<MenuItem value={VOICE_CLONE_ID} sx={{ borderBottom: '1px solid rgba(0,0,0,0.1)' }}> <MenuItem value={VOICE_CLONE_ID} sx={{ borderBottom: '1px solid rgba(0,0,0,0.1)', py: 1.5 }}>
<ListItemIcon> <ListItemIcon>
<AutoAwesome sx={{ color: "#667eea" }} /> <AutoAwesome sx={{ color: "#667eea" }} />
</ListItemIcon> </ListItemIcon>
@@ -255,6 +358,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
color: "#10b981", color: "#10b981",
height: 20, height: 20,
fontSize: "0.65rem", fontSize: "0.65rem",
fontWeight: 600,
'& .MuiChip-icon': { color: "#10b981" } '& .MuiChip-icon': { color: "#10b981" }
}} }}
/> />
@@ -273,7 +377,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
previewUrl: voiceClone.preview_audio_url previewUrl: voiceClone.preview_audio_url
}); });
}} }}
sx={{ mt: 0.5, textTransform: 'none' }} sx={{ mt: 0.5, textTransform: 'none', color: "#667eea" }}
> >
{playingPreview === VOICE_CLONE_ID ? "Stop" : "Preview"} {playingPreview === VOICE_CLONE_ID ? "Stop" : "Preview"}
</Button> </Button>
@@ -283,14 +387,17 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
</MenuItem> </MenuItem>
)} )}
<MenuItem disabled sx={{ opacity: 0.6 }}> <MenuItem disabled sx={{ opacity: 0.6, py: 1 }}>
<Typography variant="caption">System Voices</Typography> <Typography variant="caption" sx={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>System Voices</Typography>
</MenuItem> </MenuItem>
{voiceOptions.filter(v => !v.isCustom).map((voice) => ( {voiceOptions.filter(v => !v.isCustom).map((voice) => (
<MenuItem key={voice.id} value={voice.id}> <MenuItem key={voice.id} value={voice.id} sx={{ py: 1.5 }}>
<ListItemIcon>
<Mic fontSize="small" sx={{ color: "#64748b" }} />
</ListItemIcon>
<ListItemText <ListItemText
primary={voice.name} primary={<Typography sx={{ fontWeight: 500 }}>{voice.name}</Typography>}
secondary={voice.personality?.split(' - ')[0]} secondary={voice.personality?.split(' - ')[0]}
/> />
</MenuItem> </MenuItem>
@@ -299,23 +406,146 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
</FormControl> </FormControl>
{selectedVoice?.personality && ( {selectedVoice?.personality && (
<Typography variant="caption" sx={{ color: "text.secondary", mt: 0.5, display: 'block' }}> <Box sx={{ mt: 1.5, p: 1.5, bgcolor: alpha("#f0f4ff", 0.6), borderRadius: 1.5, border: "1px solid rgba(99, 102, 241, 0.15)" }}>
{selectedVoice.personality} <Typography variant="body2" sx={{ color: "#475569", fontSize: "0.8125rem", lineHeight: 1.5 }}>
</Typography> <strong style={{ color: "#0f172a" }}>Voice Personality:</strong> {selectedVoice.personality}
</Typography>
</Box>
)} )}
{showVoiceClone && !voiceClone?.success && ( {showVoiceClone && !voiceClone?.success && (
<Box sx={{ mt: 2, p: 2, bgcolor: alpha("#f8fafc", 0.5), borderRadius: 2, border: '1px dashed rgba(0,0,0,0.1)' }}> <Box sx={{ mt: 2 }}>
<Stack direction="row" spacing={1} alignItems="center"> <Button
<CloudUpload sx={{ color: "#64748b" }} /> onClick={handleTogglePanel}
<Typography variant="body2" sx={{ color: "#64748b" }}> startIcon={showVoiceClonePanel ? <ExpandLess /> : <AutoAwesome />}
Don't see your voice? Go to Onboarding Voice Cloning to create your custom voice clone. endIcon={showVoiceClonePanel ? <ExpandLess /> : <ExpandMore />}
</Typography> sx={{
</Stack> py: 1.5,
px: 2,
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%)",
border: showVoiceClonePanel
? "1px solid rgba(102, 126, 234, 0.3)"
: "1px dashed rgba(102, 126, 234, 0.4)",
borderRadius: 2,
color: "#667eea",
fontWeight: 600,
textTransform: "none",
fontSize: "0.875rem",
"&: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)",
},
}}
>
{showVoiceClonePanel ? "Hide Voice Cloning" : "Create Your Voice Clone"}
</Button>
<Collapse in={showVoiceClonePanel}>
<Box
sx={{
mt: 2,
p: 2,
borderRadius: 2,
background: "linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)",
border: "1px solid rgba(102, 126, 234, 0.2)",
boxShadow: "inset 0 1px 3px rgba(0,0,0,0.05)",
}}
>
<VoiceAvatarPlaceholder
domainName="Podcast"
onVoiceSet={handleVoiceSet}
/>
{voiceCreated && (
<Box
sx={{
mt: 2,
p: 2,
borderRadius: 2,
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)",
border: "1px solid rgba(16, 185, 129, 0.3)",
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.5 }}>
<CheckCircle sx={{ color: "#10b981", fontSize: "1.25rem" }} />
<Typography variant="subtitle2" sx={{ color: "#10b981", fontWeight: 700 }}>
Voice Clone Created Successfully!
</Typography>
</Stack>
<Typography variant="body2" sx={{ color: "#475569", mb: 1.5, fontSize: "0.875rem" }}>
Your custom voice clone is ready. Would you like to use this voice for your podcast?
</Typography>
<FormControlLabel
control={
<Checkbox
checked={useCreatedVoice}
onChange={(e) => setUseCreatedVoice(e.target.checked)}
sx={{
color: "#10b981",
"&.Mui-checked": { color: "#10b981" },
}}
/>
}
label={
<Typography sx={{ color: "#1e293b", fontWeight: 500, fontSize: "0.9375rem" }}>
Use this voice for my podcast
</Typography>
}
/>
<Divider sx={{ my: 2, borderColor: "rgba(0,0,0,0.08)" }} />
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
<Button
onClick={() => {
setShowVoiceClonePanel(false);
setVoiceCreated(false);
}}
sx={{
color: "#64748b",
"&:hover": { color: "#1e293b", background: "rgba(0,0,0,0.04)" },
}}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handleDoneWithVoice}
sx={{
background: useCreatedVoice
? "linear-gradient(135deg, #10b981 0%, #059669 100%)"
: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "#fff",
fontWeight: 600,
textTransform: "none",
px: 3,
boxShadow: useCreatedVoice
? "0 4px 12px rgba(16, 185, 129, 0.3)"
: "0 4px 12px rgba(102, 126, 234, 0.3)",
"&:hover": {
background: useCreatedVoice
? "linear-gradient(135deg, #059669 0%, #047857 100%)"
: "linear-gradient(135deg, #764ba2 0%, #667eea 100%)",
},
}}
>
{useCreatedVoice ? "Use This Voice" : "Done"}
</Button>
</Stack>
</Box>
)}
</Box>
</Collapse>
</Box> </Box>
)} )}
</Box> </Box>
); );
}; };
export default VoiceSelector; export default VoiceSelector;