|
"use client"; |
|
|
|
import { LoadingIcon } from "@/components/LoadingIcon"; |
|
import { Button } from "@/components/ui/button"; |
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"; |
|
import { Input } from "@/components/ui/input"; |
|
import { Label } from "@/components/ui/label"; |
|
import { |
|
checkStatus, |
|
generate, |
|
generate_img, |
|
generate_img_with_controlnet, |
|
getUploadUrl, |
|
} from "@/server/generate"; |
|
import { useEffect, useState } from "react"; |
|
|
|
import { |
|
Select, |
|
SelectContent, |
|
SelectGroup, |
|
SelectItem, |
|
SelectLabel, |
|
SelectTrigger, |
|
SelectValue, |
|
} from "@/components/ui/select"; |
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
|
import { ImageGenerationResult } from "@/components/ImageGenerationResult"; |
|
import { WebsocketDemo } from "@/components/WebsocketDemo"; |
|
|
|
export default function Page() { |
|
return ( |
|
<main className="flex min-h-screen flex-col items-center justify-between mt-10"> |
|
<Tabs defaultValue="ws" className="w-full max-w-[600px]"> |
|
<TabsList className="grid w-full grid-cols-4"> |
|
<TabsTrigger value="ws">Realtime</TabsTrigger> |
|
<TabsTrigger value="txt2img">txt2img</TabsTrigger> |
|
<TabsTrigger value="img2img">img2img</TabsTrigger> |
|
<TabsTrigger value="controlpose">Controlpose</TabsTrigger> |
|
</TabsList> |
|
<TabsContent value="ws"> |
|
<WebsocketDemo /> |
|
</TabsContent> |
|
<TabsContent value="txt2img"> |
|
<Txt2img /> |
|
</TabsContent> |
|
<TabsContent value="img2img"> |
|
<Img2img /> |
|
</TabsContent> |
|
<TabsContent value="controlpose"> |
|
<OpenposeToImage /> |
|
</TabsContent> |
|
</Tabs> |
|
</main> |
|
); |
|
} |
|
|
|
function Txt2img() { |
|
const [prompt, setPrompt] = useState(""); |
|
const [loading, setLoading] = useState(false); |
|
const [runIds, setRunIds] = useState<string[]>([]); |
|
|
|
return ( |
|
<Card className="w-full max-w-[600px]"> |
|
<CardHeader> |
|
Comfy Deploy - Vector Line Art Tool |
|
<div className="text-xs text-foreground opacity-50"> |
|
Lora -{" "} |
|
<a href="https://civitai.com/models/256144/stick-line-vector-illustration"> |
|
stick-line-vector-illustration |
|
</a> |
|
</div> |
|
</CardHeader> |
|
<CardContent> |
|
<form |
|
className="grid w-full items-center gap-1.5" |
|
onSubmit={(e) => { |
|
e.preventDefault(); |
|
|
|
if (loading) return; |
|
setLoading(true); |
|
|
|
const promises = Array(4).fill(null).map(() => { |
|
return generate(prompt) |
|
.then((res) => { |
|
if (res) { |
|
setRunIds((ids) => [...ids, res.run_id]); |
|
} |
|
return res; |
|
}) |
|
.catch((error) => { |
|
console.error(error); |
|
}); |
|
}); |
|
|
|
Promise.all(promises).finally(() => { |
|
setLoading(false); |
|
}); |
|
}} |
|
> |
|
<Label htmlFor="picture">Image prompt</Label> |
|
<Input |
|
id="picture" |
|
type="text" |
|
value={prompt} |
|
onChange={(e) => setPrompt(e.target.value)} |
|
/> |
|
<Button type="submit" className="flex gap-2" disabled={loading}> |
|
Generate {loading && <LoadingIcon />} |
|
</Button> |
|
|
|
<div className="grid grid-cols-2 gap-4"> |
|
{runIds.map((runId, index) => ( |
|
<ImageGenerationResult key={index} runId={runId} /> |
|
))} |
|
</div> |
|
</form> |
|
</CardContent> |
|
</Card> |
|
); |
|
} |
|
|
|
function Img2img() { |
|
const [prompt, setPrompt] = useState<File>(); |
|
const [image, setImage] = useState(""); |
|
const [loading, setLoading] = useState(false); |
|
const [runId, setRunId] = useState(""); |
|
const [status, setStatus] = useState<string>(); |
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
if (!e.target.files) return; |
|
setPrompt(e.target.files[0]); |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
if (!runId) return; |
|
const interval = setInterval(() => { |
|
checkStatus(runId).then((res) => { |
|
if (res) setStatus(res.status); |
|
if (res && res.status === "success") { |
|
console.log(res.outputs[0]?.data); |
|
setImage(res.outputs[0]?.data?.images[0].url); |
|
setLoading(false); |
|
clearInterval(interval); |
|
} |
|
}); |
|
}, 2000); |
|
return () => clearInterval(interval); |
|
}, [runId]); |
|
|
|
return ( |
|
<Card className="w-full max-w-[600px]"> |
|
<CardHeader>Comfy Deploy - Scribble to Anime Girl</CardHeader> |
|
<CardContent> |
|
<form |
|
className="grid w-full items-center gap-1.5" |
|
onSubmit={(e) => { |
|
e.preventDefault(); |
|
if (loading) return; |
|
if (!prompt) return; |
|
|
|
setImage(""); |
|
|
|
setStatus("getting url for upload"); |
|
|
|
console.log(prompt?.type, prompt?.size); |
|
|
|
getUploadUrl(prompt?.type, prompt?.size).then((res) => { |
|
if (!res) return; |
|
|
|
setStatus("uploading input"); |
|
|
|
console.log(res); |
|
|
|
fetch(res.upload_url, { |
|
method: "PUT", |
|
body: prompt, |
|
headers: { |
|
"Content-Type": prompt.type, |
|
"x-amz-acl": "public-read", |
|
"Content-Length": prompt.size.toString(), |
|
}, |
|
}).then((_res) => { |
|
if (_res.ok) { |
|
setStatus("uploaded input"); |
|
|
|
setLoading(true); |
|
generate_img(res.download_url).then((res) => { |
|
console.log(res); |
|
if (!res) { |
|
setStatus("error"); |
|
setLoading(false); |
|
return; |
|
} |
|
setRunId(res.run_id); |
|
}); |
|
setStatus("preparing"); |
|
} |
|
}); |
|
}); |
|
}} |
|
> |
|
<Label htmlFor="picture">Image prompt</Label> |
|
<Input id="picture" type="file" onChange={handleFileChange} /> |
|
<Button type="submit" className="flex gap-2" disabled={loading}> |
|
Generate {loading && <LoadingIcon />} |
|
</Button> |
|
|
|
{runId && <ImageGenerationResult key={runId} runId={runId} className="aspect-square"/>} |
|
</form> |
|
</CardContent> |
|
</Card> |
|
); |
|
} |
|
|
|
const poses = { |
|
arms_on_hips: { |
|
url: "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(1).png", |
|
name: "Arms on Hips", |
|
}, |
|
waving: { |
|
url: "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(2).png", |
|
name: "Waving", |
|
}, |
|
legs_together_sideways: { |
|
url: "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(3).png", |
|
name: "Legs together, body at an angle", |
|
}, |
|
excited_jump: { |
|
url: "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(4).png", |
|
name: "excited jump", |
|
}, |
|
pointing_to_the_stars: { |
|
url: "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(5).png", |
|
name: "Pointing to the stars", |
|
}, |
|
}; |
|
|
|
function OpenposeToImage() { |
|
const [prompt, setPrompt] = useState(""); |
|
const [poseImageUrl, setPoseImageUrl] = useState( |
|
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(1).png", |
|
); |
|
const [poseLoading, setPoseLoading] = useState(false); |
|
const [image, setImage] = useState(""); |
|
const [loading, setLoading] = useState(false); |
|
const [runId, setRunId] = useState(""); |
|
const [status, setStatus] = useState<string>(); |
|
|
|
const handleSelectChange = (value: keyof typeof poses) => { |
|
setPoseImageUrl(poses[value].url); |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
if (!runId) return; |
|
const interval = setInterval(() => { |
|
checkStatus(runId).then((res) => { |
|
if (res) setStatus(res.status); |
|
if (res && res.status === "success") { |
|
console.log(res.outputs[0]?.data); |
|
setImage(res.outputs[0]?.data?.images[0].url); |
|
setLoading(false); |
|
clearInterval(interval); |
|
} |
|
}); |
|
}, 2000); |
|
return () => clearInterval(interval); |
|
}, [runId]); |
|
|
|
return ( |
|
<Card className="w-full max-w-[600px]"> |
|
<CardHeader> |
|
Comfy Deploy - Pose Creator Tool |
|
<div className="text-xs text-foreground opacity-50"> |
|
OpenPose -{" "} |
|
<a href="https://civitai.com/models/13647/super-pose-book-vol1-controlnet"> |
|
pose book |
|
</a> |
|
</div> |
|
</CardHeader> |
|
<CardContent> |
|
<form |
|
className="grid w-full items-center gap-1.5" |
|
onSubmit={(e) => { |
|
if (loading) return; |
|
|
|
e.preventDefault(); |
|
setLoading(true); |
|
generate_img_with_controlnet(poseImageUrl, prompt).then((res) => { |
|
console.log("here", res); |
|
if (!res) { |
|
setStatus("error"); |
|
setLoading(false); |
|
return; |
|
} |
|
setRunId(res.run_id); |
|
}); |
|
setStatus("preparing"); |
|
}} |
|
> |
|
<Select |
|
defaultValue={"Arms on Hips"} |
|
onValueChange={(value) => { |
|
handleSelectChange(value as keyof typeof poses); |
|
setPoseLoading(true); // Start loading when a new pose is selected |
|
}} |
|
> |
|
<Label htmlFor="picture">Pose</Label> |
|
<SelectTrigger className="w-[180px]"> |
|
<SelectValue placeholder="Select a Pose" /> |
|
</SelectTrigger> |
|
<SelectContent> |
|
<SelectGroup> |
|
<SelectLabel>Poses</SelectLabel> |
|
{Object.entries(poses).map(([poseName, attr]) => ( |
|
<SelectItem key={poseName} value={poseName}> |
|
{attr.name} |
|
</SelectItem> |
|
))} |
|
</SelectGroup> |
|
</SelectContent> |
|
</Select> |
|
<Label htmlFor="picture">Image prompt</Label> |
|
<Input |
|
id="picture" |
|
type="text" |
|
value={prompt} |
|
onChange={(e) => setPrompt(e.target.value)} |
|
/> |
|
<Button type="submit" className="flex gap-2" disabled={loading}> |
|
Generate {loading && <LoadingIcon />} |
|
</Button> |
|
|
|
<div className="grid grid-cols-2 gap-4"> |
|
<div className="w-full rounded-lg relative"> |
|
{/* Pose Image */} |
|
{poseLoading && ( |
|
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center"> |
|
<LoadingIcon /> |
|
</div> |
|
)} |
|
{poseImageUrl && ( |
|
<img |
|
className="w-full h-full object-contain" |
|
src={poseImageUrl} |
|
alt="Selected pose" |
|
onLoad={() => setPoseLoading(false)} |
|
></img> |
|
)} |
|
</div> |
|
{/* <Separator |
|
orientation="vertical" |
|
className="border-gray-200" |
|
decorative |
|
/> */} |
|
<div className="w-full h-full"> |
|
{runId && <ImageGenerationResult key={runId} runId={runId} className="aspect-[768/1152]"/>} |
|
</div> |
|
</div> |
|
</form> |
|
</CardContent> |
|
</Card> |
|
); |
|
} |
|
|