Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import React from "react";
|
||||
import { Stack } from "@mui/material";
|
||||
import { Stack, alpha } from "@mui/material";
|
||||
import {
|
||||
VolumeUp as VolumeUpIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Image as ImageIcon,
|
||||
Videocam as VideocamIcon,
|
||||
Download as DownloadIcon,
|
||||
Share as ShareIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Scene, Job } from "../types";
|
||||
import { PrimaryButton, SecondaryButton } from "../ui";
|
||||
import { Typography } from "@mui/material"; // Import Typography
|
||||
|
||||
interface SceneActionButtonsProps {
|
||||
scene: Scene;
|
||||
@@ -76,7 +77,26 @@ export const SceneActionButtons: React.FC<SceneActionButtonsProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Failed - show retry
|
||||
// Video generation failed - show specific retry for video
|
||||
if (job?.status === "failed" && !needsAudio && hasAudio) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1} justifyContent="flex-end">
|
||||
<Typography variant="caption" color="error" sx={{ alignSelf: "center", mr: 1 }}>
|
||||
Video Generation Failed
|
||||
</Typography>
|
||||
<SecondaryButton
|
||||
onClick={() => onVideoRender(scene.id)}
|
||||
startIcon={<RefreshIcon />}
|
||||
tooltip="Retry video generation"
|
||||
sx={{ borderColor: "error.main", color: "error.main" }}
|
||||
>
|
||||
Retry Video
|
||||
</SecondaryButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Failed (Audio) - show retry
|
||||
if (job?.status === "failed") {
|
||||
return (
|
||||
<Stack direction="row" spacing={1} justifyContent="flex-end">
|
||||
@@ -85,7 +105,7 @@ export const SceneActionButtons: React.FC<SceneActionButtonsProps> = ({
|
||||
startIcon={<RefreshIcon />}
|
||||
tooltip="Retry audio generation"
|
||||
>
|
||||
Retry
|
||||
Retry Audio
|
||||
</SecondaryButton>
|
||||
</Stack>
|
||||
);
|
||||
@@ -97,40 +117,49 @@ export const SceneActionButtons: React.FC<SceneActionButtonsProps> = ({
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end" flexWrap="wrap" useFlexGap>
|
||||
{/* Generate Image */}
|
||||
{/* Generate/Regenerate Image - ALWAYS visible if we have audio */}
|
||||
<PrimaryButton
|
||||
onClick={() => onImageGenerate(scene.id)}
|
||||
disabled={isGeneratingImage || hasImage}
|
||||
disabled={isGeneratingImage}
|
||||
loading={isGeneratingImage}
|
||||
startIcon={<ImageIcon />}
|
||||
tooltip={
|
||||
hasImage
|
||||
? "Image already generated for this scene"
|
||||
: isGeneratingImage
|
||||
isGeneratingImage
|
||||
? "Generating image..."
|
||||
: hasImage
|
||||
? "Regenerate image for this scene"
|
||||
: "Generate image for video (optional)"
|
||||
}
|
||||
sx={{ minWidth: 160 }}
|
||||
sx={{
|
||||
minWidth: 160,
|
||||
// Use secondary style if image exists (to de-emphasize), primary if needed
|
||||
background: hasImage ? alpha("#667eea", 0.1) : undefined,
|
||||
color: hasImage ? "#667eea" : undefined,
|
||||
border: hasImage ? "1px solid rgba(102,126,234,0.3)" : undefined,
|
||||
"&:hover": {
|
||||
background: hasImage ? alpha("#667eea", 0.2) : undefined,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGeneratingImage ? "Generating..." : hasImage ? "Image Ready" : "Generate Image"}
|
||||
{isGeneratingImage ? "Generating..." : hasImage ? "Regenerate Image" : "Generate Image"}
|
||||
</PrimaryButton>
|
||||
|
||||
{/* Generate Video */}
|
||||
{/* Generate Video - ALWAYS visible if we have audio */}
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
onVideoRender(scene.id);
|
||||
}}
|
||||
disabled={isBusy || videoInProgress || !hasImage || hasVideo}
|
||||
disabled={isBusy || videoInProgress || !hasImage}
|
||||
startIcon={<VideocamIcon />}
|
||||
tooltip={
|
||||
hasVideo
|
||||
? "Video already generated"
|
||||
: !hasImage
|
||||
!hasImage
|
||||
? "Generate an image first to create video"
|
||||
: videoInProgress
|
||||
? "A video generation is already running. Please wait..."
|
||||
: isBusy
|
||||
? "Another operation in progress"
|
||||
: hasVideo
|
||||
? "Regenerate video"
|
||||
: "Generate video for this scene"
|
||||
}
|
||||
sx={{ minWidth: 180 }}
|
||||
@@ -138,7 +167,7 @@ export const SceneActionButtons: React.FC<SceneActionButtonsProps> = ({
|
||||
{videoInProgress && isCurrentVideo
|
||||
? "Generating Video..."
|
||||
: hasVideo
|
||||
? "Video Ready"
|
||||
? "Regenerate Video"
|
||||
: "Generate Video"}
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -154,36 +183,48 @@ export const SceneActionButtons: React.FC<SceneActionButtonsProps> = ({
|
||||
)}
|
||||
|
||||
{/* Download Audio */}
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
if (!audioUrl) {
|
||||
onError("Audio URL not found. Please regenerate audio.");
|
||||
return;
|
||||
}
|
||||
onDownloadAudio(audioUrl, scene.title);
|
||||
}}
|
||||
startIcon={<DownloadIcon />}
|
||||
tooltip={hasAudio ? "Download this scene's audio file" : "No audio available. Generate audio first."}
|
||||
disabled={!hasAudio}
|
||||
>
|
||||
Download Audio
|
||||
</SecondaryButton>
|
||||
{hasAudio && audioUrl && (
|
||||
<PrimaryButton
|
||||
onClick={() => onDownloadAudio(audioUrl, scene.title)}
|
||||
startIcon={<DownloadIcon />}
|
||||
tooltip="Download audio file"
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
width: 40,
|
||||
padding: 0,
|
||||
background: alpha("#64748b", 0.1),
|
||||
color: "#64748b",
|
||||
border: "1px solid rgba(100, 116, 139, 0.2)",
|
||||
"&:hover": {
|
||||
background: alpha("#64748b", 0.2),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Icon only */}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
|
||||
{/* Share */}
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
if (!audioUrl) {
|
||||
onError("Audio URL not found. Please regenerate audio.");
|
||||
return;
|
||||
}
|
||||
onShare(audioUrl, scene.title);
|
||||
}}
|
||||
startIcon={<ShareIcon />}
|
||||
tooltip={hasAudio ? "Share this scene's audio" : "No audio available. Generate audio first."}
|
||||
disabled={!hasAudio}
|
||||
>
|
||||
Share
|
||||
</SecondaryButton>
|
||||
{hasAudio && audioUrl && (
|
||||
<PrimaryButton
|
||||
onClick={() => onShare(audioUrl, scene.title)}
|
||||
startIcon={<ShareIcon />}
|
||||
tooltip="Share audio link"
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
width: 40,
|
||||
padding: 0,
|
||||
background: alpha("#64748b", 0.1),
|
||||
color: "#64748b",
|
||||
border: "1px solid rgba(100, 116, 139, 0.2)",
|
||||
"&:hover": {
|
||||
background: alpha("#64748b", 0.2),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Icon only */}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Stack, Typography, Alert, Paper, Chip, Divider, LinearProgress, CircularProgress, alpha } from "@mui/material";
|
||||
import { Box, Stack, Typography, Alert, Paper, Chip, Divider, LinearProgress, CircularProgress, alpha, Modal, IconButton } from "@mui/material";
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
RadioButtonUnchecked as RadioButtonUncheckedIcon,
|
||||
Info as InfoIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Videocam as VideocamIcon,
|
||||
Close as CloseIcon,
|
||||
ZoomIn as ZoomInIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Scene, Job, VideoGenerationSettings } from "../types";
|
||||
import { GlassyCard, glassyCardSx } from "../ui";
|
||||
@@ -22,6 +24,8 @@ interface SceneCardProps {
|
||||
generatingImage: string | null;
|
||||
isBusy: boolean;
|
||||
avatarImageUrl?: string | null;
|
||||
bible?: any;
|
||||
analysis?: any;
|
||||
onRender: (sceneId: string, mode: "preview" | "full") => void;
|
||||
onImageGenerate: (sceneId: string) => void;
|
||||
onVideoGenerate: (sceneId: string, settings: VideoGenerationSettings) => void;
|
||||
@@ -75,6 +79,8 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
generatingImage,
|
||||
isBusy,
|
||||
avatarImageUrl,
|
||||
bible,
|
||||
analysis,
|
||||
onRender,
|
||||
onImageGenerate,
|
||||
onVideoGenerate,
|
||||
@@ -96,6 +102,7 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
const [imageBlobUrl, setImageBlobUrl] = useState<string | null>(null);
|
||||
const [videoBlobUrl, setVideoBlobUrl] = useState<string | null>(null);
|
||||
const [showVideoModal, setShowVideoModal] = useState(false);
|
||||
const [showImageModal, setShowImageModal] = useState(false);
|
||||
const [initialVideoPrompt, setInitialVideoPrompt] = useState<string>("");
|
||||
|
||||
// Prepare a simple default prompt based on the scene title/description
|
||||
@@ -261,96 +268,151 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
<GlassyCard sx={glassyCardSx}>
|
||||
<Stack spacing={2}>
|
||||
{/* Header */}
|
||||
<Stack direction="row" spacing={2} alignItems="flex-start">
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
{/* Visual Avatar */}
|
||||
<Paper
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: 48,
|
||||
height: 48,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: alpha("#667eea", 0.2),
|
||||
border: "1px solid rgba(102,126,234,0.3)",
|
||||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
color: "#ffffff",
|
||||
fontWeight: 700,
|
||||
fontSize: "1.2rem",
|
||||
fontSize: "1.1rem",
|
||||
borderRadius: 2,
|
||||
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
</Paper>
|
||||
|
||||
{/* Title and Metadata */}
|
||||
<Box flex={1}>
|
||||
<Typography variant="h6" sx={{ mb: 0.5, color: "#0f172a", fontWeight: 600 }}>
|
||||
{scene.title}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
|
||||
<Chip label={`Scene ${scene.id.slice(-4)}`} size="small" variant="outlined" />
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={1}>
|
||||
<Typography variant="h6" sx={{ color: "#0f172a", fontWeight: 700, fontSize: "1.05rem" }}>
|
||||
{scene.title}
|
||||
</Typography>
|
||||
|
||||
{/* Quick Downloads */}
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
{job?.finalUrl && (
|
||||
<Box
|
||||
component="a"
|
||||
href={job.finalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
color: "#64748b",
|
||||
textDecoration: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
"&:hover": { color: "#6366f1" }
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon sx={{ fontSize: 14 }} />
|
||||
Audio
|
||||
</Box>
|
||||
)}
|
||||
{hasVideo && videoBlobUrl && (
|
||||
<Box
|
||||
component="a"
|
||||
href={videoBlobUrl}
|
||||
download={`${scene.title.replace(/[^a-z0-9]/gi, '_')}_video.mp4`}
|
||||
sx={{
|
||||
color: "#64748b",
|
||||
textDecoration: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
"&:hover": { color: "#6366f1" }
|
||||
}}
|
||||
>
|
||||
<VideocamIcon sx={{ fontSize: 14 }} />
|
||||
Video
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Compact Metadata Row */}
|
||||
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" sx={{ mt: 0.5 }} useFlexGap>
|
||||
{/* Scene ID */}
|
||||
<Chip
|
||||
label={`Scene ${scene.id.slice(-4)}`}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: "0.7rem",
|
||||
background: alpha("#64748b", 0.08),
|
||||
color: "#64748b",
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Audio Status */}
|
||||
<Chip
|
||||
label={hasAudio ? "Audio Ready" : "Needs Audio"}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: "0.7rem",
|
||||
background: hasAudio ? alpha("#10b981", 0.1) : alpha("#f59e0b", 0.1),
|
||||
color: hasAudio ? "#059669" : "#d97706",
|
||||
fontWeight: 700,
|
||||
border: "1px solid",
|
||||
borderColor: hasAudio ? alpha("#10b981", 0.2) : alpha("#f59e0b", 0.2),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Cost */}
|
||||
{job?.cost != null && (
|
||||
<Chip
|
||||
label={`$${job.cost.toFixed(2)}`}
|
||||
size="small"
|
||||
sx={{ background: alpha("#10b981", 0.2), color: "#6ee7b7" }}
|
||||
title="Generation cost"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: "0.7rem",
|
||||
background: alpha("#6366f1", 0.08),
|
||||
color: "#6366f1",
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{job?.fileSize && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{(job.fileSize / 1024).toFixed(1)} KB
|
||||
</Typography>
|
||||
)}
|
||||
{!job && (
|
||||
|
||||
{/* Job Status (if active/failed) */}
|
||||
{job && job.status !== "idle" && (
|
||||
<Chip
|
||||
label={hasAudio ? "Audio Ready" : "Needs Audio"}
|
||||
icon={getStatusIcon(status)}
|
||||
label={status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
size="small"
|
||||
color={hasAudio ? "success" : "warning"}
|
||||
sx={{
|
||||
background: hasAudio ? alpha("#10b981", 0.2) : alpha("#f59e0b", 0.2),
|
||||
color: hasAudio ? "#059669" : "#d97706",
|
||||
fontWeight: 600,
|
||||
height: 20,
|
||||
fontSize: "0.7rem",
|
||||
background: status === "completed" ? alpha("#10b981", 0.1) : status === "failed" ? alpha("#ef4444", 0.1) : alpha("#3b82f6", 0.1),
|
||||
color: status === "completed" ? "#059669" : status === "failed" ? "#dc2626" : "#2563eb",
|
||||
fontWeight: 700,
|
||||
"& .MuiChip-icon": { fontSize: 14, color: "inherit" }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{job?.finalUrl && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box
|
||||
component="a"
|
||||
href={job.finalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ color: "#a78bfa", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: 0.5 }}
|
||||
>
|
||||
<OpenInNewIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="caption">Download Final Audio</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{hasVideo && videoBlobUrl && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box
|
||||
component="a"
|
||||
href={videoBlobUrl}
|
||||
download={`${scene.title.replace(/[^a-z0-9]/gi, '_')}_video.mp4`}
|
||||
sx={{ color: "#a78bfa", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: 0.5 }}
|
||||
>
|
||||
<VideocamIcon sx={{ fontSize: 16 }} />
|
||||
<Typography variant="caption">Download Video</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{job && (
|
||||
<Chip
|
||||
icon={getStatusIcon(status)}
|
||||
label={status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
color={getStatusColor(status)}
|
||||
size="small"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
minWidth: 100,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Audio Player - Now directly in header section (visual integration) */}
|
||||
{hasAudio && audioUrl && (
|
||||
<Box sx={{ width: "100%", mt: 1 }}>
|
||||
<InlineAudioPlayer audioUrl={audioUrl} title={scene.title} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{job && job.status !== "idle" && job.status !== "completed" && (
|
||||
<Box>
|
||||
@@ -379,20 +441,6 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
|
||||
<Divider sx={{ borderColor: "rgba(15, 23, 42, 0.08)" }} />
|
||||
|
||||
{/* Success Alert for Pre-generated Audio */}
|
||||
{hasAudio && !job && (
|
||||
<Alert severity="success" sx={{ width: "100%", background: alpha("#10b981", 0.1), border: "1px solid rgba(16,185,129,0.3)" }}>
|
||||
<Typography variant="body2" sx={{ color: "#059669", fontWeight: 500 }}>
|
||||
✅ Audio already generated in Script Editor. Ready to use!
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Audio Player */}
|
||||
{hasAudio && audioUrl && (
|
||||
<InlineAudioPlayer audioUrl={audioUrl} title={scene.title} />
|
||||
)}
|
||||
|
||||
{/* Video Preview - Show video if available, otherwise show image */}
|
||||
{hasVideo && videoBlobUrl ? (
|
||||
<Box
|
||||
@@ -415,7 +463,7 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
height: "auto",
|
||||
display: "block",
|
||||
maxHeight: 420,
|
||||
objectFit: "cover",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
onError={(e) => {
|
||||
@@ -443,34 +491,62 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
VIDEO
|
||||
</Box>
|
||||
</Box>
|
||||
) : hasImage && (imageBlobUrl || imageUrl) ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(102,126,234,0.2)",
|
||||
background: alpha("#667eea", 0.05),
|
||||
}}
|
||||
>
|
||||
) : hasImage && (imageBlobUrl || (imageUrl && !imageUrl.includes('/api/'))) ? (
|
||||
<Box sx={{ position: "relative", width: "100%" }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={imageBlobUrl || imageUrl}
|
||||
alt={scene.title}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
display: "block",
|
||||
maxHeight: 400,
|
||||
objectFit: "cover",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(102,126,234,0.2)",
|
||||
background: alpha("#667eea", 0.05),
|
||||
cursor: "pointer",
|
||||
"&:hover .zoom-icon": {
|
||||
opacity: 1,
|
||||
}
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error("[SceneCard] Image failed to load:", {
|
||||
src: e.currentTarget.src,
|
||||
imageUrl,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
onClick={() => setShowImageModal(true)}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={imageBlobUrl || imageUrl}
|
||||
alt={scene.title}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
display: "block",
|
||||
maxHeight: 400,
|
||||
objectFit: "contain",
|
||||
background: "#000",
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error("[SceneCard] Image failed to load:", {
|
||||
src: e.currentTarget.src,
|
||||
imageUrl,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
className="zoom-icon"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "rgba(0,0,0,0.6)",
|
||||
color: "white",
|
||||
borderRadius: "50%",
|
||||
p: 1.5,
|
||||
opacity: 0,
|
||||
transition: "opacity 0.2s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ZoomInIcon sx={{ fontSize: 32 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
@@ -505,6 +581,9 @@ export const SceneCard: React.FC<SceneCardProps> = ({
|
||||
initialPrompt={initialVideoPrompt}
|
||||
initialResolution="480p"
|
||||
initialSeed={-1}
|
||||
sceneTitle={scene.title}
|
||||
bible={bible}
|
||||
analysis={analysis}
|
||||
/>
|
||||
</Stack>
|
||||
</GlassyCard>
|
||||
|
||||
@@ -26,6 +26,10 @@ interface VideoRegenerateModalProps {
|
||||
initialPrompt: string;
|
||||
initialResolution?: "480p" | "720p";
|
||||
initialSeed?: number | null;
|
||||
// Add context props
|
||||
sceneTitle?: string;
|
||||
bible?: any;
|
||||
analysis?: any;
|
||||
}
|
||||
|
||||
export const VideoRegenerateModal: React.FC<VideoRegenerateModalProps> = ({
|
||||
@@ -35,17 +39,45 @@ export const VideoRegenerateModal: React.FC<VideoRegenerateModalProps> = ({
|
||||
initialPrompt,
|
||||
initialResolution = "480p",
|
||||
initialSeed = -1,
|
||||
sceneTitle,
|
||||
bible,
|
||||
analysis,
|
||||
}) => {
|
||||
// Use a more intelligent default prompt based on context if available
|
||||
const [prompt, setPrompt] = useState(initialPrompt);
|
||||
|
||||
// Update prompt when context changes or modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
let smartPrompt = initialPrompt;
|
||||
|
||||
// If the initial prompt is generic/empty, try to build a better one
|
||||
if (!smartPrompt || smartPrompt === "Professional podcast scene with subtle movement") {
|
||||
const parts = [];
|
||||
|
||||
// Add scene context
|
||||
if (sceneTitle) parts.push(`Scene: ${sceneTitle}`);
|
||||
|
||||
// Add bible/persona context
|
||||
if (bible?.host_persona) parts.push(`Host Persona: ${bible.host_persona}`);
|
||||
if (bible?.tone) parts.push(`Tone: ${bible.tone}`);
|
||||
|
||||
// Add analysis context
|
||||
if (analysis?.content_type) parts.push(`Style: ${analysis.content_type}`);
|
||||
|
||||
// Combine into a descriptive prompt
|
||||
if (parts.length > 0) {
|
||||
smartPrompt = `Professional talking head video for podcast. ${parts.join(". ")}. Cinematic lighting, 4k, high detail.`;
|
||||
}
|
||||
}
|
||||
setPrompt(smartPrompt);
|
||||
}
|
||||
}, [open, initialPrompt, sceneTitle, bible, analysis]);
|
||||
|
||||
const [resolution, setResolution] = useState<"480p" | "720p">(initialResolution);
|
||||
const [seed, setSeed] = useState<string>(initialSeed != null && initialSeed !== -1 ? String(initialSeed) : "");
|
||||
const [maskImageUrl, setMaskImageUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(initialPrompt);
|
||||
setResolution(initialResolution);
|
||||
}, [initialResolution, initialPrompt]);
|
||||
|
||||
const handleGenerate = () => {
|
||||
const parsedSeed = seed.trim() === "" ? undefined : Number.isNaN(Number(seed)) ? undefined : Number(seed);
|
||||
const settings: VideoGenerationSettings = {
|
||||
|
||||
@@ -7,6 +7,7 @@ interface UseRenderQueueProps {
|
||||
jobs: Job[];
|
||||
knobs: Knobs;
|
||||
projectId: string;
|
||||
bible?: any | null;
|
||||
budgetCap?: number;
|
||||
avatarImageUrl?: string | null;
|
||||
onUpdateJob: (sceneId: string, updates: Partial<Job>) => void;
|
||||
@@ -21,6 +22,7 @@ export const useRenderQueue = ({
|
||||
jobs,
|
||||
knobs,
|
||||
projectId,
|
||||
bible,
|
||||
budgetCap,
|
||||
avatarImageUrl,
|
||||
onUpdateJob,
|
||||
@@ -441,6 +443,7 @@ export const useRenderQueue = ({
|
||||
sceneTitle: scene.title,
|
||||
sceneContent: sceneContent,
|
||||
baseAvatarUrl: avatarImageUrl || undefined, // Use base avatar if available
|
||||
bible: bible,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
});
|
||||
@@ -544,6 +547,7 @@ export const useRenderQueue = ({
|
||||
sceneTitle: scene.title,
|
||||
audioUrl,
|
||||
avatarImageUrl: sceneImageUrl,
|
||||
bible: bible,
|
||||
resolution: targetResolution,
|
||||
prompt: settings?.prompt || undefined,
|
||||
seed: settings?.seed ?? -1,
|
||||
|
||||
Reference in New Issue
Block a user