Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements

This commit is contained in:
ajaysi
2026-02-28 20:06:26 +05:30
parent 08a1f4a1d8
commit 4828274cbf
162 changed files with 19489 additions and 4300 deletions

View File

@@ -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>
);
};

View File

@@ -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" : "Well start AI analysis after this click"}
>
{isSubmitting ? "Analyzing..." : "Analyze & Continue"}
</PrimaryButton>
</Stack>
</Stack>
);
};

View 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 conciseone 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};