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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user