File size: 5,220 Bytes
23b413b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | import React from 'react'
import { Loader2, ImageIcon, RefreshCw } from 'lucide-react'
import { resolveFileUrl } from '../../resolveFileUrl'
// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------
type StudioPreviewPanelProps = {
imageUrl?: string | null
isGenerating?: boolean
narration?: string
prompt?: string
onRegenerateImage?: () => void
className?: string
}
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
/**
* Preview panel for the current scene, styled like Imagine's lightbox.
* Shows the scene image with narration overlay and generation status.
*/
export function StudioPreviewPanel({
imageUrl,
isGenerating = false,
narration,
prompt,
onRegenerateImage,
className = '',
}: StudioPreviewPanelProps) {
const hasImage = Boolean(imageUrl)
return (
<div className={`relative flex-1 overflow-hidden ${className}`}>
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-b from-black via-[#0a0a0f] to-[#121218]" />
{/* Main content area */}
<div className="absolute inset-0 flex items-center justify-center p-4">
{hasImage ? (
<div className="relative w-full h-full flex items-center justify-center">
{/* Image container with subtle shadow */}
<div className="relative max-w-full max-h-full">
<img
src={resolveFileUrl(imageUrl!)}
alt="Scene preview"
className="max-h-[calc(100vh-320px)] max-w-full object-contain rounded-lg shadow-2xl transition-opacity duration-700"
/>
{/* Regenerate overlay on hover */}
{onRegenerateImage && (
<div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-300 bg-black/40 rounded-lg">
<button
onClick={onRegenerateImage}
disabled={isGenerating}
className="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md border border-white/20 rounded-full text-white text-sm font-medium hover:bg-white/20 transition-colors disabled:opacity-50"
type="button"
>
<RefreshCw size={14} className={isGenerating ? 'animate-spin' : ''} />
Regenerate Image
</button>
</div>
)}
</div>
{/* Generating overlay */}
{isGenerating && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60 rounded-lg">
<div className="flex flex-col items-center gap-3">
<Loader2 size={32} className="text-purple-400 animate-spin" />
<span className="text-white/70 text-sm">Generating image...</span>
</div>
</div>
)}
</div>
) : (
/* Empty state when no image */
<div className="flex flex-col items-center justify-center text-center p-8">
{isGenerating ? (
<>
<Loader2 size={48} className="text-purple-400 animate-spin mb-4" />
<p className="text-white/70 text-sm">Generating image...</p>
{prompt && (
<p className="text-white/40 text-xs mt-2 max-w-md line-clamp-2">{prompt}</p>
)}
</>
) : (
<>
<div className="w-16 h-16 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center mb-4">
<ImageIcon size={24} className="text-white/30" />
</div>
<p className="text-white/50 text-sm">No image for this scene</p>
{onRegenerateImage && (
<button
onClick={onRegenerateImage}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 rounded-full text-white text-sm font-medium transition-colors"
type="button"
>
Generate Image
</button>
)}
</>
)}
</div>
)}
</div>
{/* Narration subtitle overlay */}
{narration && (
<div className="absolute bottom-6 left-0 right-0 flex justify-center px-8 pointer-events-none">
<div className="bg-black/80 backdrop-blur-sm px-6 py-4 rounded-xl max-w-3xl shadow-lg border border-white/5">
<p className="text-base md:text-lg text-white leading-relaxed text-center">
{narration}
</p>
</div>
</div>
)}
<style>{`
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
`}</style>
</div>
)
}
export default StudioPreviewPanel
|