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