Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
"use client" | |
import { useEffect, useState, useTransition } from "react" | |
import { useStore } from "@/app/state/useStore" | |
import { cn } from "@/lib/utils" | |
import { VideoGenerationModel, VideoInfo } from "@/types" | |
import { useLocalStorage } from "usehooks-ts" | |
import { localStorageKeys } from "@/app/state/localStorageKeys" | |
import { defaultSettings } from "@/app/state/defaultSettings" | |
import { Input } from "@/components/ui/input" | |
import { Textarea } from "@/components/ui/textarea" | |
import { Button } from "@/components/ui/button" | |
import { submitVideoRequest } from "@/app/server/actions/submitVideoRequest" | |
import { PendingVideoList } from "@/app/interface/pending-video-list" | |
import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos" | |
import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName" | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" | |
import { defaultVideoModel, defaultVoice } from "@/app/config" | |
export function UserChannelView() { | |
const [_isPending, startTransition] = useTransition() | |
const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>( | |
localStorageKeys.huggingfaceApiKey, | |
defaultSettings.huggingfaceApiKey | |
) | |
const [titleDraft, setTitleDraft] = useState("") | |
const [descriptionDraft, setDescriptionDraft] = useState("") | |
const [tagsDraft, setTagsDraft] = useState("") | |
const [promptDraft, setPromptDraft] = useState("") | |
const [modelDraft, setModelDraft] = useState<VideoGenerationModel>(defaultVideoModel) | |
const [loraDraft, setLoraDraft] = useState("") | |
const [styleDraft, setStyleDraft] = useState("") | |
const [voiceDraft, setVoiceDraft] = useState(defaultVoice) | |
const [musicDraft, setMusicDraft] = useState("") | |
// we do not include the tags in the list of required fields | |
const missingFields = !titleDraft || !descriptionDraft || !promptDraft | |
const [isSubmitting, setIsSubmitting] = useState(false) | |
const userChannel = useStore(s => s.userChannel) | |
const userChannels = useStore(s => s.userChannels) | |
const userVideos = useStore(s => s.userVideos) | |
const setUserChannel = useStore(s => s.setUserChannel) | |
const setUserChannels = useStore(s => s.setUserChannels) | |
const setUserVideos = useStore(s => s.setUserVideos) | |
useEffect(() => { | |
if (!userChannel) { | |
return | |
} | |
startTransition(async () => { | |
const videos = await getChannelVideos({ | |
channel: userChannel, | |
// status: undefined, // we want *all* status | |
}) | |
console.log("setCurrentVideos:", videos) | |
setUserVideos(videos) | |
}) | |
}, [huggingfaceApiKey, userChannel, userChannel?.id]) | |
const handleSubmit = () => { | |
if (!userChannel) { | |
return | |
} | |
if (!titleDraft || !promptDraft) { | |
console.log("missing title or prompt") | |
return | |
} | |
setIsSubmitting(true) | |
startTransition(async () => { | |
try { | |
const newVideo = await submitVideoRequest({ | |
channel: userChannel, | |
apiKey: huggingfaceApiKey, | |
title: titleDraft, | |
description: descriptionDraft, | |
prompt: promptDraft, | |
model: modelDraft, | |
lora: loraDraft, | |
style: styleDraft, | |
voice: voiceDraft, | |
music: musicDraft, | |
tags: tagsDraft.trim().split(",").map(x => x.trim()).filter(x => x), | |
}) | |
// in case of success we update the frontend immediately | |
// with our draft video | |
setUserVideos([newVideo, ...userVideos]) | |
setPromptDraft("") | |
setDescriptionDraft("") | |
setTagsDraft("") | |
setTitleDraft("") | |
setModelDraft(defaultVideoModel) | |
setVoiceDraft(defaultVoice) | |
setMusicDraft("") | |
setLoraDraft("") | |
setStyleDraft("") | |
// also renew the cache on Next's side | |
/* | |
await getChannelVideos({ | |
channel: currentChannel, | |
apiKey: huggingfaceApiKey, | |
renewCache: true, | |
}) | |
*/ | |
} catch (err) { | |
console.error(err) | |
} finally { | |
setIsSubmitting(false) | |
} | |
}) | |
} | |
const handleDelete = (video: VideoInfo) => { | |
// step 1: delete it from the dataset | |
// step 2: if the video has already been generated, | |
// we ask the robot to delete it from the index | |
} | |
return ( | |
<div className={cn( | |
`flex flex-col space-y-8` | |
)}> | |
<div className="flex flex-col space-y-8 max-w-4xl"> | |
<h2 className="text-3xl font-bold">Robot channel settings:</h2> | |
<p>TODO</p> | |
<h2 className="text-3xl font-bold">Create a new AI video:</h2> | |
<div className="flex flex-row space-x-2 items-start"> | |
<label className="flex w-24 pt-1">Title (required):</label> | |
<div className="flex flex-col space-y-2 flex-grow"> | |
<Input | |
placeholder="Title" | |
className="font-mono" | |
onChange={(x) => { | |
setTitleDraft(x.target.value) | |
}} | |
value={titleDraft} | |
/> | |
</div> | |
</div> | |
<div className="flex flex-row space-x-2 items-start"> | |
<label className="flex w-24 pt-1">Description (required):</label> | |
<div className="flex flex-col space-y-2 flex-grow"> | |
<Textarea | |
placeholder="Description" | |
className="font-mono" | |
rows={2} | |
onChange={(x) => { | |
setDescriptionDraft(x.target.value) | |
}} | |
value={descriptionDraft} | |
/> | |
<p className="text-neutral-100/70"> | |
Short description (visible to humans, and used as context by the AI). | |
</p> | |
</div> | |
</div> | |
<div className="flex flex-row space-x-2 items-start"> | |
<label className="flex w-24 pt-1">Prompt (required):</label> | |
<div className="flex flex-col space-y-2 flex-grow"> | |
<Textarea | |
placeholder="Prompt" | |
className="font-mono" | |
rows={6} | |
onChange={(x) => { | |
setPromptDraft(x.target.value) | |
}} | |
value={promptDraft} | |
/> | |
<p className="text-neutral-100/70"> | |
Describe your video content, in a synthetic way. | |
</p> | |
</div> | |
</div> | |
<div className="flex flex-row space-x-2 items-start"> | |
<label className="flex w-24 pt-1">Video model:</label> | |
<div className="flex flex-col space-y-2 flex-grow"> | |
<Select | |
onValueChange={(value: string) => { | |
setModelDraft(parseVideoModelName(value, defaultVideoModel)) | |
}} | |
defaultValue={defaultVideoModel}> | |
<SelectTrigger className=""> | |
<SelectValue placeholder="Video model" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="SVD">SVD</SelectItem> | |
<SelectItem value="HotshotXL">HotshotXL</SelectItem> | |
<SelectItem value="LaVie">LaVie</SelectItem> | |
</SelectContent> | |
</Select> | |
</div> | |
</div> | |
<div className="flex flex-row space-x-2 items-start"> | |
<label className="flex w-24 pt-1">Tags (optional):</label> | |
<div className="flex flex-col space-y-2 flex-grow"> | |
<Input | |
placeholder="Tags" | |
className="font-mono" | |
onChange={(x) => { | |
setTagsDraft(x.target.value) | |
}} | |
value={tagsDraft} | |
/> | |
<p className="text-neutral-100/70"> | |
Comma-separated tags (eg. "Education, Sports") | |
</p> | |
</div> | |
</div> | |
<div className="flex flex-row space-x-2 items-center justify-between"> | |
<Button | |
onClick={handleSubmit} | |
disabled={isSubmitting} | |
className={cn( | |
isSubmitting || missingFields ? `opacity-50` : `opacity-100` | |
)} | |
> | |
{missingFields ? 'Please fill the form' : isSubmitting ? 'Adding to the queue..' : 'Add prompt to the queue'} | |
</Button> | |
<p>Note: It can take a few hours for the video to be generated.</p> | |
</div> | |
</div> | |
<h2 className="text-3xl font-bold">Current video prompts:</h2> | |
<PendingVideoList | |
videos={userVideos} | |
onDelete={handleDelete} | |
/> | |
</div> | |
) | |
} |