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 */} ); };