import React, { useEffect, useMemo, useState } from "react"; import { Box, Paper, Stack, Typography, Alert, Chip, Tooltip, LinearProgress, Stepper, Step, StepLabel, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemButton, ListItemText, Checkbox, CircularProgress, alpha, } from "@mui/material"; import { Mic as MicIcon, Psychology as PsychologyIcon, Search as SearchIcon, EditNote as EditNoteIcon, PlayArrow as PlayArrowIcon, CheckCircle as CheckCircleIcon, Info as InfoIcon, AutoAwesome as AutoAwesomeIcon, Insights as InsightsIcon, LibraryMusic as LibraryMusicIcon, } from "@mui/icons-material"; import { ResearchProvider } from "../../services/blogWriterApi"; import { podcastApi } from "../../services/podcastApi"; import { usePodcastProjectState } from "../../hooks/usePodcastProjectState"; import { useNavigate } from "react-router-dom"; import { CreateProjectPayload, Job, Knobs, Query, Research, Script } from "./types"; import { GlassyCard, glassyCardSx, PrimaryButton, SecondaryButton } from "./ui"; import { CreateModal } from "./CreateModal"; import { AnalysisPanel } from "./AnalysisPanel"; import { FactCard } from "./FactCard"; import { ScriptEditor } from "./ScriptEditor"; import { RenderQueue } from "./RenderQueue"; import { RecentEpisodesPreview } from "./RecentEpisodesPreview"; import { ProjectList } from "./ProjectList"; import { usePreflightCheck } from "../../hooks/usePreflightCheck"; import { useBudgetTracking } from "../../hooks/useBudgetTracking"; import { PreflightBlockDialog } from "./PreflightBlockDialog"; import HeaderControls from "../shared/HeaderControls"; /* ================= Helpers ================= */ const DEFAULT_KNOBS: Knobs = { voice_emotion: "neutral", voice_speed: 1, resolution: "720p", scene_length_target: 45, sample_rate: 24000, bitrate: "standard", }; const announceError = (setAnnouncement: (msg: string) => void, error: unknown) => { const message = error instanceof Error ? error.message : "Unexpected error"; setAnnouncement(message); }; /* ================= Dashboard ================= */ const PodcastDashboard: React.FC = () => { const navigate = useNavigate(); const projectState = usePodcastProjectState(); const [showProjectList, setShowProjectList] = useState(false); const { project, analysis, queries, selectedQueries, research, rawResearch, estimate, scriptData, renderJobs, knobs: knobsState, researchProvider, showScriptEditor, showRenderQueue, currentStep, setProject, setAnalysis, setQueries, setSelectedQueries, setResearch, setRawResearch, setEstimate, setScriptData, updateRenderJob, setKnobs, setResearchProvider, setBudgetCap, setShowScriptEditor, setShowRenderQueue, initializeProject, resetState, loadProjectFromDb, } = projectState; const [isAnalyzing, setIsAnalyzing] = useState(false); const [isResearching, setIsResearching] = useState(false); const [announcement, setAnnouncement] = useState(""); const [showResumeAlert, setShowResumeAlert] = useState(false); const [showPreflightDialog, setShowPreflightDialog] = useState(false); const [preflightResponse, setPreflightResponse] = useState(null); const [preflightOperationName, setPreflightOperationName] = useState(""); // Budget tracking const budgetTracking = useBudgetTracking(projectState.budgetCap || 50); // Preflight check hook const preflightCheck = usePreflightCheck({ onBlocked: (response) => { setPreflightResponse(response); setShowPreflightDialog(true); }, }); // Update budget cap when project state changes useEffect(() => { if (projectState.budgetCap) { budgetTracking.setBudgetCap(projectState.budgetCap); } }, [projectState.budgetCap, budgetTracking]); // Check if we have a saved project on mount useEffect(() => { if (project && currentStep && currentStep !== "create") { setShowResumeAlert(true); setTimeout(() => setShowResumeAlert(false), 5000); } }, []); // Only on mount useEffect(() => { if (announcement) { const t = setTimeout(() => setAnnouncement(""), 4000); return () => clearTimeout(t); } return undefined; }, [announcement]); const handleCreate = async (payload: CreateProjectPayload) => { // Prevent duplicate submits that can spam story setup API if (isAnalyzing) return; setResearch(null); setRawResearch(null); setScriptData(null); setShowScriptEditor(false); setShowRenderQueue(false); try { setIsAnalyzing(true); setAnnouncement("Analyzing your idea — AI suggestions incoming"); const result = await podcastApi.createProject(payload); await initializeProject(payload, result.projectId); setProject({ id: result.projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers }); setAnalysis(result.analysis); setEstimate(result.estimate); setQueries(result.queries); setSelectedQueries(new Set(result.queries.map((q) => q.id))); setKnobs(payload.knobs); setBudgetCap(payload.budgetCap); setAnnouncement("Analysis complete"); } catch (error) { announceError(setAnnouncement, error); } finally { setIsAnalyzing(false); } }; const handleRunResearch = async () => { // Prevent duplicate research calls if (isResearching) return; if (!project) { setAnnouncement("Create a project first."); return; } if (selectedQueries.size === 0) { setAnnouncement("Select at least one query to research."); return; } // Preflight check before research setPreflightOperationName("Research"); const approvedQueries = queries.filter((q) => selectedQueries.has(q.id)); const preflightResult = await preflightCheck.check({ provider: researchProvider === "exa" ? "exa" : "gemini", operation_type: researchProvider === "exa" ? "exa_neural_search" : "google_grounding", tokens_requested: researchProvider === "exa" ? 0 : 1200, actual_provider_name: researchProvider || "google", }); if (!preflightResult.can_proceed) { return; // Dialog will be shown by onBlocked callback } try { setIsResearching(true); setAnnouncement(`Starting ${researchProvider === "exa" ? "deep" : "standard"} research — this may take a moment...`); setResearch(null); setRawResearch(null); setScriptData(null); setShowScriptEditor(false); setShowRenderQueue(false); try { const { research: mapped, raw } = await podcastApi.runResearch({ projectId: project.id, topic: project.idea, approvedQueries, provider: researchProvider, onProgress: (message) => { // Update announcement with progress messages setAnnouncement(message); }, }); setResearch(mapped); setRawResearch(raw); setAnnouncement("Research complete — review fact cards below"); } catch (researchError) { const errorMessage = researchError instanceof Error ? researchError.message : "Research failed. Please try again or switch to Standard Research."; // Provide helpful error messages if (errorMessage.includes("Exa") || errorMessage.includes("exa")) { setAnnouncement(`Deep research failed: ${errorMessage}. Try Standard Research instead.`); } else if (errorMessage.includes("timeout")) { setAnnouncement("Research timed out. Please try again with fewer queries."); } else { setAnnouncement(`Research failed: ${errorMessage}`); } // Log full error for debugging console.error("Research error:", researchError); throw researchError; } } catch (error) { announceError(setAnnouncement, error); } finally { setIsResearching(false); } }; const handleGenerateScript = async () => { // Avoid re-triggering script generation preflight if (showScriptEditor) return; if (!project || !research) { setAnnouncement("Project or research missing — cannot generate script"); return; } // Preflight check before script generation setPreflightOperationName("Script Generation"); const preflightResult = await preflightCheck.check({ provider: "gemini", operation_type: "script_generation", tokens_requested: 2000, actual_provider_name: "gemini", }); if (!preflightResult.can_proceed) { return; // Dialog will be shown by onBlocked callback } setScriptData(null); setShowRenderQueue(false); setShowScriptEditor(true); }; const handleProceedToRendering = (script: Script) => { setScriptData(script); // Initialize render jobs if empty if (renderJobs.length === 0) { script.scenes.forEach((scene) => { updateRenderJob(scene.id, { sceneId: scene.id, title: scene.title, status: "idle" as const, progress: 0, previewUrl: null, finalUrl: null, jobId: null, }); }); } setShowRenderQueue(true); setShowScriptEditor(false); }; const selectedCount = selectedQueries.size; const canGenerateScript = Boolean(project && research && rawResearch); const toggleQuery = (id: string) => { if (isResearching) return; const current = selectedQueries; const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); setSelectedQueries(next); }; const activeStep = useMemo(() => { if (showRenderQueue) return 3; if (showScriptEditor) return 2; if (research) return 1; if (analysis) return 0; return -1; }, [showRenderQueue, showScriptEditor, research, analysis]); const steps = [ { label: "Analysis", icon: , description: "AI analyzes your idea" }, { label: "Research", icon: , description: "Gather facts and citations" }, { label: "Script", icon: , description: "Edit and approve scenes" }, { label: "Render", icon: , description: "Generate audio files" }, ]; const handleSelectProject = async (projectId: string) => { try { await loadProjectFromDb(projectId); setShowProjectList(false); } catch (error) { setAnnouncement(`Failed to load project: ${error instanceof Error ? error.message : "Unknown error"}`); } }; if (showProjectList) { return ; } return ( {/* Header */} AI Podcast Maker Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration window.open("/docs", "_blank")} startIcon={}> Help navigate("/asset-library?source_module=podcast_maker&asset_type=audio")} startIcon={} tooltip="View all podcast episodes in Asset Library" > My Episodes setShowProjectList(true)} startIcon={} tooltip="View and resume saved projects" > My Projects { resetState(); setShowProjectList(false); }} startIcon={} > New Episode {/* Progress Stepper */} {project && activeStep >= 0 && ( {steps.map((step, index) => ( ( {completed ? : step.icon} )} > {step.label} {step.description} ))} )} {/* Resume Alert */} {showResumeAlert && project && ( setShowResumeAlert(false)} sx={{ background: "#d1fae5", border: "1px solid #a7f3d0", "& .MuiAlert-icon": { color: "#10b981" }, }} > Project Restored: Resuming from{" "} {currentStep === "analysis" ? "Analysis" : currentStep === "research" ? "Research" : currentStep === "script" ? "Script Editing" : "Rendering"}{" "} step. Your progress has been saved. )} {/* Announcements */} {announcement && ( setAnnouncement("")} sx={{ background: "#dbeafe", border: "1px solid #bfdbfe", "& .MuiAlert-icon": { color: "#3b82f6" }, }} > {announcement} )} {(isAnalyzing || isResearching) && ( } sx={{ background: "#fef3c7", border: "1px solid #fde68a", }} > {isAnalyzing ? "Analyzing your idea with AI..." : "Running research... This may take a moment."} )} {/* Create Modal */} {!project && ( <> {/* Recent Episodes Preview */} {}} /> )} {/* Main Content */} {analysis && !showScriptEditor && !showRenderQueue && ( setAnalysis({ ...analysis })} /> )} {estimate && !showScriptEditor && !showRenderQueue && ( Estimated Cost ${estimate.total.toFixed(2)} )} {queries.length > 0 && !showScriptEditor && !showRenderQueue && ( Research Queries Provider 0 ? "primary" : "default"} /> {researchProvider === "google" ? "Select at least one query (recommended: 3+ for balanced coverage). Standard research provides fact-checked results with source citations." : "Select queries for deep research. This mode provides comprehensive analysis with competitor insights and trending topics."} {queries.map((q) => ( toggleQuery(q.id)} disabled={isResearching} sx={{ borderRadius: 2, mb: 1, border: "1px solid rgba(0,0,0,0.08)", background: "#f8fafc", "&:hover": { background: alpha("#667eea", 0.08) }, }} > ))} } tooltip={ selectedCount === 0 ? "Select at least one query to run research" : `Run research with ${selectedCount} selected ${selectedCount === 1 ? "query" : "queries"}` } > {isResearching ? "Running Research..." : "Run Research"} )} {research && !showScriptEditor && !showRenderQueue && ( Research Summary {research.summary} } tooltip={!canGenerateScript ? "Complete research to generate script" : "Generate AI-powered script from research"} > Generate Script {research.factCards.length > 0 && ( <> Fact Cards ({research.factCards.length}) {research.factCards.map((fact) => ( ))} )} )} {showScriptEditor && project && research && rawResearch && ( setScriptData(s)} onBackToResearch={() => setShowScriptEditor(false)} onProceedToRendering={(s) => handleProceedToRendering(s)} onError={(msg) => setAnnouncement(msg)} /> )} {showScriptEditor && (!research || !rawResearch) && ( Complete a research run before opening the script editor. )} {showRenderQueue && project && scriptData && ( { setShowRenderQueue(false); setShowScriptEditor(true); }} onError={(msg) => setAnnouncement(msg)} /> )} {/* Preflight Block Dialog */} { setShowPreflightDialog(false); setPreflightResponse(null); }} response={preflightResponse} operationName={preflightOperationName} /> ); }; export default PodcastDashboard;