import React, { useState, useEffect } from "react";
import { Box, Stack, Typography, Alert, Paper, Chip, Divider, LinearProgress, CircularProgress, alpha } from "@mui/material";
import {
CheckCircle as CheckCircleIcon,
RadioButtonUnchecked as RadioButtonUncheckedIcon,
Info as InfoIcon,
OpenInNew as OpenInNewIcon,
Videocam as VideocamIcon,
} from "@mui/icons-material";
import { Scene, Job } from "../types";
import { GlassyCard, glassyCardSx } from "../ui";
import { InlineAudioPlayer } from "../InlineAudioPlayer";
import { SceneActionButtons } from "./SceneActionButtons";
import { aiApiClient } from "../../../api/client";
interface SceneCardProps {
scene: Scene;
job?: Job;
rendering: string | null;
generatingImage: string | null;
isBusy: boolean;
avatarImageUrl?: string | null;
onRender: (sceneId: string, mode: "preview" | "full") => void;
onImageGenerate: (sceneId: string) => void;
onVideoRender: (sceneId: string) => void;
onDownloadAudio: (audioUrl: string, title: string) => void;
onDownloadVideo: (videoUrl: string, title: string) => void;
onShare: (audioUrl: string, title: string) => void;
onError: (message: string) => void;
}
const getInitials = (title: string): string => {
return title
.split(" ")
.slice(0, 2)
.map((s) => s[0])
.join("")
.toUpperCase();
};
const getStatusColor = (status: Job["status"]) => {
switch (status) {
case "completed":
return "success";
case "failed":
return "error";
case "running":
case "previewing":
return "info";
default:
return "default";
}
};
const getStatusIcon = (status: Job["status"]) => {
switch (status) {
case "completed":
return ;
case "failed":
return ;
case "running":
case "previewing":
return ;
default:
return ;
}
};
export const SceneCard: React.FC = ({
scene,
job,
rendering,
generatingImage,
isBusy,
avatarImageUrl,
onRender,
onImageGenerate,
onVideoRender,
onDownloadAudio,
onDownloadVideo,
onShare,
onError,
}) => {
const hasAudio = Boolean(scene.audioUrl || job?.finalUrl || job?.previewUrl);
const hasImage = Boolean(scene.imageUrl || job?.imageUrl);
const hasVideo = Boolean(job?.videoUrl);
const audioUrl = job?.finalUrl || job?.previewUrl || scene.audioUrl || "";
const imageUrl = job?.imageUrl || scene.imageUrl || "";
const status = job?.status || (hasAudio ? "completed" : "idle");
const initials = getInitials(scene.title);
// Load image as blob if it's an authenticated endpoint
const [imageBlobUrl, setImageBlobUrl] = useState(null);
useEffect(() => {
if (!imageUrl) {
setImageBlobUrl(null);
return;
}
console.log('[SceneCard] Loading image:', { imageUrl, hasImage, sceneId: scene.id });
// Check if this is a podcast image endpoint that requires authentication
const isPodcastImage = imageUrl.includes('/api/podcast/images/') || imageUrl.includes('/api/story/images/');
if (!isPodcastImage) {
// Regular URL (external), use directly
console.log('[SceneCard] Using external image URL directly');
setImageBlobUrl(imageUrl);
return;
}
// Fetch as blob for authenticated endpoints
let isMounted = true;
const currentImageUrl = imageUrl;
const loadImageBlob = async () => {
try {
// Normalize path
let imagePath = currentImageUrl.startsWith('/') ? currentImageUrl : `/${currentImageUrl}`;
// Convert /api/story/images/ to /api/podcast/images/ if needed
if (imagePath.includes('/api/story/images/')) {
const filename = imagePath.split('/api/story/images/').pop() || '';
imagePath = `/api/podcast/images/${filename}`;
}
// Ensure it's a podcast image endpoint
if (!imagePath.includes('/api/podcast/images/')) {
const filename = imagePath.split('/').pop() || currentImageUrl;
imagePath = `/api/podcast/images/${filename}`;
}
// Remove query parameters if present
imagePath = imagePath.split('?')[0];
console.log('[SceneCard] Fetching image blob from:', imagePath);
const response = await aiApiClient.get(imagePath, {
responseType: 'blob',
});
if (!isMounted || imageUrl !== currentImageUrl) {
console.log('[SceneCard] Component unmounted or URL changed, skipping blob URL set');
return;
}
const blob = response.data;
const newBlobUrl = URL.createObjectURL(blob);
console.log('[SceneCard] Image blob loaded successfully, created blob URL');
setImageBlobUrl((prevBlobUrl) => {
// Clean up previous blob URL if exists
if (prevBlobUrl && prevBlobUrl !== newBlobUrl && prevBlobUrl.startsWith('blob:')) {
URL.revokeObjectURL(prevBlobUrl);
}
return newBlobUrl;
});
} catch (err) {
console.error('[SceneCard] Failed to load image blob:', err);
if (isMounted && imageUrl === currentImageUrl) {
// Try adding query token as fallback
try {
// Normalize path again for fallback
let fallbackPath = currentImageUrl.startsWith('/') ? currentImageUrl : `/${currentImageUrl}`;
// Convert /api/story/images/ to /api/podcast/images/ if needed
if (fallbackPath.includes('/api/story/images/')) {
const filename = fallbackPath.split('/api/story/images/').pop() || '';
fallbackPath = `/api/podcast/images/${filename}`;
}
// Ensure it's a podcast image endpoint
if (!fallbackPath.includes('/api/podcast/images/')) {
const filename = fallbackPath.split('/').pop() || currentImageUrl;
fallbackPath = `/api/podcast/images/${filename}`;
}
// Remove query parameters if present
fallbackPath = fallbackPath.split('?')[0];
// Get auth token from localStorage or use aiApiClient's default token
const token = localStorage.getItem('clerk_dashboard_token') || '';
if (token) {
const urlWithToken = `${fallbackPath}?token=${encodeURIComponent(token)}`;
console.log('[SceneCard] Trying URL with query token');
setImageBlobUrl(urlWithToken);
} else {
// Fallback to original URL
console.log('[SceneCard] No token available, using original URL');
setImageBlobUrl(imageUrl);
}
} catch (fallbackErr) {
console.error('[SceneCard] Fallback also failed:', fallbackErr);
setImageBlobUrl(imageUrl);
}
}
}
};
loadImageBlob();
return () => {
isMounted = false;
// Cleanup blob URL when component unmounts or URL changes
setImageBlobUrl((prevBlobUrl) => {
if (prevBlobUrl && prevBlobUrl.startsWith('blob:')) {
URL.revokeObjectURL(prevBlobUrl);
}
return null;
});
};
}, [imageUrl, hasImage, scene.id]);
return (
{/* Header */}
{initials}
{scene.title}
{job?.cost != null && (
)}
{job?.fileSize && (
{(job.fileSize / 1024).toFixed(1)} KB
)}
{!job && (
)}
{job?.finalUrl && (
Download Final Audio
)}
{hasVideo && job?.videoUrl && (
Download Video
)}
{job && (
)}
{/* Progress Bar */}
{job && job.status !== "idle" && job.status !== "completed" && (
Progress
{job.progress}%
)}
{/* Success Alert for Pre-generated Audio */}
{hasAudio && !job && (
✅ Audio already generated in Script Editor. Ready to use!
)}
{/* Audio Player */}
{hasAudio && audioUrl && (
)}
{/* Image Preview */}
{hasImage && (imageBlobUrl || imageUrl) && (
{
console.error('[SceneCard] Image failed to load:', {
src: e.currentTarget.src,
imageUrl,
imageBlobUrl,
hasImage,
});
}}
onLoad={() => {
console.log('[SceneCard] Image loaded successfully:', {
src: imageBlobUrl || imageUrl,
});
}}
/>
)}
{/* Action Buttons */}
);
};