feat(podcast): add Get Trending Topics modal to podcast topic input

Frontend Changes:
- Added TrendingTopicsModal with tabs (Interest Chart, Regions, Related Topics, Related Queries)
- Reuses existing TrendsChart component from Research module
- Clickable chips for related topics/queries that populate the topic input
- Added 'Get Trending Topics' green button next to 'Enhance Topic With AI'
- Responsive layout: buttons stack on mobile, side-by-side on desktop
- Wired up modal state in CreateModal
- Backend endpoint and podcastApi method committed in prior push
This commit is contained in:
ajaysi
2026-04-24 20:52:23 +05:30
parent fc47445181
commit a7d2ef1c09
3 changed files with 541 additions and 9 deletions

View File

@@ -15,6 +15,7 @@ import { PodcastConfiguration } from "./CreateStep/PodcastConfiguration";
import { AvatarSelector } from "./CreateStep/AvatarSelector";
import { CreateActions } from "./CreateStep/CreateActions";
import { EnhancedTopicChoicesModal } from "./EnhancedTopicChoicesModal";
import { TrendingTopicsModal } from "./CreateStep/TrendingTopicsModal";
const ENHANCE_TOPIC_PROGRESS_MESSAGES = [
"Analyzing your topic idea...",
@@ -61,6 +62,10 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
const [choicesModalOpen, setChoicesModalOpen] = useState(false);
const [editedChoices, setEditedChoices] = useState<string[]>([]);
// Trending topics state
const [trendingModalOpen, setTrendingModalOpen] = useState(false);
const [trendingLoading, setTrendingLoading] = useState(false);
// Rotate placeholder every 3 seconds
useEffect(() => {
if (!topicInput) {
@@ -582,9 +587,11 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
isUrl={isUrl}
showAIDetailsButton={showAIDetailsButton}
onAIDetailsClick={handleAIDetailsClick}
onTrendingTopicsClick={() => setTrendingModalOpen(true)}
placeholderIndex={placeholderIndex}
loading={enhancingTopic}
loadingMessage={enhanceTopicMessage}
trendingLoading={trendingLoading}
estimatedCost={null}
duration={duration}
speakers={speakers}
@@ -651,6 +658,14 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
onSelectChoice={handleChoiceSelection}
loading={enhancingTopic}
/>
{/* Trending Topics Modal */}
<TrendingTopicsModal
open={trendingModalOpen}
onClose={() => setTrendingModalOpen(false)}
onSelectTopic={(topic) => setTopicInput(topic)}
initialKeywords={topicInput}
/>
</Stack>
</Paper>
);

View File

@@ -1,6 +1,6 @@
import React from "react";
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 { AutoAwesome as AutoAwesomeIcon, AttachMoney as AttachMoneyIcon, TrendingUp as TrendingUpIcon } from "@mui/icons-material";
import { Knobs } from "../types";
export const TOPIC_PLACEHOLDERS = [
@@ -18,9 +18,11 @@ interface TopicUrlInputProps {
isUrl: boolean;
showAIDetailsButton: boolean;
onAIDetailsClick?: () => void;
onTrendingTopicsClick?: () => void;
placeholderIndex: number;
loading?: boolean;
loadingMessage?: string;
trendingLoading?: boolean;
estimatedCost?: {
ttsCost: number;
avatarCost: number;
@@ -39,9 +41,11 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
isUrl,
showAIDetailsButton,
onAIDetailsClick,
onTrendingTopicsClick,
placeholderIndex,
loading = false,
loadingMessage,
trendingLoading = false,
estimatedCost,
duration = 1,
speakers = 1,
@@ -249,9 +253,47 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
/>
</Tooltip>
{/* Enhance topic with AI button - appears when user types (and not a URL) */}
{/* Enhance topic with AI button + Get Trending Topics - appears when user types (and not a URL) */}
{showAIDetailsButton && !isUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5, flexDirection: "column", alignItems: "flex-end", gap: 0.6 }}>
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5, flexDirection: { xs: "column", sm: "row" }, alignItems: { xs: "stretch", sm: "flex-end" }, gap: 1 }}>
<Button
size="small"
variant="contained"
startIcon={
trendingLoading ? (
<CircularProgress size={14} thickness={5} sx={{ color: "rgba(255,255,255,0.92)" }} />
) : (
<TrendingUpIcon />
)
}
onClick={onTrendingTopicsClick}
disabled={trendingLoading || loading}
sx={{
textTransform: "none",
fontSize: "0.875rem",
fontWeight: 600,
borderRadius: 2.5,
color: "#f8fbff",
px: 2,
py: 0.75,
border: "1px solid rgba(16, 185, 129, 0.4)",
background: "linear-gradient(120deg, #10b981 0%, #059669 55%, #047857 100%)",
boxShadow: "0 8px 18px rgba(16, 185, 129, 0.28), inset 0 1px 0 rgba(255,255,255,0.22)",
"&:hover": {
background: "linear-gradient(120deg, #34d399 0%, #10b981 50%, #059669 100%)",
boxShadow: "0 12px 24px rgba(16, 185, 129, 0.35), inset 0 1px 0 rgba(255,255,255,0.26)",
transform: "translateY(-1px)",
},
"&.Mui-disabled": {
color: "#e2e8f0",
borderColor: "rgba(110, 231, 183, 0.7)",
background: "linear-gradient(120deg, #10b981 0%, #059669 55%, #047857 100%)",
opacity: 0.78,
},
}}
>
{trendingLoading ? "Fetching Trends..." : "Get Trending Topics"}
</Button>
<Button
size="small"
variant="contained"
@@ -263,7 +305,7 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
)
}
onClick={onAIDetailsClick}
disabled={loading}
disabled={loading || trendingLoading}
sx={{
textTransform: "none",
fontSize: "0.875rem",
@@ -290,13 +332,13 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
>
{loading ? "Enhancing Topic With AI..." : "Enhance Topic With AI"}
</Button>
{loading && (
<Typography sx={{ fontSize: "0.75rem", color: "#1d4ed8", fontWeight: 600 }}>
{loadingMessage || "Analyzing your topic and improving clarity..."}
</Typography>
)}
</Box>
)}
{loading && (
<Typography sx={{ fontSize: "0.75rem", color: "#1d4ed8", fontWeight: 600, mt: 0.5, textAlign: "right" }}>
{loadingMessage || "Analyzing your topic and improving clarity..."}
</Typography>
)}
</Box>
</Box>
);

View File

@@ -0,0 +1,475 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
Tabs,
Tab,
Chip,
Stack,
CircularProgress,
Alert,
LinearProgress,
IconButton,
alpha,
} from "@mui/material";
import {
TrendingUp as TrendingUpIcon,
Close as CloseIcon,
Public as PublicIcon,
Search as SearchIcon,
AutoAwesome as AutoAwesomeIcon,
} from "@mui/icons-material";
import { TrendsChart } from "../../Research/steps/components/TrendsChart";
import { GoogleTrendsData } from "../../Research/types/intent.types";
import { podcastApi } from "../../../services/podcastApi";
interface TrendingTopicsModalProps {
open: boolean;
onClose: () => void;
onSelectTopic: (topic: string) => void;
initialKeywords: string;
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => (
<Box role="tabpanel" hidden={value !== index} sx={{ pt: 2 }}>
{value === index && children}
</Box>
);
export const TrendingTopicsModal: React.FC<TrendingTopicsModalProps> = ({
open,
onClose,
onSelectTopic,
initialKeywords,
}) => {
const [trendsData, setTrendsData] = useState<GoogleTrendsData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tabValue, setTabValue] = useState(0);
const fetchTrends = useCallback(async () => {
if (!initialKeywords.trim()) return;
const keywords = initialKeywords
.split(/[,;]+/)
.map((k) => k.trim())
.filter(Boolean)
.slice(0, 5);
if (keywords.length === 0) return;
setLoading(true);
setError(null);
setTrendsData(null);
try {
const result = await podcastApi.getTrendingTopics({
keywords,
timeframe: "today 12-m",
geo: "US",
});
if (result.success && result.data) {
setTrendsData(result.data as GoogleTrendsData);
} else {
setError(result.error || "Failed to fetch trends data");
}
} catch (err: any) {
setError(err?.response?.data?.detail || err?.message || "Failed to fetch trending topics");
} finally {
setLoading(false);
}
}, [initialKeywords]);
useEffect(() => {
if (open && initialKeywords.trim()) {
fetchTrends();
}
}, [open, initialKeywords, fetchTrends]);
const handleSelectTopic = (topic: string) => {
onSelectTopic(topic);
onClose();
};
const handleClose = () => {
setTrendsData(null);
setError(null);
setTabValue(0);
onClose();
};
const regions = trendsData?.interest_by_region || [];
const relatedTopics = trendsData?.related_topics || { top: [], rising: [] };
const relatedQueries = trendsData?.related_queries || { top: [], rising: [] };
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
maxHeight: "90vh",
},
}}
>
<DialogTitle
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
pb: 1,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%)",
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Box
sx={{
p: 0.75,
borderRadius: 1.5,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<TrendingUpIcon sx={{ color: "#fff", fontSize: "1.25rem" }} />
</Box>
<Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: "#0f172a", fontSize: "1.1rem" }}>
Trending Topics
</Typography>
<Typography variant="caption" sx={{ color: "#64748b" }}>
Google Trends insights for &ldquo;{initialKeywords}&rdquo;
</Typography>
</Box>
</Stack>
<IconButton onClick={handleClose} sx={{ color: "#64748b" }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ px: 3, py: 2 }}>
{loading && (
<Box sx={{ py: 4, textAlign: "center" }}>
<CircularProgress size={40} sx={{ color: "#667eea", mb: 2 }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
Fetching trending topics from Google Trends...
</Typography>
<LinearProgress sx={{ mt: 2, borderRadius: 1 }} />
</Box>
)}
{error && (
<Alert severity="error" sx={{ mt: 2, borderRadius: 2 }}>
{error}
</Alert>
)}
{!loading && trendsData && (
<>
<Tabs
value={tabValue}
onChange={(_, v) => setTabValue(v)}
variant="scrollable"
scrollButtons="auto"
sx={{
borderBottom: "1px solid rgba(0,0,0,0.08)",
"& .MuiTab-root": {
textTransform: "none",
fontWeight: 600,
fontSize: "0.875rem",
},
"& .Mui-selected": {
color: "#667eea",
},
"& .MuiTabs-indicator": {
background: "linear-gradient(90deg, #667eea 0%, #764ba2 100%)",
},
}}
>
<Tab icon={<TrendingUpIcon sx={{ fontSize: "1rem" }} />} iconPosition="start" label="Interest Chart" />
<Tab icon={<PublicIcon sx={{ fontSize: "1rem" }} />} iconPosition="start" label="Regions" />
<Tab icon={<AutoAwesomeIcon sx={{ fontSize: "1rem" }} />} iconPosition="start" label="Related Topics" />
<Tab icon={<SearchIcon sx={{ fontSize: "1rem" }} />} iconPosition="start" label="Related Queries" />
</Tabs>
<TabPanel value={tabValue} index={0}>
<Box sx={{ mt: 1 }}>
<TrendsChart data={trendsData} height={280} showAverage={true} />
</Box>
</TabPanel>
<TabPanel value={tabValue} index={1}>
{regions.length === 0 ? (
<Box sx={{ py: 3, textAlign: "center" }}>
<PublicIcon sx={{ fontSize: 40, color: "#cbd5e1", mb: 1 }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
No regional data available for this topic.
</Typography>
</Box>
) : (
<Stack spacing={1} sx={{ maxHeight: 350, overflow: "auto" }}>
{regions.slice(0, 15).map((region: any, idx: number) => {
const regionName = region.regionName || region.geoName || region.name || `Region ${idx + 1}`;
const value = region.value || region.interest || 0;
const maxVal = Math.max(...regions.slice(0, 15).map((r: any) => r.value || r.interest || 0));
const pct = maxVal > 0 ? (value / maxVal) * 100 : 0;
return (
<Box
key={idx}
sx={{
display: "flex",
alignItems: "center",
gap: 1.5,
p: 1,
borderRadius: 1,
"&:hover": { background: "rgba(102, 126, 234, 0.04)" },
}}
>
<Typography variant="body2" sx={{ minWidth: 30, fontWeight: 600, color: "#64748b" }}>
{idx + 1}
</Typography>
<Typography variant="body2" sx={{ flex: 1, fontWeight: 500, color: "#0f172a" }}>
{regionName}
</Typography>
<Box sx={{ flex: 1, maxWidth: 200 }}>
<Box
sx={{
height: 8,
borderRadius: 4,
background: "rgba(102, 126, 234, 0.1)",
position: "relative",
}}
>
<Box
sx={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: `${pct}%`,
borderRadius: 4,
background: "linear-gradient(90deg, #667eea 0%, #764ba2 100%)",
transition: "width 0.3s ease",
}}
/>
</Box>
</Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: "#667eea", minWidth: 30 }}>
{value}
</Typography>
</Box>
);
})}
</Stack>
)}
</TabPanel>
<TabPanel value={tabValue} index={2}>
{relatedTopics.top.length === 0 && relatedTopics.rising.length === 0 ? (
<Box sx={{ py: 3, textAlign: "center" }}>
<AutoAwesomeIcon sx={{ fontSize: 40, color: "#cbd5e1", mb: 1 }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
No related topics data available.
</Typography>
</Box>
) : (
<Stack spacing={2}>
{relatedTopics.rising.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#059669", fontWeight: 700 }}>
Rising Topics
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{relatedTopics.rising.map((topic: any, idx: number) => {
const label = topic.topic_title || topic.title || topic.query || String(topic);
return (
<Chip
key={idx}
label={label}
size="small"
onClick={() => handleSelectTopic(label)}
sx={{
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)",
color: "#059669",
border: "1px solid rgba(16, 185, 129, 0.3)",
fontWeight: 600,
fontSize: "0.75rem",
cursor: "pointer",
mb: 0.5,
"&:hover": {
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(5, 150, 105, 0.2) 100%)",
},
}}
/>
);
})}
</Stack>
</Box>
)}
{relatedTopics.top.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#667eea", fontWeight: 700 }}>
Top Topics
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{relatedTopics.top.map((topic: any, idx: number) => {
const label = topic.topic_title || topic.title || topic.query || String(topic);
return (
<Chip
key={idx}
label={label}
size="small"
onClick={() => handleSelectTopic(label)}
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)",
color: "#667eea",
border: "1px solid rgba(102, 126, 234, 0.25)",
fontWeight: 600,
fontSize: "0.75rem",
cursor: "pointer",
mb: 0.5,
"&:hover": {
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.18) 0%, rgba(118, 75, 162, 0.18) 100%)",
},
}}
/>
);
})}
</Stack>
</Box>
)}
</Stack>
)}
</TabPanel>
<TabPanel value={tabValue} index={3}>
{relatedQueries.top.length === 0 && relatedQueries.rising.length === 0 ? (
<Box sx={{ py: 3, textAlign: "center" }}>
<SearchIcon sx={{ fontSize: 40, color: "#cbd5e1", mb: 1 }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
No related queries data available.
</Typography>
</Box>
) : (
<Stack spacing={2}>
{relatedQueries.rising.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#059669", fontWeight: 700 }}>
Rising Queries
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{relatedQueries.rising.map((query: any, idx: number) => {
const label = query.query || query.title || String(query);
return (
<Chip
key={idx}
label={label}
size="small"
onClick={() => handleSelectTopic(label)}
sx={{
background: "linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%)",
color: "#d97706",
border: "1px solid rgba(245, 158, 11, 0.25)",
fontWeight: 600,
fontSize: "0.75rem",
cursor: "pointer",
mb: 0.5,
"&:hover": {
background: "linear-gradient(135deg, rgba(245, 158, 11, 0.18) 0%, rgba(217, 119, 6, 0.18) 100%)",
},
}}
/>
);
})}
</Stack>
</Box>
)}
{relatedQueries.top.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#667eea", fontWeight: 700 }}>
Top Queries
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{relatedQueries.top.map((query: any, idx: number) => {
const label = query.query || query.title || String(query);
return (
<Chip
key={idx}
label={label}
size="small"
onClick={() => handleSelectTopic(label)}
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)",
color: "#667eea",
border: "1px solid rgba(102, 126, 234, 0.25)",
fontWeight: 600,
fontSize: "0.75rem",
cursor: "pointer",
mb: 0.5,
"&:hover": {
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.18) 0%, rgba(118, 75, 162, 0.18) 100%)",
},
}}
/>
);
})}
</Stack>
</Box>
)}
</Stack>
)}
</TabPanel>
</>
)}
{!loading && !error && !trendsData && (
<Box sx={{ py: 4, textAlign: "center" }}>
<TrendingUpIcon sx={{ fontSize: 48, color: "#cbd5e1", mb: 1 }} />
<Typography variant="body2" sx={{ color: "#64748b" }}>
Enter a topic and click &ldquo;Get Trending Topics&rdquo; to see Google Trends data.
</Typography>
</Box>
)}
</DialogContent>
<DialogActions
sx={{
px: 3,
py: 2,
borderTop: "1px solid rgba(0,0,0,0.08)",
justifyContent: "space-between",
}}
>
<Typography variant="caption" sx={{ color: "#94a3b8" }}>
Data from Google Trends
</Typography>
<Button
onClick={handleClose}
sx={{
textTransform: "none",
fontWeight: 600,
color: "#64748b",
}}
>
Close
</Button>
</DialogActions>
</Dialog>
);
};