Alpha123B's picture
Add VideoInput component
395741f verified
Raw
History Blame Contribute Delete
6.99 kB
import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { motion } from 'framer-motion';
import toast from 'react-hot-toast';
import { ingestUrl, ingestUpload, getSessionStatus } from '../api/client';
export default function VideoInput({ onVideoLoaded }) {
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [statusMessage, setStatusMessage] = useState('');
const handleUrlSubmit = async (e) => {
e.preventDefault();
if (!url.trim()) return;
setLoading(true);
setStatusMessage('Starting download...');
try {
const result = await ingestUrl(url.trim());
// Poll for download completion
const sessionId = result.session_id;
let attempts = 0;
const maxAttempts = 120; // 10 minutes max
const pollStatus = async () => {
while (attempts < maxAttempts) {
await new Promise(r => setTimeout(r, 5000));
attempts++;
setProgress(Math.min(attempts * 2, 90));
try {
const status = await getSessionStatus(sessionId);
if (status.status === 'downloaded') {
setProgress(100);
setStatusMessage('Download complete!');
toast.success('Video downloaded successfully!');
onVideoLoaded(status);
return;
} else if (status.status === 'error') {
throw new Error(status.error || 'Download failed');
}
setStatusMessage(`Downloading... (${attempts * 5}s)`);
} catch (err) {
if (err.message !== 'Download failed') continue;
throw err;
}
}
throw new Error('Download timed out');
};
await pollStatus();
} catch (err) {
toast.error(err.response?.data?.detail || err.message || 'Failed to download video');
setStatusMessage('');
} finally {
setLoading(false);
setProgress(0);
}
};
const onDrop = useCallback(async (acceptedFiles) => {
const file = acceptedFiles[0];
if (!file) return;
// Validate size
if (file.size > 2 * 1024 * 1024 * 1024) {
toast.error('File too large. Maximum size is 2GB.');
return;
}
setLoading(true);
setStatusMessage('Uploading...');
try {
const result = await ingestUpload(file, (percent) => {
setProgress(percent);
setStatusMessage(`Uploading... ${percent}%`);
});
toast.success('Video uploaded successfully!');
onVideoLoaded(result);
} catch (err) {
toast.error(err.response?.data?.detail || 'Upload failed');
setStatusMessage('');
} finally {
setLoading(false);
setProgress(0);
}
}, [onVideoLoaded]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'video/*': ['.mp4', '.mov', '.mkv', '.avi', '.webm']
},
maxFiles: 1,
disabled: loading,
});
return (
<div className="max-w-3xl mx-auto">
{/* Hero */}
<div className="text-center mb-12">
<motion.h2
className="text-5xl font-black mb-4 gradient-text"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
>
Turn Long Videos Into Viral Clips
</motion.h2>
<p className="text-lg text-white/50">
Drop a video or paste a URL. We'll AI-generate multiple viral-ready short clips with
voiceover, captions, sound effects, and more.
</p>
</div>
{/* URL Input */}
<div className="card mb-6">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-4">
πŸ”— Paste Video URL
</h3>
<form onSubmit={handleUrlSubmit} className="flex gap-3">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://youtube.com/watch?v=... or any video URL"
className="flex-1 bg-dark-900 border border-white/10 rounded-xl px-4 py-3 text-white
placeholder:text-white/30 focus:outline-none focus:border-primary-500/50 transition-colors"
disabled={loading}
/>
<button
type="submit"
className="btn-primary whitespace-nowrap disabled:opacity-50"
disabled={loading || !url.trim()}
>
{loading ? '⏳ Processing...' : 'πŸš€ Analyze Video'}
</button>
</form>
<p className="text-xs text-white/30 mt-2">
Supports YouTube, Vimeo, Twitter/X, and most public video URLs
</p>
</div>
{/* Divider */}
<div className="flex items-center gap-4 my-8">
<div className="flex-1 h-px bg-white/10" />
<span className="text-white/30 text-sm font-medium">OR</span>
<div className="flex-1 h-px bg-white/10" />
</div>
{/* File Upload Dropzone */}
<div
{...getRootProps()}
className={`card cursor-pointer transition-all duration-300 border-2 border-dashed
${isDragActive ? 'border-primary-500 bg-primary-500/5' : 'border-white/10 hover:border-white/20'}
${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<input {...getInputProps()} />
<div className="text-center py-12">
<div className="text-5xl mb-4">
{isDragActive ? 'πŸ“₯' : 'πŸŽ₯'}
</div>
<h3 className="text-xl font-semibold mb-2">
{isDragActive ? 'Drop it here!' : 'Drag & Drop Video File'}
</h3>
<p className="text-white/40 mb-4">
or click to browse β€’ MP4, MOV, MKV, AVI, WebM up to 2GB
</p>
<div className="inline-flex items-center gap-2 px-4 py-2 bg-dark-900 rounded-lg text-sm text-white/50">
<span>πŸ“‹</span> Requirements: 60 seconds – 3 hours duration
</div>
</div>
</div>
{/* Progress Bar */}
{loading && (
<motion.div
className="mt-6 card"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-white/60">{statusMessage}</span>
<span className="text-sm font-mono text-primary-500">{progress}%</span>
</div>
<div className="w-full h-2 bg-dark-900 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-primary-500 to-accent-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</motion.div>
)}
</div>
);
}