Video_AdGenesis_App / frontend /src /components /GenerationComplete.tsx
sushilideaclan01's picture
first push
91d209c
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useGeneration } from '@/context/GenerationContext';
import { CheckIcon, DownloadIcon, PlayIcon, RefreshIcon, VideoIcon } from './Icons';
import { mergeVideos, ClipMetadata } from '@/utils/api';
export const GenerationComplete: React.FC = () => {
const { state, reset } = useGeneration();
const { generatedVideos, provider } = state;
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
const [isMerging, setIsMerging] = useState(false);
const [mergeError, setMergeError] = useState<string | null>(null);
const [mergedVideoUrl, setMergedVideoUrl] = useState<string | null>(null);
const [isPlayingMerged, setIsPlayingMerged] = useState(false);
const accentColor = provider === 'kling' ? 'coral' : 'electric';
const handleDownload = async (video: typeof generatedVideos[0], index: number) => {
try {
const url = video.blobUrl || video.url;
const response = await fetch(url);
const blob = await response.blob();
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `video-segment-${index + 1}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('Download failed:', error);
}
};
const handleDownloadAll = async () => {
for (let i = 0; i < generatedVideos.length; i++) {
await handleDownload(generatedVideos[i], i);
// Small delay between downloads
await new Promise(r => setTimeout(r, 500));
}
};
// Merge all videos into a single file
const handleMergeAndExport = async () => {
if (generatedVideos.length === 0) return;
setIsMerging(true);
setMergeError(null);
try {
// Collect all video blobs
const videoBlobs: Blob[] = [];
const clipMetadata: ClipMetadata[] = [];
for (let i = 0; i < generatedVideos.length; i++) {
const video = generatedVideos[i];
const url = video.blobUrl || video.url;
// Fetch blob from URL
const response = await fetch(url);
const blob = await response.blob();
videoBlobs.push(blob);
// Create clip metadata
// Use Whisper-detected trim point if available, otherwise use full duration
// No start trimming - keep full video from beginning
const trimStart = 0; // Always start from beginning (no overlap removal)
const trimEnd = video.trimPoint || video.duration; // Use Whisper trim point if available
clipMetadata.push({
index: i,
startTime: trimStart,
endTime: trimEnd,
type: 'video',
duration: trimEnd - trimStart,
});
}
console.log('🎬 Merging videos...', clipMetadata);
// Call merge API
const mergedBlob = await mergeVideos(videoBlobs, clipMetadata);
// Create URL for preview (don't auto-download)
const previewUrl = URL.createObjectURL(mergedBlob);
setMergedVideoUrl(previewUrl);
console.log('✅ Merged video ready for preview!');
} catch (error) {
console.error('Merge failed:', error);
setMergeError(error instanceof Error ? error.message : 'Failed to merge videos');
} finally {
setIsMerging(false);
}
};
// Download the merged video
const handleDownloadMerged = () => {
if (!mergedVideoUrl) return;
const a = document.createElement('a');
a.href = mergedVideoUrl;
a.download = `final-video-${Date.now()}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="min-h-[60vh] flex flex-col items-center justify-center p-8"
>
<div className="max-w-4xl w-full">
{/* Success Header */}
<div className="text-center mb-12">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', duration: 0.5 }}
className={`
w-20 h-20 mx-auto rounded-full flex items-center justify-center mb-6
${accentColor === 'coral' ? 'bg-coral-500/20' : 'bg-electric-500/20'}
`}
>
<CheckIcon
size={40}
className={accentColor === 'coral' ? 'text-coral-400' : 'text-electric-400'}
/>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-4xl font-display font-bold mb-4"
>
<span className={accentColor === 'coral' ? 'gradient-text' : 'gradient-text-electric'}>
Generation Complete!
</span>
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="text-void-400 text-lg"
>
{generatedVideos.length} video{generatedVideos.length !== 1 ? 's' : ''} generated successfully
</motion.p>
</div>
{/* Video Grid */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"
>
{generatedVideos.map((video, index) => (
<div
key={video.id}
className="card group relative overflow-hidden"
>
{/* Video Preview */}
<div className="relative aspect-[9/16] bg-void-950 rounded-lg overflow-hidden mb-4">
<video
src={video.blobUrl || video.url}
className="w-full h-full object-cover"
controls={playingIndex === index}
poster={video.thumbnails[0]}
onEnded={() => setPlayingIndex(null)}
/>
{playingIndex !== index && (
<button
onClick={() => setPlayingIndex(index)}
className="absolute inset-0 flex items-center justify-center bg-black/40 group-hover:bg-black/50 transition-colors"
>
<div className={`
w-14 h-14 rounded-full flex items-center justify-center
${accentColor === 'coral' ? 'bg-coral-500' : 'bg-electric-500'}
group-hover:scale-110 transition-transform
`}>
<PlayIcon size={24} className="text-white ml-1" />
</div>
</button>
)}
</div>
{/* Video Info */}
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-void-200">
Segment {index + 1}
</h3>
<p className="text-xs text-void-400">
~{Math.round(video.duration)}s duration
</p>
</div>
<button
onClick={() => handleDownload(video, index)}
className={`
p-2 rounded-lg transition-colors
${accentColor === 'coral'
? 'hover:bg-coral-500/20 text-coral-400'
: 'hover:bg-electric-500/20 text-electric-400'
}
`}
>
<DownloadIcon size={20} />
</button>
</div>
</div>
))}
</motion.div>
{/* Merged Video Preview */}
{mergedVideoUrl && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className={`card border-2 ${accentColor === 'coral' ? 'border-coral-500/50 bg-coral-500/5' : 'border-electric-500/50 bg-electric-500/5'}`}>
<div className="flex items-center gap-3 mb-4">
<div className={`p-2 rounded-lg ${accentColor === 'coral' ? 'bg-coral-500/20' : 'bg-electric-500/20'}`}>
<VideoIcon size={24} className={accentColor === 'coral' ? 'text-coral-400' : 'text-electric-400'} />
</div>
<div>
<h3 className="font-bold text-lg text-void-100">Final Exported Video</h3>
<p className="text-sm text-void-400">All segments merged into one video</p>
</div>
</div>
{/* Video Player */}
<div className="relative aspect-[9/16] max-w-md mx-auto bg-void-950 rounded-xl overflow-hidden mb-4">
<video
src={mergedVideoUrl}
className="w-full h-full object-contain"
controls={isPlayingMerged}
onEnded={() => setIsPlayingMerged(false)}
/>
{!isPlayingMerged && (
<button
onClick={() => setIsPlayingMerged(true)}
className="absolute inset-0 flex items-center justify-center bg-black/40 hover:bg-black/50 transition-colors"
>
<div className={`
w-16 h-16 rounded-full flex items-center justify-center
${accentColor === 'coral' ? 'bg-coral-500' : 'bg-electric-500'}
hover:scale-110 transition-transform shadow-lg
`}>
<PlayIcon size={28} className="text-white ml-1" />
</div>
</button>
)}
</div>
{/* Download Button */}
<button
onClick={handleDownloadMerged}
className={`
w-full flex items-center justify-center gap-2 py-3 font-semibold rounded-xl
${accentColor === 'coral' ? 'btn-primary' : 'btn-electric'}
`}
>
<DownloadIcon size={20} />
Download Final Video
</button>
</div>
</motion.div>
)}
{/* Merge Error */}
{mergeError && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-xl text-center"
>
<p className="text-red-300 text-sm">{mergeError}</p>
</motion.div>
)}
{/* Actions */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4"
>
{/* Primary: Merge & Export */}
{!mergedVideoUrl && (
<button
onClick={handleMergeAndExport}
disabled={isMerging || generatedVideos.length < 2}
className={`
flex items-center gap-2 px-6 py-3 font-semibold rounded-xl
${accentColor === 'coral' ? 'btn-primary' : 'btn-electric'}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{isMerging ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Merging...</span>
</>
) : (
<>
<VideoIcon size={20} />
<span>Export Final Video</span>
</>
)}
</button>
)}
{/* Re-merge option if already merged */}
{mergedVideoUrl && (
<button
onClick={() => {
URL.revokeObjectURL(mergedVideoUrl);
setMergedVideoUrl(null);
handleMergeAndExport();
}}
disabled={isMerging}
className="btn-secondary flex items-center gap-2"
>
<RefreshIcon size={20} />
Re-merge Video
</button>
)}
{/* Secondary: Download All */}
<button
onClick={handleDownloadAll}
className="btn-secondary flex items-center gap-2"
>
<DownloadIcon size={20} />
Download Segments
</button>
{/* Tertiary: Generate More */}
<button
onClick={reset}
className="btn-secondary flex items-center gap-2"
>
<RefreshIcon size={20} />
Generate More
</button>
</motion.div>
{/* Tip */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="text-center text-void-500 text-sm mt-8"
>
{mergedVideoUrl
? 'Your final video is ready! Download it or re-merge with different settings.'
: generatedVideos.length >= 2
? '"Export Final Video" will merge all segments into a single video file with Whisper-optimized trim points.'
: 'Videos are ready to use in your video editor or social media.'
}
</motion.p>
</div>
</motion.div>
);
};