|
import React, { useState, useEffect, FormEvent, ChangeEvent } from 'react'; |
|
|
|
interface InputData { |
|
prompt: string; |
|
width: number; |
|
height: number; |
|
steps: number; |
|
fps: number; |
|
seed?: number; |
|
} |
|
|
|
interface OutputData { |
|
prediction: any; |
|
timestamp: string; |
|
prompt: string; |
|
} |
|
const TruncatedText = ({ text }: { text: string }) => { |
|
const [isTruncated, setIsTruncated] = useState(true); |
|
|
|
const toggleTruncated = () => { |
|
setIsTruncated(!isTruncated); |
|
}; |
|
|
|
if (text.length < 34) { |
|
return <span>{text}</span>; |
|
} |
|
|
|
return ( |
|
<span> |
|
{isTruncated ? `${text.substring(0, 24)}...` : text} |
|
<button onClick={toggleTruncated}> |
|
{isTruncated ? 'Read More' : 'Read Less'} |
|
</button> |
|
</span> |
|
); |
|
}; |
|
|
|
const IndexPage = () => { |
|
const [inputData, setInputData] = useState<InputData>({ |
|
prompt: '', |
|
width: 1024, |
|
height: 576, |
|
steps: 40, |
|
fps: 15, |
|
seed: undefined, |
|
}); |
|
const [output, setOutput] = useState<OutputData[]>([]); |
|
const [isLoading, setIsLoading] = useState(false); |
|
|
|
useEffect(() => { |
|
const savedOutput = localStorage.getItem('output'); |
|
if (savedOutput) { |
|
setOutput(JSON.parse(savedOutput)); |
|
} |
|
}, []); |
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { |
|
const { name, value } = e.target; |
|
setInputData((prevData) => ({ ...prevData, [name]: value })); |
|
}; |
|
|
|
|
|
const handleSubmit = async (e: FormEvent) => { |
|
e.preventDefault(); |
|
setIsLoading(true); |
|
|
|
try { |
|
const response = await fetch('/api/replicate', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify(inputData), |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (response.ok) { |
|
const timestamp = new Date().toISOString(); |
|
const newOutput = { |
|
prediction: result.prediction, |
|
timestamp, |
|
prompt: inputData.prompt, |
|
}; |
|
const updatedOutput = [...output, newOutput]; |
|
setOutput(updatedOutput); |
|
localStorage.setItem('output', JSON.stringify(updatedOutput)); |
|
setInputData({ |
|
prompt: '', |
|
width: 1024, |
|
height: 576, |
|
steps: 40, |
|
fps: 15, |
|
seed: undefined, |
|
}); |
|
} |
|
} catch (error) { |
|
console.error("An error occurred:", error); |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
return ( |
|
<div className="bg-gray-900 text-white p-4 container mx-auto" style={{ minHeight: '100vh' }}> |
|
<h1 className="text-2xl mb-4">Video Generation App</h1> |
|
<form onSubmit={handleSubmit} className="space-y-4"> |
|
<textarea |
|
name="prompt" |
|
value={inputData.prompt} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full h-24" |
|
placeholder="Enter video prompt..." |
|
></textarea> |
|
<div className="flex flex-wrap -mx-2"> |
|
<label className="flex flex-col items-center w-full md:w-1/5 px-2"> |
|
<span>Width</span> |
|
<input type="number" name="width" value={inputData.width} onChange={handleChange} className="p-2 bg-gray-800 rounded border border-gray-600 w-full" /> |
|
</label> |
|
<label className="flex flex-col items-center w-full md:w-1/5 px-2"> |
|
<span>Height</span> |
|
<input type="number" name="height" value={inputData.height} onChange={handleChange} className="p-2 bg-gray-800 rounded border border-gray-600 w-full" /> |
|
</label> |
|
<label className="flex flex-col items-center w-full md:w-1/5 px-2"> |
|
<span>Steps</span> |
|
<input type="number" name="steps" value={inputData.steps} onChange={handleChange} className="p-2 bg-gray-800 rounded border border-gray-600 w-full" /> |
|
</label> |
|
<label className="flex flex-col items-center w-full md:w-1/5 px-2"> |
|
<span>FPS</span> |
|
<input type="number" name="fps" value={inputData.fps} onChange={handleChange} className="p-2 bg-gray-800 rounded border border-gray-600 w-full" /> |
|
</label> |
|
<label className="flex flex-col items-center w-full md:w-1/5 px-2"> |
|
<span>Seed</span> |
|
<input type="number" name="seed" value={inputData.seed} onChange={handleChange} className="p-2 bg-gray-800 rounded border border-gray-600 w-full" /> |
|
</label> |
|
</div> |
|
|
|
|
|
<button |
|
type="submit" |
|
className={`p-2 px-5 rounded ${isLoading ? 'bg-gray-600' : 'bg-green-600'} text-white`} |
|
disabled={isLoading} |
|
style={{ cursor: isLoading ? 'not-allowed' : 'pointer' }} |
|
> |
|
{isLoading ? 'Generating...' : 'Generate'} |
|
</button> |
|
|
|
</form> |
|
<div className="grid lg:grid-cols-3 grid-cols-1 gap-4 mt-8"> |
|
{output.map((video, index) => ( |
|
<div key={index} className="space-y-2"> |
|
<h2 className="text-xl">Generated Video {index + 1}:</h2> |
|
<div className="relative"> |
|
<video |
|
controls |
|
loop |
|
width="100%" |
|
onMouseOver={event => { |
|
event.currentTarget.play(); |
|
event.currentTarget.setAttribute("controlsList", "nodownload"); |
|
}} |
|
onMouseOut={event => { |
|
event.currentTarget.pause(); |
|
event.currentTarget.removeAttribute("controlsList"); |
|
}} |
|
> |
|
<source src={video.prediction.output} type="video/mp4" /> |
|
</video> |
|
{/* Download Button */} |
|
<button |
|
onClick={async () => { |
|
try { |
|
const response = await fetch(video.prediction.output); |
|
const blob = await response.blob(); |
|
const url = window.URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.href = url; |
|
link.download = 'video.mp4'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
window.URL.revokeObjectURL(url); |
|
} catch (error) { |
|
console.error("Failed to download the video", error); |
|
} |
|
}} |
|
className="absolute top-0 right-0 bg-green-600 text-white px-3 py-1 rounded" |
|
> |
|
Download |
|
</button> |
|
|
|
</div> |
|
<p>Date: {new Date(video.timestamp).toLocaleDateString()} Time: {new Date(video.timestamp).toLocaleTimeString()}</p> |
|
<TruncatedText text={video.prompt} /> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default IndexPage; |