AI podcast maker performance optimizations
This commit is contained in:
@@ -1,79 +1,26 @@
|
||||
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 React, { useState, useCallback } from "react";
|
||||
import { Box, Paper, Stack, Alert, Divider, CircularProgress, alpha } from "@mui/material";
|
||||
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 { Script } from "./types";
|
||||
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 ================= */
|
||||
import {
|
||||
Header,
|
||||
ProgressStepper,
|
||||
EstimateCard,
|
||||
QuerySelection,
|
||||
ResearchSummary,
|
||||
usePodcastWorkflow,
|
||||
DEFAULT_KNOBS,
|
||||
getStepLabel,
|
||||
} from "./PodcastDashboard/index";
|
||||
|
||||
const PodcastDashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const projectState = usePodcastProjectState();
|
||||
const [showProjectList, setShowProjectList] = useState(false);
|
||||
const {
|
||||
@@ -91,250 +38,39 @@ const PodcastDashboard: React.FC = () => {
|
||||
showScriptEditor,
|
||||
showRenderQueue,
|
||||
currentStep,
|
||||
setProject,
|
||||
setAnalysis,
|
||||
setQueries,
|
||||
setSelectedQueries,
|
||||
setResearch,
|
||||
setRawResearch,
|
||||
setEstimate,
|
||||
setScriptData,
|
||||
updateRenderJob,
|
||||
setKnobs,
|
||||
setResearchProvider,
|
||||
setBudgetCap,
|
||||
setShowScriptEditor,
|
||||
setShowRenderQueue,
|
||||
initializeProject,
|
||||
setResearchProvider,
|
||||
updateRenderJob,
|
||||
resetState,
|
||||
loadProjectFromDb,
|
||||
setCurrentStep,
|
||||
} = 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<any>(null);
|
||||
const [preflightOperationName, setPreflightOperationName] = useState<string>("");
|
||||
|
||||
// Budget tracking
|
||||
const budgetTracking = useBudgetTracking(projectState.budgetCap || 50);
|
||||
|
||||
// Preflight check hook
|
||||
const preflightCheck = usePreflightCheck({
|
||||
onBlocked: (response) => {
|
||||
setPreflightResponse(response);
|
||||
setShowPreflightDialog(true);
|
||||
const workflow = usePodcastWorkflow({
|
||||
projectState,
|
||||
onError: (msg: string) => {
|
||||
// Error handling is done through workflow's own announcement system
|
||||
console.error("Workflow error:", msg);
|
||||
},
|
||||
});
|
||||
|
||||
// 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<string>(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: <PsychologyIcon />, description: "AI analyzes your idea" },
|
||||
{ label: "Research", icon: <SearchIcon />, description: "Gather facts and citations" },
|
||||
{ label: "Script", icon: <EditNoteIcon />, description: "Edit and approve scenes" },
|
||||
{ label: "Render", icon: <PlayArrowIcon />, description: "Generate audio files" },
|
||||
];
|
||||
|
||||
const handleSelectProject = async (projectId: string) => {
|
||||
const handleSelectProject = useCallback(async (projectId: string) => {
|
||||
try {
|
||||
await loadProjectFromDb(projectId);
|
||||
setShowProjectList(false);
|
||||
} catch (error) {
|
||||
setAnnouncement(`Failed to load project: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
const errorMsg = `Failed to load project: ${error instanceof Error ? error.message : "Unknown error"}`;
|
||||
// Use workflow's setAnnouncement - workflow is stable from hook
|
||||
workflow.setAnnouncement(errorMsg);
|
||||
}
|
||||
};
|
||||
}, [loadProjectFromDb, workflow.setAnnouncement]);
|
||||
|
||||
const handleNewEpisode = useCallback(() => {
|
||||
resetState();
|
||||
setShowProjectList(false);
|
||||
}, [resetState]);
|
||||
|
||||
if (showProjectList) {
|
||||
return <ProjectList onSelectProject={handleSelectProject} />;
|
||||
@@ -362,147 +98,93 @@ const PodcastDashboard: React.FC = () => {
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
{/* Header */}
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap={2}>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
color: "#1e293b",
|
||||
fontWeight: 800,
|
||||
mb: 0.5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<MicIcon fontSize="large" sx={{ color: "#667eea" }} />
|
||||
AI Podcast Maker
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
|
||||
<SecondaryButton onClick={() => window.open("/docs", "_blank")} startIcon={<InfoIcon />}>
|
||||
Help
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
onClick={() => navigate("/asset-library?source_module=podcast_maker&asset_type=audio")}
|
||||
startIcon={<LibraryMusicIcon />}
|
||||
tooltip="View all podcast episodes in Asset Library"
|
||||
>
|
||||
My Episodes
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
onClick={() => setShowProjectList(true)}
|
||||
startIcon={<MicIcon />}
|
||||
tooltip="View and resume saved projects"
|
||||
>
|
||||
My Projects
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
resetState();
|
||||
setShowProjectList(false);
|
||||
}}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
>
|
||||
New Episode
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Header onShowProjects={() => setShowProjectList(true)} onNewEpisode={handleNewEpisode} />
|
||||
|
||||
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
|
||||
|
||||
{/* Progress Stepper */}
|
||||
{project && activeStep >= 0 && (
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2.5,
|
||||
background: "#f8fafc",
|
||||
border: "1px solid rgba(0,0,0,0.08)",
|
||||
borderRadius: 2,
|
||||
{project && workflow.activeStep >= 0 && (
|
||||
<ProgressStepper
|
||||
activeStep={workflow.activeStep}
|
||||
completedSteps={[
|
||||
...(analysis ? [0] : []), // Analysis step
|
||||
...(research ? [1] : []), // Research step
|
||||
...(scriptData ? [2] : []), // Script step
|
||||
...(scriptData && renderJobs.length > 0 ? [3] : []), // Render step (if script exists and has jobs)
|
||||
]}
|
||||
onStepClick={(stepIndex) => {
|
||||
// Navigate to the clicked step
|
||||
// Step indices: 0 = Analysis, 1 = Research, 2 = Script, 3 = Render
|
||||
if (stepIndex === 0) {
|
||||
// Navigate to Analysis
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(false);
|
||||
setCurrentStep('analysis');
|
||||
} else if (stepIndex === 1) {
|
||||
// Navigate to Research
|
||||
if (!analysis) {
|
||||
workflow.setAnnouncement("Complete Analysis first to access Research.");
|
||||
return;
|
||||
}
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(false);
|
||||
setCurrentStep('research');
|
||||
} else if (stepIndex === 2) {
|
||||
// Navigate to Script
|
||||
if (!research) {
|
||||
workflow.setAnnouncement("Complete Research first to access Script Editor.");
|
||||
return;
|
||||
}
|
||||
setShowRenderQueue(false);
|
||||
setShowScriptEditor(true);
|
||||
setCurrentStep('script');
|
||||
} else if (stepIndex === 3) {
|
||||
// Navigate to Render
|
||||
if (!scriptData) {
|
||||
workflow.setAnnouncement("Generate and approve script first to access Render Queue.");
|
||||
return;
|
||||
}
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(true);
|
||||
setCurrentStep('render');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stepper activeStep={activeStep} orientation="horizontal" sx={{ "& .MuiStepLabel-root": { cursor: "pointer" } }}>
|
||||
{steps.map((step, index) => (
|
||||
<Step key={step.label} completed={index < activeStep}>
|
||||
<StepLabel
|
||||
StepIconComponent={({ active, completed }) => (
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: completed
|
||||
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
: active
|
||||
? alpha("#667eea", 0.15)
|
||||
: "#e2e8f0",
|
||||
border: active ? "2px solid #667eea" : "1px solid rgba(0,0,0,0.1)",
|
||||
color: completed || active ? "#fff" : "#64748b",
|
||||
}}
|
||||
>
|
||||
{completed ? <CheckCircleIcon /> : step.icon}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Typography variant="subtitle2">{step.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{step.description}
|
||||
</Typography>
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Paper>
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Resume Alert */}
|
||||
{showResumeAlert && project && (
|
||||
{workflow.showResumeAlert && project && (
|
||||
<Alert
|
||||
severity="success"
|
||||
onClose={() => setShowResumeAlert(false)}
|
||||
onClose={() => workflow.setShowResumeAlert(false)}
|
||||
sx={{
|
||||
background: "#d1fae5",
|
||||
border: "1px solid #a7f3d0",
|
||||
"& .MuiAlert-icon": { color: "#10b981" },
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<strong>Project Restored:</strong> Resuming from{" "}
|
||||
{currentStep === "analysis"
|
||||
? "Analysis"
|
||||
: currentStep === "research"
|
||||
? "Research"
|
||||
: currentStep === "script"
|
||||
? "Script Editing"
|
||||
: "Rendering"}{" "}
|
||||
step. Your progress has been saved.
|
||||
</Typography>
|
||||
<Box component="span" sx={{ fontSize: "0.875rem" }}>
|
||||
<strong>Project Restored:</strong> Resuming from {getStepLabel(currentStep)} step. Your progress has been saved.
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Announcements */}
|
||||
{announcement && (
|
||||
{workflow.announcement && (
|
||||
<Alert
|
||||
severity="info"
|
||||
onClose={() => setAnnouncement("")}
|
||||
onClose={() => workflow.setAnnouncement("")}
|
||||
sx={{
|
||||
background: "#dbeafe",
|
||||
border: "1px solid #bfdbfe",
|
||||
"& .MuiAlert-icon": { color: "#3b82f6" },
|
||||
}}
|
||||
>
|
||||
{announcement}
|
||||
{workflow.announcement}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{(isAnalyzing || isResearching) && (
|
||||
{(workflow.isAnalyzing || workflow.isResearching) && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
icon={<CircularProgress size={20} />}
|
||||
@@ -511,17 +193,21 @@ const PodcastDashboard: React.FC = () => {
|
||||
border: "1px solid #fde68a",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{isAnalyzing ? "Analyzing your idea with AI..." : "Running research... This may take a moment."}
|
||||
</Typography>
|
||||
<Box component="span" sx={{ fontSize: "0.875rem" }}>
|
||||
{workflow.isAnalyzing ? "Analyzing your idea with AI..." : "Running research... This may take a moment."}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Create Modal */}
|
||||
{!project && (
|
||||
<>
|
||||
<CreateModal open onCreate={handleCreate} defaultKnobs={DEFAULT_KNOBS} isSubmitting={isAnalyzing} />
|
||||
{/* Recent Episodes Preview */}
|
||||
<CreateModal
|
||||
open
|
||||
onCreate={workflow.handleCreate}
|
||||
defaultKnobs={DEFAULT_KNOBS}
|
||||
isSubmitting={workflow.isAnalyzing}
|
||||
/>
|
||||
<RecentEpisodesPreview onSelectEpisode={() => {}} />
|
||||
</>
|
||||
)}
|
||||
@@ -531,217 +217,32 @@ const PodcastDashboard: React.FC = () => {
|
||||
{analysis && !showScriptEditor && !showRenderQueue && (
|
||||
<AnalysisPanel
|
||||
analysis={analysis}
|
||||
onRegenerate={() => setAnalysis({ ...analysis })}
|
||||
onRegenerate={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{estimate && !showScriptEditor && !showRenderQueue && (
|
||||
<GlassyCard
|
||||
sx={{
|
||||
...glassyCardSx,
|
||||
background: "#ffffff",
|
||||
border: "1px solid rgba(0,0,0,0.06)",
|
||||
boxShadow: "0 10px 28px rgba(15,23,42,0.06)",
|
||||
color: "#0f172a",
|
||||
}}
|
||||
aria-label="estimate"
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, color: "#0f172a", fontWeight: 700 }}>
|
||||
<InsightsIcon />
|
||||
Estimated Cost
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: "#4f46e5", fontWeight: 800 }}>
|
||||
${estimate.total.toFixed(2)}
|
||||
</Typography>
|
||||
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
|
||||
<Stack direction="row" spacing={2} flexWrap="wrap" useFlexGap>
|
||||
<Chip
|
||||
label={`Voice: $${estimate.ttsCost.toFixed(2)}`}
|
||||
size="small"
|
||||
title="Voice narration cost"
|
||||
sx={{ background: "#eef2ff", color: "#0f172a", border: "1px solid rgba(0,0,0,0.06)" }}
|
||||
/>
|
||||
<Chip
|
||||
label={`Visuals: $${estimate.avatarCost.toFixed(2)}`}
|
||||
size="small"
|
||||
title="Avatar/video cost"
|
||||
sx={{ background: "#eef2ff", color: "#0f172a", border: "1px solid rgba(0,0,0,0.06)" }}
|
||||
/>
|
||||
<Chip
|
||||
label={`Research: $${estimate.researchCost.toFixed(2)}`}
|
||||
size="small"
|
||||
title="Research and fact-checking cost"
|
||||
sx={{ background: "#eef2ff", color: "#0f172a", border: "1px solid rgba(0,0,0,0.06)" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</GlassyCard>
|
||||
<EstimateCard estimate={estimate} />
|
||||
)}
|
||||
|
||||
{queries.length > 0 && !showScriptEditor && !showRenderQueue && (
|
||||
<GlassyCard
|
||||
sx={{
|
||||
...glassyCardSx,
|
||||
background: "#ffffff",
|
||||
border: "1px solid rgba(0,0,0,0.06)",
|
||||
boxShadow: "0 10px 28px rgba(15,23,42,0.06)",
|
||||
color: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={2}>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, color: "#0f172a", fontWeight: 700 }}>
|
||||
<SearchIcon />
|
||||
Research Queries
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Provider</InputLabel>
|
||||
<Select
|
||||
value={researchProvider}
|
||||
onChange={(e) => setResearchProvider(e.target.value as ResearchProvider)}
|
||||
label="Provider"
|
||||
disabled={isResearching}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: "#f8fafc",
|
||||
"&:hover": {
|
||||
backgroundColor: "#f1f5f9",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="google">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<SearchIcon fontSize="small" />
|
||||
<span>Standard Research</span>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
<MenuItem value="exa">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<AutoAwesomeIcon fontSize="small" />
|
||||
<span>Deep Research</span>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Chip
|
||||
label={`${selectedCount} / ${queries.length} selected`}
|
||||
size="small"
|
||||
color={selectedCount > 0 ? "primary" : "default"}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
researchProvider === "google"
|
||||
? "Standard Research: Fast, fact-checked results with source citations"
|
||||
: "Deep Research: Comprehensive analysis with competitor insights and trending topics"
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
background: "#e0f2fe",
|
||||
border: "1px solid #bae6fd",
|
||||
color: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: "#0f172a" }}>
|
||||
{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."}
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Tooltip>
|
||||
|
||||
<List>
|
||||
{queries.map((q) => (
|
||||
<ListItem key={q.id} disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => 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) },
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={selectedQueries.has(q.id)} edge="start" />
|
||||
<ListItemText
|
||||
primary={q.query}
|
||||
secondary={q.rationale}
|
||||
primaryTypographyProps={{ variant: "body2", fontWeight: 600, color: "#0f172a" }}
|
||||
secondaryTypographyProps={{ variant: "caption", sx: { color: "#475569" } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<PrimaryButton
|
||||
onClick={handleRunResearch}
|
||||
disabled={!project || selectedCount === 0 || isResearching}
|
||||
loading={isResearching}
|
||||
startIcon={<SearchIcon />}
|
||||
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"}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</GlassyCard>
|
||||
<QuerySelection
|
||||
queries={queries}
|
||||
selectedQueries={selectedQueries}
|
||||
researchProvider={researchProvider}
|
||||
isResearching={workflow.isResearching}
|
||||
onToggleQuery={workflow.toggleQuery}
|
||||
onProviderChange={setResearchProvider}
|
||||
onRunResearch={workflow.handleRunResearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
{research && !showScriptEditor && !showRenderQueue && (
|
||||
<GlassyCard sx={glassyCardSx}>
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap={2}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
|
||||
<InsightsIcon />
|
||||
Research Summary
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{research.summary}
|
||||
</Typography>
|
||||
</Box>
|
||||
<PrimaryButton
|
||||
onClick={handleGenerateScript}
|
||||
disabled={!canGenerateScript}
|
||||
startIcon={<EditNoteIcon />}
|
||||
tooltip={!canGenerateScript ? "Complete research to generate script" : "Generate AI-powered script from research"}
|
||||
>
|
||||
Generate Script
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
{research.factCards.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
Fact Cards ({research.factCards.length})
|
||||
</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", lg: "1fr 1fr 1fr" }, gap: 2 }}>
|
||||
{research.factCards.map((fact) => (
|
||||
<FactCard key={fact.id} fact={fact} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</GlassyCard>
|
||||
<ResearchSummary
|
||||
research={research}
|
||||
canGenerateScript={workflow.canGenerateScript}
|
||||
onGenerateScript={workflow.handleGenerateScript}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showScriptEditor && project && research && rawResearch && (
|
||||
@@ -756,8 +257,8 @@ const PodcastDashboard: React.FC = () => {
|
||||
script={scriptData}
|
||||
onScriptChange={(s) => setScriptData(s)}
|
||||
onBackToResearch={() => setShowScriptEditor(false)}
|
||||
onProceedToRendering={(s) => handleProceedToRendering(s)}
|
||||
onError={(msg) => setAnnouncement(msg)}
|
||||
onProceedToRendering={(s) => workflow.handleProceedToRendering(s)}
|
||||
onError={(msg) => workflow.setAnnouncement(msg)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -776,11 +277,12 @@ const PodcastDashboard: React.FC = () => {
|
||||
budgetCap={projectState.budgetCap}
|
||||
avatarImageUrl={null}
|
||||
onUpdateJob={updateRenderJob}
|
||||
onUpdateScript={(updatedScript) => setScriptData(updatedScript)}
|
||||
onBack={() => {
|
||||
setShowRenderQueue(false);
|
||||
setShowScriptEditor(true);
|
||||
}}
|
||||
onError={(msg) => setAnnouncement(msg)}
|
||||
onError={(msg) => workflow.setAnnouncement(msg)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -789,13 +291,13 @@ const PodcastDashboard: React.FC = () => {
|
||||
|
||||
{/* Preflight Block Dialog */}
|
||||
<PreflightBlockDialog
|
||||
open={showPreflightDialog}
|
||||
open={workflow.showPreflightDialog}
|
||||
onClose={() => {
|
||||
setShowPreflightDialog(false);
|
||||
setPreflightResponse(null);
|
||||
workflow.setShowPreflightDialog(false);
|
||||
workflow.setPreflightResponse(null);
|
||||
}}
|
||||
response={preflightResponse}
|
||||
operationName={preflightOperationName}
|
||||
response={workflow.preflightResponse}
|
||||
operationName={workflow.preflightOperationName}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user