From 80cdd7ff2993f7887d1c194d0156be2e2bcfb949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Mon, 20 Apr 2026 08:28:13 +0530 Subject: [PATCH] Add B-roll chart panel to script write phase --- .../ScriptEditor/ScriptEditor.tsx | 108 +++++++++++++++++- .../ScriptEditor/parts/BrollInfoPanel.tsx | 53 ++++++--- 2 files changed, 146 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/PodcastMaker/ScriptEditor/ScriptEditor.tsx b/frontend/src/components/PodcastMaker/ScriptEditor/ScriptEditor.tsx index c7cd0a80..444eb49c 100644 --- a/frontend/src/components/PodcastMaker/ScriptEditor/ScriptEditor.tsx +++ b/frontend/src/components/PodcastMaker/ScriptEditor/ScriptEditor.tsx @@ -8,6 +8,7 @@ import { GlassyCard, PrimaryButton, SecondaryButton } from "../ui"; import { SceneEditor } from "./SceneEditor"; import { InlineAudioPlayer } from "../InlineAudioPlayer"; import { aiApiClient } from "../../../api/client"; +import { BrollInfoPanel } from "./parts/BrollInfoPanel"; interface ScriptEditorProps { projectId: string; @@ -50,6 +51,7 @@ export const ScriptEditor: React.FC = ({ const [approvingSceneId, setApprovingSceneId] = useState(null); const [generatingAudioId, setGeneratingAudioId] = useState(null); const [showScriptFormatInfo, setShowScriptFormatInfo] = useState(false); + const [generatingChartId, setGeneratingChartId] = useState(null); const [combiningAudio, setCombiningAudio] = useState(false); const [combinedAudioResult, setCombinedAudioResult] = useState<{ url: string; @@ -277,6 +279,102 @@ export const ScriptEditor: React.FC = ({ } }, [script, projectId, onError]); + const generateChartPreviews = useCallback(async () => { + if (!script) return; + + const scenesWithData = script.scenes.filter( + (scene) => scene.chart_data && Object.keys(scene.chart_data).length > 0 + ); + + if (scenesWithData.length === 0) { + onError("No scenes have chart data to generate previews."); + return; + } + + try { + setGeneratingChartId("all"); + + const updatedScenes = await Promise.all( + script.scenes.map(async (scene) => { + if (!scene.chart_data || Object.keys(scene.chart_data).length === 0) { + return scene; + } + + try { + const result = await podcastApi.generateChartPreview({ + chart_data: scene.chart_data, + chart_type: scene.chart_data.type || "bar_comparison", + title: scene.title, + }); + + return { + ...scene, + broll_preview_url: result.preview_url, + chart_id: result.chart_id, + }; + } catch (error) { + console.error(`Failed to generate chart preview for scene ${scene.id}:`, error); + return scene; + } + }) + ); + + const updatedScript = { ...script, scenes: updatedScenes }; + setScript(updatedScript); + emitScriptChange(updatedScript); + } catch (error: any) { + console.error("Chart preview generation failed:", error); + onError(`Failed to generate chart previews: ${error.message || error}`); + } finally { + setGeneratingChartId(null); + } + }, [script, emitScriptChange, onError]); + + const regenerateChart = useCallback(async (sceneId: string) => { + if (!script) return; + const scene = script.scenes.find((s) => s.id === sceneId); + if (!scene?.chart_data) return; + + try { + setGeneratingChartId(sceneId); + const result = await podcastApi.generateChartPreview({ + chart_data: scene.chart_data, + chart_type: scene.chart_data.type || "bar_comparison", + title: scene.title, + }); + + const updatedScript = { + ...script, + scenes: script.scenes.map((s) => + s.id === sceneId + ? { ...s, broll_preview_url: result.preview_url, chart_id: result.chart_id } + : s + ), + }; + setScript(updatedScript); + emitScriptChange(updatedScript); + } catch (error: any) { + console.error("Chart regeneration failed:", error); + onError(`Failed to regenerate chart: ${error.message || error}`); + } finally { + setGeneratingChartId(null); + } + }, [script, emitScriptChange, onError]); + + const removeChart = useCallback((sceneId: string) => { + if (!script) return; + const updatedScript = { + ...script, + scenes: script.scenes.map((scene) => + scene.id === sceneId + ? { ...scene, chart_data: undefined, broll_preview_url: undefined, broll_video_url: undefined } + : scene + ), + }; + setScript(updatedScript); + emitScriptChange(updatedScript); + }, [script, emitScriptChange]); + return ( @@ -607,6 +705,15 @@ export const ScriptEditor: React.FC = ({ + s.chart_data && Object.keys(s.chart_data).length > 0).length} + /> + {script.scenes.map((scene, idx) => ( = ({ ); }; - diff --git a/frontend/src/components/PodcastMaker/ScriptEditor/parts/BrollInfoPanel.tsx b/frontend/src/components/PodcastMaker/ScriptEditor/parts/BrollInfoPanel.tsx index 3d236197..c27c1f8c 100644 --- a/frontend/src/components/PodcastMaker/ScriptEditor/parts/BrollInfoPanel.tsx +++ b/frontend/src/components/PodcastMaker/ScriptEditor/parts/BrollInfoPanel.tsx @@ -2,24 +2,47 @@ import React from "react"; import { Stack, Box, Typography, Alert, Paper, Button, CircularProgress, Chip } from "@mui/material"; import { BarChart as BarChartIcon, AutoAwesome as AutoAwesomeIcon, Refresh as RefreshIcon, DeleteOutline as DeleteIcon } from "@mui/icons-material"; import { useScriptEditor } from "../ScriptEditorContext"; +import { Script } from "../../types"; + +interface BrollInfoPanelProps { + activeScript?: Script | null; + generatingChartId?: string | null; + generateChartPreviews?: () => Promise; + regenerateChart?: (sceneId: string) => Promise; + removeChart?: (sceneId: string) => void; + scenesWithCharts?: number; +} + +export const BrollInfoPanel: React.FC = (props) => { + let contextValue: ReturnType | null = null; + try { + contextValue = useScriptEditor(); + } catch { + contextValue = null; + } -export const BrollInfoPanel: React.FC = () => { const { activeScript, generatingChartId, - setGeneratingChartId, generateChartPreviews, regenerateChart, removeChart, scenesWithCharts - } = useScriptEditor(); + } = contextValue ?? {}; - if (!activeScript || activeScript.scenes.length === 0) { + const resolvedActiveScript = props.activeScript ?? activeScript; + const resolvedGeneratingChartId = props.generatingChartId ?? generatingChartId; + const resolvedGenerateChartPreviews = props.generateChartPreviews ?? generateChartPreviews; + const resolvedRegenerateChart = props.regenerateChart ?? regenerateChart; + const resolvedRemoveChart = props.removeChart ?? removeChart; + + if (!resolvedActiveScript || resolvedActiveScript.scenes.length === 0) { return null; } - const scenesWithData = activeScript.scenes.filter(s => s.chart_data && Object.keys(s.chart_data).length > 0); + const scenesWithData = resolvedActiveScript.scenes.filter(s => s.chart_data && Object.keys(s.chart_data).length > 0); const hasChartData = scenesWithData.length > 0; + const resolvedScenesWithCharts = props.scenesWithCharts ?? scenesWithCharts ?? scenesWithData.length; return ( { {hasChartData && ( 1 ? 's' : ''} with charts`} + label={`${resolvedScenesWithCharts} scene${resolvedScenesWithCharts > 1 ? 's' : ''} with charts`} size="small" sx={{ background: "rgba(34, 197, 94, 0.1)", color: "#16a34a", fontWeight: 600 }} /> @@ -68,9 +91,9 @@ export const BrollInfoPanel: React.FC = () => { @@ -104,7 +127,7 @@ export const BrollInfoPanel: React.FC = () => { - {generatingChartId === scene.id ? ( + {resolvedGeneratingChartId === scene.id ? ( ) : scene.broll_preview_url ? ( <> @@ -116,14 +139,16 @@ export const BrollInfoPanel: React.FC = () => {