Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
import React from "react";
|
||||
import { Stack, Box, Typography, Tabs, Tab, CircularProgress, Button, IconButton, Tooltip, alpha } from "@mui/material";
|
||||
import {
|
||||
Person as PersonIcon,
|
||||
Info as InfoIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Collections as CollectionsIcon,
|
||||
Delete as DeleteIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CloudUpload as CloudUploadIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { AvatarAssetBrowser } from "../AvatarAssetBrowser";
|
||||
import { SecondaryButton } from "../ui";
|
||||
|
||||
interface AvatarSelectorProps {
|
||||
avatarTab: number;
|
||||
setAvatarTab: (event: React.SyntheticEvent, newValue: number) => void;
|
||||
avatarFile: File | null;
|
||||
avatarPreview: string | null;
|
||||
avatarUrl: string | null;
|
||||
loadingBrandAvatar: boolean;
|
||||
handleUseBrandAvatar: () => void;
|
||||
handleAvatarSelectFromLibrary: (url: string) => void;
|
||||
handleAvatarChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleRemoveAvatar: () => void;
|
||||
handleMakePresentable: () => void;
|
||||
makingPresentable: boolean;
|
||||
avatarPreviewBlobUrl: string | null;
|
||||
brandAvatarFromDb?: string | null;
|
||||
brandAvatarBlobUrl?: string | null;
|
||||
}
|
||||
|
||||
export const AvatarSelector: React.FC<AvatarSelectorProps> = ({
|
||||
avatarTab,
|
||||
setAvatarTab,
|
||||
avatarFile,
|
||||
avatarPreview,
|
||||
avatarUrl,
|
||||
loadingBrandAvatar,
|
||||
handleUseBrandAvatar,
|
||||
handleAvatarSelectFromLibrary,
|
||||
handleAvatarChange,
|
||||
handleRemoveAvatar,
|
||||
handleMakePresentable,
|
||||
makingPresentable,
|
||||
avatarPreviewBlobUrl,
|
||||
brandAvatarFromDb,
|
||||
brandAvatarBlobUrl,
|
||||
}) => {
|
||||
const isAuthenticatedUrl = React.useCallback((url: string | null): boolean => {
|
||||
if (!url) return false;
|
||||
return url.includes('/api/podcast/') ||
|
||||
url.includes('/api/youtube/') ||
|
||||
url.includes('/api/story/') ||
|
||||
(url.startsWith('/') && !url.startsWith('//'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
p: 2.5,
|
||||
borderRadius: 2,
|
||||
background: "#ffffff",
|
||||
border: "1px solid rgba(15, 23, 42, 0.08)",
|
||||
boxShadow: "0 1px 2px rgba(15, 23, 42, 0.04)",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<PersonIcon fontSize="small" sx={{ color: "#667eea" }} />
|
||||
</Box>
|
||||
<Typography variant="subtitle2" sx={{ color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}>
|
||||
Podcast Presenter Avatar
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
Avatar Options:
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
|
||||
<strong>Upload your photo:</strong> We'll enhance it into a professional podcast presenter using AI.<br/><br/>
|
||||
<strong>Brand Avatar:</strong> Use your configured brand avatar for consistency.<br/><br/>
|
||||
<strong>Asset Library:</strong> Choose from your previously uploaded images.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<InfoIcon fontSize="small" sx={{ color: "#94a3b8", cursor: "help" }} />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Stack direction={{ xs: "column", lg: "row" }} spacing={3} alignItems="flex-start">
|
||||
{/* Left Side: Tabs & Content */}
|
||||
<Box sx={{ flex: 1, width: "100%" }}>
|
||||
<Tabs
|
||||
value={avatarTab}
|
||||
onChange={setAvatarTab}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
sx={{
|
||||
mb: 3,
|
||||
minHeight: 48,
|
||||
"& .MuiTabs-indicator": {
|
||||
display: "none",
|
||||
},
|
||||
"& .MuiTabs-flexContainer": {
|
||||
gap: 1.5,
|
||||
},
|
||||
"& .MuiTab-root": {
|
||||
textTransform: "none",
|
||||
minHeight: 44,
|
||||
fontWeight: 600,
|
||||
fontSize: "0.875rem",
|
||||
borderRadius: "12px",
|
||||
px: 2.5,
|
||||
color: "#64748b",
|
||||
border: "1.5px solid #e2e8f0",
|
||||
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
backgroundColor: "#ffffff",
|
||||
"&:hover": {
|
||||
borderColor: "#cbd5e1",
|
||||
backgroundColor: "#f8fafc",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
color: "#ffffff",
|
||||
borderColor: "transparent",
|
||||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.25)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label="Use Brand Avatar" />
|
||||
<Tab label="Asset Library" />
|
||||
<Tab label="Upload Your Photo" />
|
||||
</Tabs>
|
||||
|
||||
{avatarTab === 0 && (
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ minHeight: 200, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", bgcolor: "#f8fafc", borderRadius: 2, p: 2, position: "relative" }}>
|
||||
{loadingBrandAvatar ? (
|
||||
<CircularProgress size={32} />
|
||||
) : avatarPreview && avatarPreview === brandAvatarFromDb ? (
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={avatarPreviewBlobUrl || ""}
|
||||
alt="Selected Brand Avatar"
|
||||
sx={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
objectFit: "cover",
|
||||
borderRadius: 2.5,
|
||||
border: "2px solid #667eea",
|
||||
boxShadow: "0 4px 12px rgba(102, 126, 234, 0.25)",
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRemoveAvatar}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: -8,
|
||||
right: -8,
|
||||
bgcolor: "white",
|
||||
border: "1.5px solid #e2e8f0",
|
||||
boxShadow: "0 2px 4px rgba(15, 23, 42, 0.1)",
|
||||
"&:hover": {
|
||||
bgcolor: "#fef2f2",
|
||||
borderColor: "#ef4444",
|
||||
color: "#ef4444",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<CheckCircleIcon color="primary" fontSize="small" />
|
||||
<Typography variant="body2" sx={{ color: "#64748b", fontStyle: "italic" }}>
|
||||
Active Presenter
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : brandAvatarFromDb ? (
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Box
|
||||
component="img"
|
||||
src={brandAvatarBlobUrl || ""}
|
||||
alt="Available Brand Avatar"
|
||||
sx={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
objectFit: "cover",
|
||||
borderRadius: 2.5,
|
||||
border: "1.5px solid #e2e8f0",
|
||||
opacity: 0.8,
|
||||
filter: "grayscale(0.3)",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleUseBrandAvatar}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
textTransform: "none",
|
||||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
}}
|
||||
>
|
||||
Use this Avatar
|
||||
</Button>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<PersonIcon sx={{ fontSize: 48, color: "#cbd5e1" }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No brand avatar found.
|
||||
</Typography>
|
||||
<Button size="small" startIcon={<RefreshIcon />} onClick={() => void handleUseBrandAvatar()}>
|
||||
Retry
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
background: alpha("#f0f4ff", 0.6),
|
||||
border: "1px solid rgba(99, 102, 241, 0.2)",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: "0.875rem", fontWeight: 600, mb: 0.5, display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<AutoAwesomeIcon fontSize="small" sx={{ color: "#667eea" }} />
|
||||
Brand Avatar
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#475569", fontSize: "0.8125rem", lineHeight: 1.6 }}>
|
||||
Select your pre-configured brand avatar to maintain consistency. If not selected, a new AI presenter will be generated.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{avatarTab === 1 && (
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ minHeight: 300, position: "relative" }}>
|
||||
{avatarPreview && !avatarFile && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRemoveAvatar}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 10,
|
||||
bgcolor: "white",
|
||||
border: "1.5px solid #e2e8f0",
|
||||
boxShadow: "0 2px 4px rgba(15, 23, 42, 0.1)",
|
||||
"&:hover": {
|
||||
bgcolor: "#fef2f2",
|
||||
borderColor: "#ef4444",
|
||||
color: "#ef4444",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<AvatarAssetBrowser
|
||||
selectedUrl={avatarUrl}
|
||||
onSelect={(url) => handleAvatarSelectFromLibrary(url)}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
background: alpha("#f8fafc", 0.8),
|
||||
border: "1px solid rgba(15, 23, 42, 0.1)",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: "0.875rem", fontWeight: 600, mb: 0.5, display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<CollectionsIcon fontSize="small" sx={{ color: "#64748b" }} />
|
||||
Asset Library
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#475569", fontSize: "0.8125rem", lineHeight: 1.6 }}>
|
||||
Select from your previously uploaded images. Filter by favorites or search to find the perfect presenter.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{avatarTab === 2 && (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
{avatarFile && avatarPreview ? (
|
||||
<Stack spacing={2} alignItems="center" sx={{ bgcolor: "#f8fafc", borderRadius: 2, p: 2 }}>
|
||||
<Box sx={{ position: "relative", display: "inline-block" }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={avatarPreviewBlobUrl || (avatarPreview.startsWith("data:") ? avatarPreview : "")}
|
||||
alt="Avatar preview"
|
||||
sx={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
objectFit: "cover",
|
||||
borderRadius: 2.5,
|
||||
border: "2px solid #e2e8f0",
|
||||
boxShadow: "0 2px 8px rgba(15, 23, 42, 0.08)",
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRemoveAvatar}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: -8,
|
||||
right: -8,
|
||||
bgcolor: "white",
|
||||
border: "1.5px solid #e2e8f0",
|
||||
boxShadow: "0 2px 4px rgba(15, 23, 42, 0.1)",
|
||||
"&:hover": {
|
||||
bgcolor: "#f8fafc",
|
||||
borderColor: "#dc2626",
|
||||
color: "#dc2626",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{avatarUrl && (
|
||||
<Tooltip
|
||||
title="Transform your uploaded photo into a professional podcast presenter."
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box>
|
||||
<SecondaryButton
|
||||
onClick={handleMakePresentable}
|
||||
disabled={makingPresentable}
|
||||
loading={makingPresentable}
|
||||
startIcon={!makingPresentable ? <AutoAwesomeIcon fontSize="small" /> : undefined}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{makingPresentable ? "Transforming..." : "Make Presentable"}
|
||||
</SecondaryButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Box
|
||||
component="label"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
minHeight: 200,
|
||||
border: "2px dashed #cbd5e1",
|
||||
borderRadius: 2.5,
|
||||
bgcolor: "#f8fafc",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
"&:hover": {
|
||||
borderColor: "#667eea",
|
||||
bgcolor: "#f1f5f9",
|
||||
borderWidth: "2.5px",
|
||||
boxShadow: "0 0 0 3px rgba(102, 126, 234, 0.08)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<CloudUploadIcon sx={{ color: "#94a3b8", fontSize: 36, mb: 1.5 }} />
|
||||
<Typography variant="body2" sx={{ color: "#64748b", fontWeight: 600, mb: 0.5 }}>
|
||||
Upload Your Photo
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "#94a3b8", textAlign: "center", px: 2, lineHeight: 1.5 }}>
|
||||
JPG, PNG, WebP (max 5MB)
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
background: alpha("#f8fafc", 0.8),
|
||||
border: "1px solid rgba(15, 23, 42, 0.1)",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: "#0f172a", fontSize: "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: "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: 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: "0.8125rem", fontWeight: 500, display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<InfoIcon fontSize="inherit" />
|
||||
Supported formats: JPG, PNG, WebP (max 5MB)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { Stack, Alert, Typography, alpha } from "@mui/material";
|
||||
import {
|
||||
Info as InfoIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { PrimaryButton, SecondaryButton } from "../ui";
|
||||
|
||||
interface CreateActionsProps {
|
||||
reset: () => void;
|
||||
submit: () => void;
|
||||
canSubmit: boolean;
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
export const CreateActions: React.FC<CreateActionsProps> = ({
|
||||
reset,
|
||||
submit,
|
||||
canSubmit,
|
||||
isSubmitting,
|
||||
}) => {
|
||||
return (
|
||||
<Stack spacing={3.5}>
|
||||
{/* Info Banner */}
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<InfoIcon sx={{ color: "#6366f1", fontSize: "1.125rem" }} />}
|
||||
sx={{
|
||||
background: alpha("#f0f4ff", 0.6),
|
||||
border: "1px solid rgba(99, 102, 241, 0.15)",
|
||||
borderRadius: 2,
|
||||
boxShadow: "0 1px 3px rgba(99, 102, 241, 0.08)",
|
||||
"& .MuiAlert-message": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontSize: "0.875rem", color: "#475569", lineHeight: 1.6, fontWeight: 400 }}>
|
||||
Podcast avatar Image is required, brand avatar is Default, you can choose existing images from asset library Or Upload your Picture. If not, AI Avatar will be generated automatically.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={1}>
|
||||
<SecondaryButton onClick={reset} startIcon={<RefreshIcon />}>
|
||||
Reset
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={submit}
|
||||
disabled={!canSubmit || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
tooltip={!canSubmit ? "Enter an idea or URL to continue" : "We’ll start AI analysis after this click"}
|
||||
>
|
||||
{isSubmitting ? "Analyzing..." : "Analyze & Continue"}
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
227
frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx
Normal file
227
frontend/src/components/PodcastMaker/CreateStep/CreateHeader.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React from 'react';
|
||||
import { Stack, Box, Typography, Tooltip, IconButton, Chip, alpha } from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
HelpOutline as HelpOutlineIcon,
|
||||
AttachMoney as AttachMoneyIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { Knobs } from '../types';
|
||||
|
||||
interface CreateHeaderProps {
|
||||
subscription: any;
|
||||
duration: number;
|
||||
speakers: number;
|
||||
knobs: Knobs;
|
||||
estimatedCost: {
|
||||
ttsCost: number;
|
||||
avatarCost: number;
|
||||
videoCost: number;
|
||||
researchCost: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const CreateHeader: React.FC<CreateHeaderProps> = ({
|
||||
subscription,
|
||||
duration,
|
||||
speakers,
|
||||
knobs,
|
||||
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 }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: "#0f172a",
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: "1.5rem", md: "1.75rem" },
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Create New Podcast Episode
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||||
Tips for best results:
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem" }}>
|
||||
• Provide one clear topic OR a single blog URL (we won't auto-run anything).<br />
|
||||
• Keep it concise—one sentence topic works best.<br />
|
||||
• We start analysis only after you confirm, so you stay in control.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
bgcolor: "#0f172a",
|
||||
color: "#ffffff",
|
||||
maxWidth: 300,
|
||||
fontSize: "0.875rem",
|
||||
p: 1.5,
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#0f172a",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
color: "#64748b",
|
||||
"&:hover": {
|
||||
color: "#667eea",
|
||||
backgroundColor: alpha("#667eea", 0.08),
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HelpOutlineIcon fontSize="small" />
|
||||
</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: {
|
||||
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>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
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";
|
||||
|
||||
interface PodcastConfigurationProps {
|
||||
duration: number;
|
||||
setDuration: (value: number) => void;
|
||||
speakers: number;
|
||||
setSpeakers: (value: number) => void;
|
||||
}
|
||||
|
||||
export const PodcastConfiguration: React.FC<PodcastConfigurationProps> = ({
|
||||
duration,
|
||||
setDuration,
|
||||
speakers,
|
||||
setSpeakers,
|
||||
}) => {
|
||||
const handleDurationChange = (value: number) => {
|
||||
const clamped = Math.min(10, Math.max(1, value));
|
||||
setDuration(clamped);
|
||||
};
|
||||
|
||||
const handleSpeakersChange = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
newValue: number | null
|
||||
) => {
|
||||
if (newValue !== null) {
|
||||
setSpeakers(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
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)",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2.5, color: "#0f172a", fontWeight: 600, fontSize: "0.9375rem" }}>
|
||||
Basic Configuration
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={3}>
|
||||
{/* Duration Input */}
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 500 }}>
|
||||
Duration (minutes)
|
||||
</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
value={duration}
|
||||
onChange={(e) => handleDurationChange(Number(e.target.value) || 1)}
|
||||
InputProps={{ inputProps: { min: 1, max: 10 } }}
|
||||
size="small"
|
||||
helperText={duration > 10 ? "Maximum duration is 10 minutes" : "Recommended: 1-3 mins"}
|
||||
error={duration > 10}
|
||||
fullWidth
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#ffffff",
|
||||
border: "1px solid rgba(15, 23, 42, 0.12)",
|
||||
borderRadius: 2,
|
||||
transition: "all 0.2s",
|
||||
"&:hover": {
|
||||
borderColor: "rgba(102, 126, 234, 0.6)",
|
||||
},
|
||||
"&.Mui-focused": {
|
||||
borderColor: "#667eea",
|
||||
boxShadow: "0 0 0 3px rgba(102, 126, 234, 0.1)",
|
||||
},
|
||||
},
|
||||
"& .MuiOutlinedInput-input": {
|
||||
color: "#0f172a",
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9375rem",
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
color: duration > 10 ? "#dc2626" : "#64748b",
|
||||
fontSize: "0.75rem",
|
||||
mt: 0.75,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Speakers Toggle */}
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ display: "block", mb: 1, color: "#64748b", fontWeight: 500 }}>
|
||||
Number of Speakers
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={speakers}
|
||||
exclusive
|
||||
onChange={handleSpeakersChange}
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: "#ffffff",
|
||||
border: "1px solid rgba(15, 23, 42, 0.12)",
|
||||
borderRadius: 2,
|
||||
p: 0.5,
|
||||
"& .MuiToggleButton-root": {
|
||||
border: "none",
|
||||
borderRadius: 1.5,
|
||||
color: "#64748b",
|
||||
textTransform: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.875rem",
|
||||
py: 1,
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
backgroundColor: alpha("#64748b", 0.05),
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: alpha("#667eea", 0.1),
|
||||
color: "#667eea",
|
||||
fontWeight: 600,
|
||||
"&:hover": {
|
||||
backgroundColor: alpha("#667eea", 0.15),
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={1} aria-label="1 speaker">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<PersonIcon fontSize="small" />
|
||||
<Typography variant="body2">1 Speaker</Typography>
|
||||
</Stack>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={2} aria-label="2 speakers">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<GroupIcon fontSize="small" />
|
||||
<Typography variant="body2">2 Speakers</Typography>
|
||||
</Stack>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Typography variant="caption" sx={{ display: "block", mt: 0.75, color: "#64748b", fontSize: "0.75rem" }}>
|
||||
{speakers === 1 ? "Single host format" : "Host and guest conversation"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import { Box, Typography, TextField, Tooltip, Button, alpha } from "@mui/material";
|
||||
import { AutoAwesome as AutoAwesomeIcon } from "@mui/icons-material";
|
||||
|
||||
export const TOPIC_PLACEHOLDERS = [
|
||||
"Industry insights: Latest trends in AI for Content Marketing",
|
||||
"Product deep-dive: How our new feature solves common pain points",
|
||||
"Educational: 5 ways to improve your workflow with automation",
|
||||
"Thought leadership: The future of decentralized finance (DeFi)",
|
||||
"Interview prep: Key questions for your next tech hiring round",
|
||||
"Podcast prep: Analyzing the impact of remote work on mental health",
|
||||
];
|
||||
|
||||
interface TopicUrlInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
isUrl: boolean;
|
||||
showAIDetailsButton: boolean;
|
||||
onAIDetailsClick?: () => void;
|
||||
placeholderIndex: number;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
isUrl,
|
||||
showAIDetailsButton,
|
||||
onAIDetailsClick,
|
||||
placeholderIndex,
|
||||
loading = false,
|
||||
}) => {
|
||||
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
|
||||
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" }}>
|
||||
Topic Idea or Blog URL
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
isUrl
|
||||
? "We detected a URL. We'll fetch insights from this page."
|
||||
: "Enter a concise idea or paste a blog URL."
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={5}
|
||||
placeholder={!value ? `e.g., "${TOPIC_PLACEHOLDERS[placeholderIndex]}" or paste a URL` : ""}
|
||||
inputProps={{
|
||||
sx: {
|
||||
"&::placeholder": { color: "#94a3b8", opacity: 1 },
|
||||
color: "#0f172a",
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
size="small"
|
||||
helperText={
|
||||
isUrl
|
||||
? "URL detected. We'll analyze this page content."
|
||||
: "Enter a clear, concise topic. We'll expand it into a full script after you click Analyze."
|
||||
}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
backgroundColor: "#ffffff",
|
||||
border: "1.5px solid rgba(15, 23, 42, 0.12)",
|
||||
borderRadius: 2,
|
||||
"&:hover": {
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "rgba(102, 126, 234, 0.4)",
|
||||
},
|
||||
"&.Mui-focused": {
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: isUrl ? "#10b981" : "#667eea", // Green for URL, Blue for Topic
|
||||
borderWidth: 2,
|
||||
},
|
||||
},
|
||||
"& .MuiOutlinedInput-input": {
|
||||
fontSize: "0.9375rem",
|
||||
lineHeight: 1.6,
|
||||
color: "#0f172a",
|
||||
fontWeight: 400,
|
||||
},
|
||||
"& .MuiInputBase-input::placeholder": {
|
||||
color: "#94a3b8",
|
||||
opacity: 1,
|
||||
fontWeight: 400,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
color: isUrl ? "#059669" : "#64748b",
|
||||
fontSize: "0.8125rem",
|
||||
fontWeight: 400,
|
||||
mt: 0.75,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Add details with AI button - appears when user types (and not a URL) */}
|
||||
{showAIDetailsButton && !isUrl && (
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
onClick={onAIDetailsClick}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 600,
|
||||
borderColor: "#667eea",
|
||||
borderWidth: 1.5,
|
||||
color: "#667eea",
|
||||
borderRadius: 2,
|
||||
"&:hover": {
|
||||
borderColor: "#5568d3",
|
||||
backgroundColor: alpha("#667eea", 0.08),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? "Enhancing..." : "Add details with AI"}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user