Merge remote-tracking branch 'origin/codex/locate-and-render-brollinfopanel-component'
This commit is contained in:
@@ -8,6 +8,7 @@ import { GlassyCard, PrimaryButton, SecondaryButton } from "../ui";
|
|||||||
import { SceneEditor } from "./SceneEditor";
|
import { SceneEditor } from "./SceneEditor";
|
||||||
import { InlineAudioPlayer } from "../InlineAudioPlayer";
|
import { InlineAudioPlayer } from "../InlineAudioPlayer";
|
||||||
import { aiApiClient } from "../../../api/client";
|
import { aiApiClient } from "../../../api/client";
|
||||||
|
import { BrollInfoPanel } from "./parts/BrollInfoPanel";
|
||||||
|
|
||||||
interface ScriptEditorProps {
|
interface ScriptEditorProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -50,6 +51,7 @@ export const ScriptEditor: React.FC<ScriptEditorProps> = ({
|
|||||||
const [approvingSceneId, setApprovingSceneId] = useState<string | null>(null);
|
const [approvingSceneId, setApprovingSceneId] = useState<string | null>(null);
|
||||||
const [generatingAudioId, setGeneratingAudioId] = useState<string | null>(null);
|
const [generatingAudioId, setGeneratingAudioId] = useState<string | null>(null);
|
||||||
const [showScriptFormatInfo, setShowScriptFormatInfo] = useState(false);
|
const [showScriptFormatInfo, setShowScriptFormatInfo] = useState(false);
|
||||||
|
const [generatingChartId, setGeneratingChartId] = useState<string | null>(null);
|
||||||
const [combiningAudio, setCombiningAudio] = useState(false);
|
const [combiningAudio, setCombiningAudio] = useState(false);
|
||||||
const [combinedAudioResult, setCombinedAudioResult] = useState<{
|
const [combinedAudioResult, setCombinedAudioResult] = useState<{
|
||||||
url: string;
|
url: string;
|
||||||
@@ -277,6 +279,102 @@ export const ScriptEditor: React.FC<ScriptEditorProps> = ({
|
|||||||
}
|
}
|
||||||
}, [script, projectId, onError]);
|
}, [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 (
|
return (
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 4 }}>
|
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 4 }}>
|
||||||
@@ -607,6 +705,15 @@ export const ScriptEditor: React.FC<ScriptEditorProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
<BrollInfoPanel
|
||||||
|
activeScript={script}
|
||||||
|
generatingChartId={generatingChartId}
|
||||||
|
generateChartPreviews={generateChartPreviews}
|
||||||
|
regenerateChart={regenerateChart}
|
||||||
|
removeChart={removeChart}
|
||||||
|
scenesWithCharts={script.scenes.filter((s) => s.chart_data && Object.keys(s.chart_data).length > 0).length}
|
||||||
|
/>
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{script.scenes.map((scene, idx) => (
|
{script.scenes.map((scene, idx) => (
|
||||||
<GlassyCard
|
<GlassyCard
|
||||||
@@ -837,4 +944,3 @@ export const ScriptEditor: React.FC<ScriptEditorProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,47 @@ import React from "react";
|
|||||||
import { Stack, Box, Typography, Alert, Paper, Button, CircularProgress, Chip } from "@mui/material";
|
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 { BarChart as BarChartIcon, AutoAwesome as AutoAwesomeIcon, Refresh as RefreshIcon, DeleteOutline as DeleteIcon } from "@mui/icons-material";
|
||||||
import { useScriptEditor } from "../ScriptEditorContext";
|
import { useScriptEditor } from "../ScriptEditorContext";
|
||||||
|
import { Script } from "../../types";
|
||||||
|
|
||||||
|
interface BrollInfoPanelProps {
|
||||||
|
activeScript?: Script | null;
|
||||||
|
generatingChartId?: string | null;
|
||||||
|
generateChartPreviews?: () => Promise<void>;
|
||||||
|
regenerateChart?: (sceneId: string) => Promise<void>;
|
||||||
|
removeChart?: (sceneId: string) => void;
|
||||||
|
scenesWithCharts?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrollInfoPanel: React.FC<BrollInfoPanelProps> = (props) => {
|
||||||
|
let contextValue: ReturnType<typeof useScriptEditor> | null = null;
|
||||||
|
try {
|
||||||
|
contextValue = useScriptEditor();
|
||||||
|
} catch {
|
||||||
|
contextValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
export const BrollInfoPanel: React.FC = () => {
|
|
||||||
const {
|
const {
|
||||||
activeScript,
|
activeScript,
|
||||||
generatingChartId,
|
generatingChartId,
|
||||||
setGeneratingChartId,
|
|
||||||
generateChartPreviews,
|
generateChartPreviews,
|
||||||
regenerateChart,
|
regenerateChart,
|
||||||
removeChart,
|
removeChart,
|
||||||
scenesWithCharts
|
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;
|
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 hasChartData = scenesWithData.length > 0;
|
||||||
|
const resolvedScenesWithCharts = props.scenesWithCharts ?? scenesWithCharts ?? scenesWithData.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -45,7 +68,7 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
|
|
||||||
{hasChartData && (
|
{hasChartData && (
|
||||||
<Chip
|
<Chip
|
||||||
label={`${scenesWithData.length} scene${scenesWithData.length > 1 ? 's' : ''} with charts`}
|
label={`${resolvedScenesWithCharts} scene${resolvedScenesWithCharts > 1 ? 's' : ''} with charts`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ background: "rgba(34, 197, 94, 0.1)", color: "#16a34a", fontWeight: 600 }}
|
sx={{ background: "rgba(34, 197, 94, 0.1)", color: "#16a34a", fontWeight: 600 }}
|
||||||
/>
|
/>
|
||||||
@@ -68,9 +91,9 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={generatingChartId ? <CircularProgress size={16} color="inherit" /> : <AutoAwesomeIcon />}
|
startIcon={resolvedGeneratingChartId ? <CircularProgress size={16} color="inherit" /> : <AutoAwesomeIcon />}
|
||||||
onClick={generateChartPreviews}
|
onClick={resolvedGenerateChartPreviews}
|
||||||
disabled={!!generatingChartId}
|
disabled={!!resolvedGeneratingChartId || !resolvedGenerateChartPreviews}
|
||||||
sx={{
|
sx={{
|
||||||
background: "linear-gradient(135deg, #22c55e 0%, #10b981 100%)",
|
background: "linear-gradient(135deg, #22c55e 0%, #10b981 100%)",
|
||||||
"&:hover": { background: "linear-gradient(135deg, #16a34a 0%, #059669 100%)" },
|
"&:hover": { background: "linear-gradient(135deg, #16a34a 0%, #059669 100%)" },
|
||||||
@@ -78,7 +101,7 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{generatingChartId ? "Generating..." : "Generate Chart Previews"}
|
{resolvedGeneratingChartId ? "Generating..." : "Generate Chart Previews"}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -104,7 +127,7 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
{generatingChartId === scene.id ? (
|
{resolvedGeneratingChartId === scene.id ? (
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
) : scene.broll_preview_url ? (
|
) : scene.broll_preview_url ? (
|
||||||
<>
|
<>
|
||||||
@@ -116,14 +139,16 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<RefreshIcon />}
|
startIcon={<RefreshIcon />}
|
||||||
onClick={() => regenerateChart(scene.id)}
|
onClick={() => resolvedRegenerateChart?.(scene.id)}
|
||||||
|
disabled={!resolvedRegenerateChart}
|
||||||
>
|
>
|
||||||
Regenerate
|
Regenerate
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<DeleteIcon />}
|
startIcon={<DeleteIcon />}
|
||||||
onClick={() => removeChart(scene.id)}
|
onClick={() => resolvedRemoveChart?.(scene.id)}
|
||||||
|
disabled={!resolvedRemoveChart}
|
||||||
sx={{ color: "#ef4444" }}
|
sx={{ color: "#ef4444" }}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -137,4 +162,4 @@ export const BrollInfoPanel: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user