"use client" import { useEffect, useRef, useState, useTransition } from "react" import { useSpring, animated } from "@react-spring/web" import { usePathname, useRouter, useSearchParams } from "next/navigation" import useSmoothScroll from "react-smooth-scroll-hook" import { split } from "sentence-splitter" import { useToast } from "@/components/ui/use-toast" import { cn } from "@/lib/utils" import { headingFont } from "@/app/interface/fonts" import { useCharacterLimit } from "@/lib/useCharacterLimit" import { generateStoryLines } from "@/app/server/actions/generateStoryLines" import { Story, StoryLine, TTSVoice } from "@/types" import { TooltipProvider } from "@radix-ui/react-tooltip" import { useCountdown } from "@/lib/useCountdown" import { useAudio } from "@/lib/useAudio" import { Countdown } from "../countdown" import { generateImage } from "@/app/server/actions/generateImage" type Stage = "generate" | "finished" export function Generate() { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const searchParamsEntries = searchParams ? Array.from(searchParams.entries()) : [] const [_isPending, startTransition] = useTransition() const scrollRef = useRef(null) const [isLocked, setLocked] = useState(false) const [promptDraft, setPromptDraft] = useState("") const [assetUrl, setAssetUrl] = useState("") const [isOverSubmitButton, setOverSubmitButton] = useState(false) const [isOverPauseButton, setOverPauseButton] = useState(false) const [runs, setRuns] = useState(0) const runsRef = useRef(0) const currentLineIndexRef = useRef(0) const [currentLineIndex, setCurrentLineIndex] = useState(0) const voices: TTSVoice[] = ["Cloée", "Julian"] const [voice, setVoice] = useState("Cloée") const { scrollTo } = useSmoothScroll({ ref: scrollRef, speed: 2000, direction: 'y', }); useEffect(() => { currentLineIndexRef.current = currentLineIndex }, [currentLineIndex]) const [storyLines, setStoryLines] = useState([]) const [images, setImages] = useState([]) const imagesRef = useRef([]) const imageListKey = images.join("") useEffect(() => { imagesRef.current = images }, [imageListKey]) // computing those is cheap const wholeStory = storyLines.map(line => line.text).join("\n") const currentLine = storyLines.at(currentLineIndex) const currentLineText = currentLine?.text || "" const currentLineAudio = currentLine?.audio || "" // reset the whole player when story changes useEffect(() => { setCurrentLineIndex(0) }, [wholeStory]) const [stage, setStage] = useState("generate") const { toast } = useToast() const { playback, isPlaying, isSwitchingTracks, isLoaded, progress, togglePause } = useAudio() const { progressPercent, remainingTimeInSec } = useCountdown({ isActive: isLocked, timerId: runs, // everytime we change this, the timer will reset durationInSec: /*stage === "interpolate" ? 30 :*/ 50, // it usually takes 40 seconds, but there might be lag onEnd: () => {} }) const { shouldWarn, colorClass, nbCharsUsed, nbCharsLimits } = useCharacterLimit({ value: promptDraft, nbCharsLimits: 70, warnBelow: 10, }) const submitButtonBouncer = useSpring({ transform: isOverSubmitButton ? 'scale(1.05)' : 'scale(1.0)', boxShadow: isOverSubmitButton ? `0px 5px 15px 0px rgba(0, 0, 0, 0.05)` : `0px 0px 0px 0px rgba(0, 0, 0, 0.05)`, loop: true, config: { tension: 300, friction: 10, }, }) const pauseButtonBouncer = useSpring({ transform: isOverPauseButton ? 'scale(1.05)' : 'scale(1.0)', boxShadow: isOverPauseButton ? `0px 5px 15px 0px rgba(0, 0, 0, 0.05)` : `0px 0px 0px 0px rgba(0, 0, 0, 0.05)`, loop: true, config: { tension: 300, friction: 10, }, }) const handleSubmit = () => { if (isLocked) { return } if (!promptDraft) { return } setRuns(runsRef.current + 1) setLocked(true) setStage("generate") scrollRef.current?.scroll({ top: 0, behavior: 'smooth' }) startTransition(async () => { // now you got a read/write object const current = new URLSearchParams(searchParamsEntries) current.set("prompt", promptDraft) const search = current.toString() router.push(`${pathname}${search ? `?${search}` : ""}`) const voice: TTSVoice = "Cloée" setRuns(runsRef.current + 1) try { // console.log("starting transition, calling generateAnimation") const newStoryLines = await generateStoryLines(promptDraft, voice) console.log(`generated ${newStoryLines.length} story lines`) setStoryLines(newStoryLines) } catch (err) { toast({ title: "We couldn't generate your story 👀", description: "We are probably over capacity, but you can try again 🤗", }) console.log("generation failed! probably just a Gradio failure, so let's just run the round robin again!") return } finally { setLocked(false) setStage("finished") } }) } /* This is where we could download existing bedtime stories useEffect(() => { startTransition(async () => { const posts = await getLatestPosts({ maxNbPosts: 32, shuffle: true, }) if (posts?.length) { setCommunityRoll(posts) } }) }, []) */ const handleClickPlay = () => { console.log("let's play the story! but it could also be automatic") } useEffect(() => { const fn = async () => { if (!currentLineAudio) { return } console.log("story audio changed!") try { const isLastLine = (storyLines.length === 0) || (currentLineIndexRef.current === (storyLines.length - 1)) scrollTo(`#story-line-${currentLineIndexRef.current}`) const nextLineIndex = (currentLineIndexRef.current += 1) const nextLineText = storyLines[nextLineIndex]?.text || "" if (nextLineText) { setTimeout(() => { startTransition(async () => { try { const newImage = await generateImage({ positivePrompt: [ "bedtime story illustration", "painting illustration", promptDraft, nextLineText, ].join(", "), width: 1024, height: 800 }) // console.log("newImage:", newImage.slice(0, 50)) setImages(imagesRef.current.concat(newImage)) } catch (err) { setImages(imagesRef.current.concat("")) } }) }, 100) } else { setImages(imagesRef.current.concat("")) } await playback(currentLineAudio, isLastLine) // play if (!isLastLine && nextLineText) { setTimeout(() => { setCurrentLineIndex(nextLineIndex) }, 1000) } } catch (err) { console.error(err) } } fn() return () => { playback() // stop } }, [currentLineText, currentLineAudio]) return (
{isLocked ? : null}
{assetUrl ?
{assetUrl && }
: null}
setPromptDraft(e.target.value)} onKeyDown={({ key }) => { if (key === 'Enter') { if (!isLocked) { handleSubmit() } } }} disabled={isLocked} />
{nbCharsUsed} / {nbCharsLimits}
setOverSubmitButton(true)} onMouseLeave={() => setOverSubmitButton(false)} className={cn( storyLines?.length ? `text-2xl leading-10 px-4 h-16` : `text-3xl leading-14 px-6 h-[70px]`, `rounded-full`, `transition-all duration-300 ease-in-out`, `backdrop-blur-sm`, isLocked ? `bg-blue-900/70 text-sky-50/80 border-yellow-600/10` : `bg-yellow-400/70 text-sky-50 border-yellow-800/20 hover:bg-yellow-400/80`, `text-center`, `w-full`, `border`, headingFont.className, // `transition-all duration-300`, // `hover:animate-bounce` )} disabled={isLocked} onClick={handleSubmit} > {isLocked ? `Creating..` : "Imagine ⭐️" }
{ /* !!storyLines.length &&
setOverPauseButton(true)} onMouseLeave={() => setOverPauseButton(false)} className={cn( `px-4 h-16`, `rounded-full`, `transition-all duration-300 ease-in-out`, `backdrop-blur-sm`, isLocked ? `bg-orange-200/30 text-sky-50/60 border-yellow-600/10` : `bg-yellow-400/50 text-sky-50 border-yellow-800/20 hover:bg-yellow-400/60`, `text-center`, `w-full`, `text-2xl `, `border`, headingFont.className, // `transition-all duration-300`, // `hover:animate-bounce` )} disabled={isLocked} onClick={togglePause} > {isPlaying || isSwitchingTracks ? "Pause 🔊" : "Play 🔊" }
*/ }
{assetUrl ?
{assetUrl && }
: null}
{storyLines.map((line, i) =>
{ line.text.split("").map((c, j, arr) => {c || " "}) }
{images.at(i) ? : null}
)}
) }