|
import React, { useState, useEffect, FormEvent, ChangeEvent } from 'react'; |
|
|
|
interface InputData { |
|
prompt: string; |
|
negative_prompt: string; |
|
image: File | null; |
|
mask: File | null; |
|
width: number; |
|
height: number; |
|
num_outputs: number; |
|
scheduler: string; |
|
num_inference_steps: number; |
|
guidance_scale: number; |
|
prompt_strength: number; |
|
seed: string; |
|
refine: string; |
|
high_noise_frac: number; |
|
refine_steps: string; |
|
apply_watermark: boolean; |
|
lora_scale: number; |
|
} |
|
|
|
interface Prediction { |
|
output: string[]; |
|
} |
|
|
|
interface OutputData { |
|
prediction: Prediction | null; |
|
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: '', |
|
negative_prompt: '', |
|
image: null, |
|
mask: null, |
|
width: 1024, |
|
height: 1024, |
|
num_outputs: 1, |
|
scheduler: 'K_EULER', |
|
num_inference_steps: 50, |
|
guidance_scale: 7.5, |
|
prompt_strength: 0.8, |
|
seed: '', |
|
refine: 'no_refiner', |
|
high_noise_frac: 0.8, |
|
refine_steps: '', |
|
apply_watermark: false, |
|
lora_scale: 0.6, |
|
}); |
|
|
|
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 | HTMLSelectElement>) => { |
|
const { name, value, type } = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; |
|
|
|
|
|
if (type === 'file') { |
|
const files = (e.target as HTMLInputElement).files; |
|
setInputData((prevData) => ({ ...prevData, [name]: files ? files[0] : null })); |
|
} else if (type === 'checkbox') { |
|
|
|
if ('checked' in e.target) { |
|
setInputData((prevData) => ({ ...prevData, [name]: (e.target as HTMLInputElement).checked })); |
|
} |
|
} else { |
|
setInputData((prevData) => ({ ...prevData, [name]: value })); |
|
} |
|
}; |
|
|
|
|
|
const handleSubmit = async (e: FormEvent) => { |
|
e.preventDefault(); |
|
setIsLoading(true); |
|
try { |
|
const formData = new FormData(); |
|
for (const key in inputData) { |
|
|
|
if (Object.prototype.hasOwnProperty.call(inputData, key)) { |
|
const inputKey = key as keyof InputData; |
|
if (typeof inputData[inputKey] === 'string' || inputData[inputKey] instanceof Blob) { |
|
formData.append(inputKey, inputData[inputKey] as string | Blob); |
|
} |
|
} |
|
} |
|
|
|
const response = await fetch('/api/replicate_logo', { |
|
method: 'POST', |
|
body: formData, |
|
}); |
|
|
|
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: '', |
|
negative_prompt: '', |
|
image: null, |
|
mask: null, |
|
width: 1024, |
|
height: 1024, |
|
num_outputs: 1, |
|
scheduler: 'K_EULER', |
|
num_inference_steps: 50, |
|
guidance_scale: 7.5, |
|
prompt_strength: 0.8, |
|
seed: '', |
|
refine: 'no_refiner', |
|
high_noise_frac: 0.8, |
|
refine_steps: '', |
|
apply_watermark: false, |
|
lora_scale: 0.6, |
|
}); |
|
} |
|
} 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">Image Generation App</h1> |
|
<form onSubmit={handleSubmit} className="space-y-4"> |
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
|
{/* Textareas take full width */} |
|
<div className="col-span-full"> |
|
<textarea |
|
name="prompt" |
|
value={inputData.prompt} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full h-24" |
|
placeholder="Input prompt" |
|
></textarea> |
|
</div> |
|
<div className="col-span-full"> |
|
<textarea |
|
name="negative_prompt" |
|
value={inputData.negative_prompt} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full h-24" |
|
placeholder="Input Negative Prompt" |
|
></textarea> |
|
</div> |
|
|
|
{/* Single row, multiple items */} |
|
<div> |
|
<input |
|
type="file" |
|
name="image" |
|
accept="image/*" |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="file" |
|
name="mask" |
|
accept="image/*" |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="width" |
|
value={inputData.width} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Width" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="height" |
|
value={inputData.height} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Height" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="num_outputs" |
|
value={inputData.num_outputs} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Number of Outputs" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="num_inference_steps" |
|
value={inputData.num_inference_steps} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Number of Inference Steps" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="guidance_scale" |
|
value={inputData.guidance_scale} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Guidance Scale" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="prompt_strength" |
|
value={inputData.prompt_strength} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Prompt Strength" |
|
/> |
|
</div> |
|
<div> |
|
<input |
|
type="text" |
|
name="seed" |
|
value={inputData.seed} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="Seed" |
|
/> |
|
</div> |
|
<div className="col-span-full"> |
|
<select |
|
id="refine" |
|
name="refine" |
|
value={inputData.refine} |
|
onChange={handleChange} |
|
className="w-full p-2 bg-gray-800 rounded border border-gray-600 disabled:cursor-not-allowed disabled:opacity-50 text-white" |
|
> |
|
<option value="no_refiner">no_refiner</option> |
|
<option value="expert_ensemble_refiner">expert_ensemble_refiner</option> |
|
<option value="base_image_refiner">base_image_refiner</option> |
|
</select> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="high_noise_frac" |
|
value={inputData.high_noise_frac} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="High Noise Fraction" |
|
/> |
|
</div> |
|
<div className="col-span-full"> |
|
<label className="flex items-center"> |
|
<input |
|
type="checkbox" |
|
name="apply_watermark" |
|
checked={inputData.apply_watermark} |
|
onChange={handleChange} |
|
className="mr-2" |
|
/> |
|
Apply Watermark |
|
</label> |
|
</div> |
|
<div> |
|
<input |
|
type="number" |
|
name="lora_scale" |
|
value={inputData.lora_scale} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 w-full" |
|
placeholder="LoRA Scale" |
|
/> |
|
</div> |
|
</div> |
|
|
|
{/* Submit button */} |
|
<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((imageData, index) => ( |
|
<div key={index} className="space-y-2"> |
|
<h2 className="text-xl">Generated Image {index + 1}:</h2> |
|
{imageData.prediction?.output ? ( |
|
<div className="relative"> |
|
<img src={imageData.prediction.output[0]} alt={`Generated Image ${index + 1}`} /> |
|
<button |
|
onClick={async () => { |
|
try { |
|
const response = await fetch(imageData.prediction?.output[0] || ""); // Used optional chaining and default to empty string |
|
const blob = await response.blob(); |
|
const url = window.URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.href = url; |
|
link.download = `generated_image_${index + 1}.png`; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
window.URL.revokeObjectURL(url); |
|
} catch (error) { |
|
console.error("Failed to download the image", error); |
|
} |
|
}} |
|
className="absolute top-0 right-0 bg-green-600 text-white px-3 py-1 rounded" |
|
> |
|
Download |
|
</button> |
|
</div> |
|
) : ( |
|
<p>Image URLs not available.</p> |
|
)} |
|
<p>Date: {new Date(imageData.timestamp).toLocaleDateString()} Time: {new Date(imageData.timestamp).toLocaleTimeString()}</p> |
|
<TruncatedText text={imageData.prompt} /> |
|
</div> |
|
))} |
|
</div> |
|
|
|
|
|
</div> |
|
)} |
|
|
|
export default IndexPage; |
|
|