From 280159669bf6261078634b150aae64ccf1f4cba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Sun, 19 Apr 2026 16:39:49 +0530 Subject: [PATCH] Add accessible cost estimate chip and phase breakdown in podcast header --- .../PodcastMaker/PodcastDashboard.tsx | 48 +++++- .../PodcastMaker/PodcastDashboard/Header.tsx | 149 +++++++++++++++++- .../PodcastDashboard/ResearchSummary.tsx | 14 -- 3 files changed, 189 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx index 82d97867..b3d9c31c 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard.tsx @@ -1,7 +1,8 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback, useEffect, useMemo } from "react"; import { shouldSkipOnboarding } from '../../utils/demoMode'; import { Box, Paper, Stack, Alert, Divider, CircularProgress, alpha, Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@mui/material"; import { usePodcastProjectState } from "../../hooks/usePodcastProjectState"; +import { PodcastCostEst } from "./types"; import { CreateModal } from "./CreateModal"; import { AnalysisPanel } from "./AnalysisPanel"; import { ScriptEditor } from "./ScriptEditor"; @@ -68,6 +69,50 @@ const PodcastDashboard: React.FC = () => { }); const [showRegenModal, setShowRegenModal] = useState(false); + const headerCostEst = useMemo(() => { + const defaultBreakdown: PodcastCostEst["breakdown"] = [ + { phase: "Analyze", cost: 0 }, + { phase: "Gather", cost: 0 }, + { phase: "Write", cost: 0 }, + { phase: "Produce", cost: 0 }, + ]; + + if (!estimate && !research?.costEst) { + return null; + } + + const breakdownMap = new Map(defaultBreakdown.map((item) => [item.phase, item.cost])); + + if (research?.costEst?.breakdown?.length) { + research.costEst.breakdown.forEach((item) => { + breakdownMap.set(item.phase, Number(item.cost) || 0); + }); + } + + if (estimate) { + const gatherCost = breakdownMap.get("Gather") || 0; + const produceCost = breakdownMap.get("Produce") || 0; + if (gatherCost === 0 && estimate.researchCost > 0) { + breakdownMap.set("Gather", estimate.researchCost); + } + if (produceCost === 0) { + breakdownMap.set("Produce", estimate.ttsCost + estimate.avatarCost + estimate.videoCost); + } + } + + const breakdown: PodcastCostEst["breakdown"] = defaultBreakdown.map((item) => ({ + phase: item.phase, + cost: breakdownMap.get(item.phase) || 0, + })); + const total = breakdown.reduce((sum, item) => sum + item.cost, 0); + + return { + total, + breakdown, + currency: "USD", + last_updated: research?.costEst?.last_updated || new Date().toISOString(), + }; + }, [estimate, research?.costEst]); const handleSelectProject = useCallback(async (projectId: string) => { try { @@ -122,6 +167,7 @@ const PodcastDashboard: React.FC = () => { ...(scriptData ? [2] : []), ...(renderJobs.some(j => j.status === "completed") ? [3] : []), ]} + costEst={headerCostEst} onStepClick={(step) => { // Handle step clicks - could navigate to different views }} diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard/Header.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard/Header.tsx index 7c5248e4..a586b026 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard/Header.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard/Header.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; -import { Stack, Typography, Box, IconButton, Menu, MenuItem, Divider, ListItemIcon, ListItemText, Collapse } from "@mui/material"; +import { Stack, Typography, Box, IconButton, Menu, MenuItem, Divider, ListItemIcon, ListItemText, Collapse, Chip, Popover, ButtonBase, useMediaQuery } from "@mui/material"; import { Mic as MicIcon, Menu as MenuIcon, Close as CloseIcon, - AutoAwesome as AutoAwesomeIcon, + AttachMoney as AttachMoneyIcon, LibraryMusic as LibraryMusicIcon, Folder as FolderIcon, Help as HelpIcon, Add as AddIcon, - BarChart as BarChartIcon, } from "@mui/icons-material"; +import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; -import { PrimaryButton } from "../ui"; +import { PodcastCostEst } from "../types"; import HeaderControls from "../../shared/HeaderControls"; import { ProgressStepper } from "./ProgressStepper"; @@ -22,12 +22,22 @@ interface HeaderProps { activeStep?: number; completedSteps?: number[]; onStepClick?: (stepIndex: number) => void; + costEst?: PodcastCostEst | null; } -export const Header: React.FC = ({ onShowProjects, onNewEpisode, activeStep = -1, completedSteps = [], onStepClick }) => { +const COST_PHASE_ORDER: PodcastCostEst["breakdown"][number]["phase"][] = ["Analyze", "Gather", "Write", "Produce"]; + +export const Header: React.FC = ({ onShowProjects, onNewEpisode, activeStep = -1, completedSteps = [], onStepClick, costEst }) => { const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const [anchorEl, setAnchorEl] = useState(null); + const [costAnchorEl, setCostAnchorEl] = useState(null); + const [isMobileCostOpen, setIsMobileCostOpen] = useState(false); const isMenuOpen = Boolean(anchorEl); + const isCostOpen = Boolean(costAnchorEl); + const costTriggerId = "podcast-cost-est-trigger"; + const costBreakdownId = "podcast-cost-est-breakdown"; const handleMenuOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -57,6 +67,35 @@ export const Header: React.FC = ({ onShowProjects, onNewEpisode, ac onNewEpisode(); }; + const handleCostToggle = (event: React.MouseEvent) => { + if (!costEst) return; + if (isMobile) { + setIsMobileCostOpen((prev) => !prev); + return; + } + setCostAnchorEl((prev) => (prev ? null : event.currentTarget)); + }; + + const handleCostKeyDown = (event: React.KeyboardEvent) => { + if (!costEst) return; + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + if (isMobile) { + setIsMobileCostOpen((prev) => !prev); + } else { + setCostAnchorEl((prev) => (prev ? null : event.currentTarget)); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setCostAnchorEl(null); + setIsMobileCostOpen(false); + } + }; + + const handleCloseCostPopover = () => { + setCostAnchorEl(null); + }; + return ( = ({ onShowProjects, onNewEpisode, ac {/* Right side - Hamburger Menu + HeaderControls + Create */} - + + {costEst && ( + + } + label={`Cost Est $${costEst.total.toFixed(2)}`} + size="small" + sx={{ + cursor: "pointer", + background: "rgba(245, 158, 11, 0.12)", + color: "#92400e", + fontWeight: 700, + border: "1px solid rgba(245, 158, 11, 0.3)", + "& .MuiChip-label": { + px: 1.2, + }, + }} + /> + + )} + {/* Header Controls (alerts + user) */} @@ -235,6 +302,74 @@ export const Header: React.FC = ({ onShowProjects, onNewEpisode, ac + {costEst && ( + <> + + + {COST_PHASE_ORDER.map((phase) => { + const phaseCost = costEst.breakdown.find((item) => item.phase === phase)?.cost || 0; + return ( + + + {phase} + + + ${phaseCost.toFixed(2)} + + + ); + })} + + + + + + + {COST_PHASE_ORDER.map((phase) => { + const phaseCost = costEst.breakdown.find((item) => item.phase === phase)?.cost || 0; + return ( + + + {phase} + + + ${phaseCost.toFixed(2)} + + + ); + })} + + + + + )} + {/* Progress Stepper - integrated into header when active */} = 0} timeout={400}> @@ -247,4 +382,4 @@ export const Header: React.FC = ({ onShowProjects, onNewEpisode, ac ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx b/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx index 166bd304..8b25ccd6 100644 --- a/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx +++ b/frontend/src/components/PodcastMaker/PodcastDashboard/ResearchSummary.tsx @@ -3,7 +3,6 @@ import { Stack, Typography, Chip, Divider, Box, alpha, Paper, Stepper, Step, Ste import { Insights as InsightsIcon, Search as SearchIcon, - AttachMoney as AttachMoneyIcon, EditNote as EditNoteIcon, Article as ArticleIcon, AutoAwesome as AutoAwesomeIcon, @@ -130,19 +129,6 @@ export const ResearchSummary: React.FC = ({ }} /> )} - {research.costEst?.total !== undefined && ( - } - label={`$${research.costEst.total.toFixed(3)}`} - size="small" - sx={{ - background: alpha("#f59e0b", 0.1), - color: "#d97706", - fontWeight: 600, - border: "1px solid rgba(245, 158, 11, 0.2)", - }} - /> - )}