Files
ALwrity/frontend/src/components/PodcastMaker/ScriptEditor/parts/BrollInfoPanel.tsx
ajaysi ba94ee30bc feat(phase-4): UI/UX improvements for Podcast Maker Write phase
Frontend Changes:
- Add scene numbering badge (1/N) next to scene titles
- Add inline status chips (Complete, Audio, Image, Voice, Why Script)
- Professional AI-like gradient styling for all chips with shadows
- Remove Script Editor header and 'Why This Script Format?' collapsible
- Move Voice and Why Script info to per-scene chips
- Make scene section mobile-responsive (responsive layout, button sizing)
- Rename 'B-Roll Charts' to 'Podcast Charts' with accordion (collapsed by default)
- Add sceneIndex prop to SceneEditor for scene numbering
- Enhanced accessibility with keyboard navigation and focus states

Backend Changes:
- Audio handler improvements
- B-roll handler enhancements
- Script handler updates
- B-roll composer and service improvements
- Removed temporary broll_temp files

Technical:
- Full mobile responsiveness for scene cards
- Gradient chip styling: vibrant colors with white text and shadows
- Non-breaking approval/generation flow preserved
- TypeScript compatibility maintained
2026-04-24 15:44:09 +05:30

403 lines
16 KiB
TypeScript

import React, { useState } from "react";
import { Stack, Box, Typography, Paper, Button, CircularProgress, Chip, IconButton, Tooltip, Accordion, AccordionSummary, AccordionDetails, Dialog, DialogContent, DialogTitle } from "@mui/material";
import { BarChart as BarChartIcon, AutoAwesome as AutoAwesomeIcon, Refresh as RefreshIcon, DeleteOutline as DeleteIcon, Fullscreen as FullscreenIcon, ExpandMore as ExpandMoreIcon, Close as CloseIcon, ZoomOutMap as ZoomOutMapIcon } from "@mui/icons-material";
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) => {
const [expanded, setExpanded] = useState(false);
const [previewModal, setPreviewModal] = useState<{ url: string; title: string } | null>(null);
const ctx = useScriptEditor();
const {
activeScript: ctxActiveScript,
generatingChartId: ctxGeneratingChartId,
generateChartPreviews: ctxGenerateChartPreviews,
regenerateChart: ctxRegenerateChart,
removeChart: ctxRemoveChart,
scenesWithCharts: ctxScenesWithCharts
} = ctx;
const resolvedActiveScript = props.activeScript ?? ctxActiveScript;
const resolvedGeneratingChartId = props.generatingChartId ?? ctxGeneratingChartId;
const resolvedGenerateChartPreviews = props.generateChartPreviews ?? ctxGenerateChartPreviews;
const resolvedRegenerateChart = props.regenerateChart ?? ctxRegenerateChart;
const resolvedRemoveChart = props.removeChart ?? ctxRemoveChart;
if (!resolvedActiveScript || resolvedActiveScript.scenes.length === 0) {
return null;
}
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 ?? ctxScenesWithCharts ?? scenesWithData.length;
const openPreview = (url: string, title: string) => {
setPreviewModal({ url, title });
};
const closePreview = () => {
setPreviewModal(null);
};
return (
<>
<Accordion
expanded={expanded}
onChange={(_, isExpanded) => setExpanded(isExpanded)}
sx={{
background: "linear-gradient(135deg, rgba(34, 197, 94, 0.03) 0%, rgba(16, 185, 129, 0.03) 100%)",
border: "1px solid rgba(34, 197, 94, 0.15)",
borderRadius: 2,
'&:before': {
display: 'none',
},
'&.MuiAccordion-root': {
borderRadius: 2,
},
'& .MuiAccordionSummary-root': {
borderRadius: 2,
},
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: '#22c55e' }} />}
sx={{
'& .MuiAccordionSummary-content': {
alignItems: 'center',
justifyContent: 'space-between',
},
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Box sx={{
p: 0.75,
borderRadius: 1.5,
background: "linear-gradient(135deg, #22c55e 0%, #16a34a 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<BarChartIcon sx={{ fontSize: 18, color: "#fff" }} />
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: "#0f172a", lineHeight: 1.2 }}>
Podcast Charts
</Typography>
<Typography variant="caption" sx={{ color: "#64748b", fontSize: "0.7rem" }}>
{resolvedScenesWithCharts} chart{resolvedScenesWithCharts !== 1 ? 's' : ''} for visual storytelling
</Typography>
</Box>
</Stack>
</AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}>
{hasChartData && (
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end' }}>
<Button
variant="contained"
size="small"
startIcon={resolvedGeneratingChartId ? <CircularProgress size={14} color="inherit" /> : <AutoAwesomeIcon sx={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
resolvedGenerateChartPreviews?.();
}}
disabled={!!resolvedGeneratingChartId}
sx={{
background: "linear-gradient(135deg, #22c55e 0%, #16a34a 100%)",
fontSize: "0.75rem",
py: 0.5,
px: 1.5,
textTransform: "none",
fontWeight: 600,
boxShadow: "0 2px 8px rgba(34, 197, 94, 0.3)",
"&:hover": {
background: "linear-gradient(135deg, #16a34a 0%, #15803d 100%)",
},
"&:disabled": {
background: "rgba(34, 197, 94, 0.5)",
}
}}
>
{resolvedGeneratingChartId ? "Generating..." : "Generate Charts"}
</Button>
</Box>
)}
{hasChartData ? (
<Stack spacing={1.5}>
{scenesWithData.map((scene) => {
const chartData = scene.chart_data;
const hasPreview = !!scene.broll_preview_url;
return (
<Box
key={scene.id}
sx={{
p: 1.5,
background: "#fff",
borderRadius: 1.5,
border: "1px solid rgba(0,0,0,0.06)",
display: "flex",
alignItems: "center",
gap: 2,
transition: "all 0.2s ease",
"&:hover": {
borderColor: "rgba(34, 197, 94, 0.3)",
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
}
}}
>
{/* Thumbnail */}
<Box
onClick={() => hasPreview && scene.broll_preview_url && openPreview(scene.broll_preview_url, scene.title)}
sx={{
width: 72,
height: 48,
flexShrink: 0,
borderRadius: 1,
overflow: "hidden",
background: hasPreview ? "rgba(0,0,0,0.04)" : "linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: hasPreview ? "pointer" : "default",
transition: "all 0.2s ease",
position: "relative",
"&:hover": hasPreview ? {
transform: "scale(1.05)",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
"& .zoom-overlay": {
opacity: 1,
},
} : {}
}}
>
{resolvedGeneratingChartId === scene.id ? (
<CircularProgress size={24} sx={{ color: "#22c55e" }} />
) : hasPreview && scene.broll_preview_url ? (
<>
<Box
component="img"
src={scene.broll_preview_url}
alt={`Chart for ${scene.title}`}
sx={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
<Box
className="zoom-overlay"
sx={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: 0,
transition: "opacity 0.2s ease",
borderRadius: 1,
}}
>
<ZoomOutMapIcon sx={{ color: "#fff", fontSize: 18 }} />
</Box>
</>
) : (
<BarChartIcon sx={{ fontSize: 20, color: "#94a3b8" }} />
)}
</Box>
{/* Chart Info */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{
fontWeight: 600,
color: "#1e293b",
fontSize: "0.8rem",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{scene.title}
</Typography>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 0.25 }}>
<Chip
label={chartData?.type || "chart"}
size="small"
sx={{
height: 18,
fontSize: "0.65rem",
background: "rgba(34, 197, 94, 0.1)",
color: "#16a34a",
fontWeight: 600,
}}
/>
<Typography variant="caption" sx={{ color: "#64748b", fontSize: "0.7rem" }}>
{chartData?.labels?.length || 0} labels
</Typography>
{hasPreview && (
<Chip
label="Ready"
size="small"
sx={{
height: 18,
fontSize: "0.65rem",
background: "rgba(34, 197, 94, 0.15)",
color: "#16a34a",
fontWeight: 600,
}}
/>
)}
</Stack>
</Box>
{/* Takeaway */}
{chartData?.takeaway && (
<Box sx={{
flex: 1.5,
display: { xs: "none", md: "block" },
px: 1,
py: 0.5,
background: "rgba(34, 197, 94, 0.04)",
borderRadius: 1,
}}>
<Typography variant="caption" sx={{
color: "#475569",
fontSize: "0.7rem",
fontStyle: "italic",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}>
"{chartData.takeaway}"
</Typography>
</Box>
)}
{/* Actions */}
<Stack direction="row" spacing={0.5}>
{hasPreview && (
<Tooltip title="View fullsize">
<IconButton
size="small"
onClick={() => scene.broll_preview_url && openPreview(scene.broll_preview_url, scene.title)}
sx={{
color: "#64748b",
"&:hover": { color: "#22c55e", background: "rgba(34, 197, 94, 0.1)" }
}}
>
<FullscreenIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
)}
<Tooltip title="Regenerate">
<IconButton
size="small"
onClick={() => resolvedRegenerateChart?.(scene.id)}
disabled={!resolvedRegenerateChart || !!resolvedGeneratingChartId}
sx={{
color: "#64748b",
"&:hover": { color: "#f59e0b", background: "rgba(245, 158, 11, 0.1)" }
}}
>
<RefreshIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
<Tooltip title="Remove chart">
<IconButton
size="small"
onClick={() => resolvedRemoveChart?.(scene.id)}
disabled={!resolvedRemoveChart}
sx={{
color: "#64748b",
"&:hover": { color: "#ef4444", background: "rgba(239, 68, 68, 0.1)" }
}}
>
<DeleteIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
</Stack>
</Box>
);
})}
</Stack>
) : (
<Box sx={{ py: 3, textAlign: "center" }}>
<BarChartIcon sx={{ fontSize: 36, color: "#cbd5e1", mb: 1 }} />
<Typography variant="body2" sx={{ color: "#64748b", fontSize: "0.8rem" }}>
No chart data yet. Add chart data to scenes to generate B-roll visuals.
</Typography>
</Box>
)}
</AccordionDetails>
</Accordion>
{/* Full-size chart preview modal */}
<Dialog
open={!!previewModal}
onClose={closePreview}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
background: "#0f172a",
overflow: "hidden",
}
}}
>
{previewModal && (
<>
<DialogTitle sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
color: "#f1f5f9",
py: 1.5,
px: 2,
}}>
<Stack direction="row" alignItems="center" spacing={1}>
<BarChartIcon sx={{ fontSize: 20, color: "#22c55e" }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: "#f1f5f9" }}>
{previewModal.title}
</Typography>
</Stack>
<IconButton
onClick={closePreview}
size="small"
sx={{ color: "#94a3b8", "&:hover": { color: "#f1f5f9" } }}
>
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 0, display: "flex", justifyContent: "center", alignItems: "center", minHeight: 300, background: "#0f172a" }}>
<Box
component="img"
src={previewModal.url}
alt={`Chart: ${previewModal.title}`}
sx={{
maxWidth: "100%",
maxHeight: "70vh",
objectFit: "contain",
p: 2,
}}
/>
</DialogContent>
</>
)}
</Dialog>
</>
);
};