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": "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

View File

@@ -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<AnalysisTabNavProps> = ({ activeTab, onTabChange }) => {
return (
<Stack direction="row" flexWrap="wrap" gap={1}>
{ANALYSIS_TABS.map((tab) => (
<Button
key={tab.id}
onClick={() => onTabChange(tab.id)}
startIcon={tab.icon}
sx={getTabButtonStyles(activeTab === tab.id)}
>
{tab.label}
</Button>
))}
</Stack>
<Box
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, rgba(118, 75, 162, 0.04) 100%)",
borderRadius: 2.5,
p: 1.5,
border: "1px solid rgba(102, 126, 234, 0.1)",
}}
>
<Stack direction="row" flexWrap="wrap" gap={1}>
{ANALYSIS_TABS.map((tab) => (
<Button
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";
// 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<CreateModalProps> = ({ 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<CreateModalProps> = ({ onCreate, open, defaul
}}
>
<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">
<Box sx={{ flex: 1 }}>
<TopicUrlInput
@@ -540,6 +537,10 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
placeholderIndex={placeholderIndex}
loading={enhancingTopic}
loadingMessage={enhanceTopicMessage}
estimatedCost={estimatedCost}
duration={duration}
speakers={speakers}
knobs={knobs}
/>
</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="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
sx={{
width: 36,
@@ -439,30 +453,16 @@ export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
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)",
}}
>
<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 }}>
<PhotoCameraIcon fontSize="small" sx={{ color: "#64748b" }} />
Take a Selfie
</Typography>
<Typography variant="body2" sx={{ color: "#475569", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, lineHeight: 1.6 }}>
Capture a photo using your device camera and use <strong>"Make Presentable"</strong> to enhance it into a professional presenter using AI.
</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>
<Stack direction="row" alignItems="center" spacing={0.5}>
<PhotoCameraIcon fontSize="small" sx={{ color: "#667eea" }} />
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, fontWeight: 600 }}>
Capture a photo using your device camera and use "Make Presentable" to enhance it. Camera access required.
</Typography>
</Stack>
</Box>
</Stack>
)}
@@ -570,30 +570,16 @@ export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
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)",
}}
>
<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 }}>
<CloudUploadIcon fontSize="small" sx={{ color: "#64748b" }} />
Upload Your Photo
</Typography>
<Typography variant="body2" sx={{ color: "#475569", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, lineHeight: 1.6 }}>
Upload a new photo and use <strong>"Make Presentable"</strong> to enhance it into a professional presenter using AI.
</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>
<Stack direction="row" alignItems="center" spacing={0.5}>
<CloudUploadIcon fontSize="small" sx={{ color: "#667eea" }} />
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: { xs: "0.75rem", sm: "0.8125rem" }, fontWeight: 600 }}>
Upload a photo and use "Make Presentable" to enhance it into a professional presenter. Supported: JPG, PNG, WebP (max 5MB)
</Typography>
</Stack>
</Box>
</Stack>
)}

View File

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

View File

@@ -29,32 +29,53 @@ export const CreateHeader: React.FC<CreateHeaderProps> = ({
estimatedCost,
}) => {
return (
<Stack direction="row" spacing={2} alignItems="flex-start" justifyContent="space-between" flexWrap="wrap" gap={2}>
<Stack direction="row" spacing={2} alignItems="flex-start" sx={{ flex: 1, minWidth: { xs: "100%", md: "60%" } }}>
<Box
sx={{
width: 48,
height: 48,
borderRadius: 2,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<AutoAwesomeIcon sx={{ color: "#667eea", fontSize: "1.75rem" }} />
</Box>
<Box sx={{ flex: 1 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
<Box
sx={{
width: "100%",
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 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
variant="h5"
variant="h6"
sx={{
color: "#0f172a",
background: "linear-gradient(135deg, #1e293b 0%, #334155 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
fontWeight: 700,
fontSize: { xs: "1.5rem", md: "1.75rem" },
fontSize: { xs: "1.125rem", md: "1.25rem" },
letterSpacing: "-0.02em",
lineHeight: 1.2,
}}
>
Create New Podcast Episode
@@ -106,122 +127,122 @@ export const CreateHeader: React.FC<CreateHeaderProps> = ({
</IconButton>
</Tooltip>
</Stack>
</Box>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ alignItems: "center" }}>
<Tooltip
title={`Your current subscription plan: ${subscription?.tier || "free"}. Upgrade for more features.`}
arrow
placement="top"
>
<Chip
label={`Plan: ${subscription?.tier || "free"}`}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
color: "#667eea",
fontWeight: 600,
border: "1px solid rgba(102, 126, 234, 0.2)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
/>
</Tooltip>
<Tooltip
title={`Podcast duration: ${duration} minutes. Maximum duration is 10 minutes. Recommended: 5-10 minutes for best results.`}
arrow
placement="top"
>
<Chip
label={`Duration: ${duration} min`}
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={`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: {
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ alignItems: "center" }}>
<Tooltip
title={`Your current subscription plan: ${subscription?.tier || "free"}. Upgrade for more features.`}
arrow
placement="top"
>
<Chip
label={`Plan: ${subscription?.tier || "free"}`}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
color: "#667eea",
fontWeight: 600,
border: "1px solid rgba(102, 126, 234, 0.2)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
/>
</Tooltip>
<Tooltip
title={`Podcast duration: ${duration} minutes. Maximum duration is 10 minutes. Recommended: 5-10 minutes for best results.`}
arrow
placement="top"
>
<Chip
label={`Duration: ${duration} min`}
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={`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>
</Box>
);
};

View File

@@ -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<PodcastConfigurationProps> = ({
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%)",
},
}}
>
<Typography variant="subtitle2" sx={{ mb: 2.5, color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}>
Basic Configuration
</Typography>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2.5 }}>
<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 }}>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}>
{/* Duration Input */}
<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)
</Typography>
<TextField
@@ -64,27 +114,30 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
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<PodcastConfigurationProps> = ({
{/* Speakers Toggle */}
<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
</Typography>
<ToggleButtonGroup
@@ -102,8 +155,8 @@ export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
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<PodcastConfigurationProps> = ({
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<PodcastConfigurationProps> = ({
</Stack>
</ToggleButton>
</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"}
</Typography>
</Box>

View File

@@ -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<TopicUrlInputProps> = ({
@@ -31,23 +42,137 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
placeholderIndex,
loading = false,
loadingMessage,
estimatedCost,
duration = 1,
speakers = 1,
knobs,
}) => {
return (
<Box
sx={{
p: 3,
borderRadius: 2,
background: alpha("#f8fafc", 0.5),
border: "1px solid rgba(15, 23, 42, 0.06)",
height: "100%", // Fill height of parent
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%)",
},
}}
>
<Box flex={1} display="flex" flexDirection="column">
<Typography variant="subtitle2" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}>
Topic Idea or Blog URL
</Typography>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<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
title={
isUrl
@@ -65,7 +190,7 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
inputProps={{
sx: {
"&::placeholder": { color: "#94a3b8", opacity: 1 },
color: "#0f172a",
color: "#1e293b",
},
}}
value={value}
@@ -78,35 +203,41 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
}
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<TopicUrlInputProps> = ({
{/* Enhance topic with AI button - appears when user types (and not a URL) */}
{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
size="small"
variant="contained"
@@ -133,7 +264,8 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
fontWeight: 600,
borderRadius: 2.5,
color: "#f8fbff",
px: 1.8,
px: 2,
py: 0.75,
border: "1px solid rgba(148, 211, 255, 0.6)",
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)",

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Stack, Typography } from "@mui/material";
import { Stack, Typography, Box } from "@mui/material";
import {
Mic as MicIcon,
Info as InfoIcon,
@@ -19,7 +19,27 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
const navigate = useNavigate();
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
direction="row"
justifyContent="space-between"
@@ -27,20 +47,35 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
flexWrap="wrap"
gap={2}
>
<Typography
variant="h3"
sx={{
color: "#1e293b",
fontWeight: 800,
display: "flex",
alignItems: "center",
gap: 1.5,
fontSize: { xs: "1.5rem", md: "2rem" },
}}
>
<MicIcon fontSize="large" sx={{ color: "#667eea" }} />
AI Podcast Maker
</Typography>
<Stack direction="row" alignItems="center" gap={1.5}>
<Box
sx={{
width: 44,
height: 44,
borderRadius: 2,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.3)",
}}
>
<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
direction="row"
spacing={1}
@@ -114,14 +149,7 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
</PrimaryButton>
</Stack>
</Stack>
<Typography
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>
</Box>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect, useMemo, useCallback } from "react";
import {
Box,
Typography,
@@ -15,17 +15,23 @@ import {
IconButton,
ListItemIcon,
ListItemText,
Collapse,
FormControlLabel,
Checkbox,
Divider,
} from "@mui/material";
import {
Mic,
PlayArrow,
Pause,
CloudUpload,
HelpOutline,
AutoAwesome,
CheckCircle,
ExpandLess,
ExpandMore,
} from "@mui/icons-material";
import { getLatestVoiceClone, VoiceCloneResponse } from "../../api/brandAssets";
import { VoiceAvatarPlaceholder } from "../OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder";
export type VoiceOption = {
id: string;
@@ -75,6 +81,23 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
const [voiceClone, setVoiceClone] = useState<VoiceCloneResponse | null>(null);
const [loadingVoiceClone, setLoadingVoiceClone] = useState(false);
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 options: VoiceOption[] = [...PREDEFINED_VOICES];
@@ -101,19 +124,6 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
useEffect(() => {
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();
}, [showVoiceClone]);
@@ -154,6 +164,30 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
const isVoiceCloneSelected = value === VOICE_CLONE_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) {
return (
<FormControl fullWidth size="small">
@@ -183,18 +217,65 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
}
return (
<Box>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Voice
<Box
sx={{
p: 3,
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>
<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" />
</IconButton>
</Tooltip>
{showVoiceClone && loadingVoiceClone && (
<CircularProgress size={16} sx={{ ml: 1 }} />
<CircularProgress size={16} sx={{ ml: 1, color: "#667eea" }} />
)}
</Stack>
@@ -210,8 +291,8 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
);
return (
<Stack direction="row" spacing={1} alignItems="center">
<Mic fontSize="small" sx={{ color: voice?.isCustom ? "#667eea" : "inherit" }} />
<Typography>{voice?.name}</Typography>
<Mic fontSize="small" sx={{ color: voice?.isCustom ? "#667eea" : "#64748b" }} />
<Typography sx={{ fontWeight: 500 }}>{voice?.name}</Typography>
{voice?.isCustom && (
<Chip
label="Cloned"
@@ -221,22 +302,44 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
color: "#667eea",
height: 20,
fontSize: "0.7rem",
fontWeight: 600,
}}
/>
)}
</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={{
PaperProps: {
sx: {
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 && (
<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>
<AutoAwesome sx={{ color: "#667eea" }} />
</ListItemIcon>
@@ -255,6 +358,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
color: "#10b981",
height: 20,
fontSize: "0.65rem",
fontWeight: 600,
'& .MuiChip-icon': { color: "#10b981" }
}}
/>
@@ -273,7 +377,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
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"}
</Button>
@@ -283,14 +387,17 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
</MenuItem>
)}
<MenuItem disabled sx={{ opacity: 0.6 }}>
<Typography variant="caption">System Voices</Typography>
<MenuItem disabled sx={{ opacity: 0.6, py: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>System Voices</Typography>
</MenuItem>
{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
primary={voice.name}
primary={<Typography sx={{ fontWeight: 500 }}>{voice.name}</Typography>}
secondary={voice.personality?.split(' - ')[0]}
/>
</MenuItem>
@@ -299,23 +406,146 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
</FormControl>
{selectedVoice?.personality && (
<Typography variant="caption" sx={{ color: "text.secondary", mt: 0.5, display: 'block' }}>
{selectedVoice.personality}
</Typography>
<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)" }}>
<Typography variant="body2" sx={{ color: "#475569", fontSize: "0.8125rem", lineHeight: 1.5 }}>
<strong style={{ color: "#0f172a" }}>Voice Personality:</strong> {selectedVoice.personality}
</Typography>
</Box>
)}
{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)' }}>
<Stack direction="row" spacing={1} alignItems="center">
<CloudUpload sx={{ color: "#64748b" }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
Don't see your voice? Go to Onboarding Voice Cloning to create your custom voice clone.
</Typography>
</Stack>
<Box sx={{ mt: 2 }}>
<Button
onClick={handleTogglePanel}
startIcon={showVoiceClonePanel ? <ExpandLess /> : <AutoAwesome />}
endIcon={showVoiceClonePanel ? <ExpandLess /> : <ExpandMore />}
sx={{
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>
);
};
export default VoiceSelector;
export default VoiceSelector;