"use client" import { useEffect, useRef, useState, useTransition } from "react" import { useSpring, animated } from "@react-spring/web" import { usePathname, useRouter, useSearchParams } from "next/navigation" 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" 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 [runs, setRuns] = useState(0) const runsRef = useRef(0) const currentLineIndexRef = useRef(0) const [currentLineIndex, setCurrentLineIndex] = useState(0) useEffect(() => { currentLineIndexRef.current = currentLineIndex }, [currentLineIndex]) const [storyLines, setStoryLines] = useState([]) // 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 audio = useAudio() /* // to simulate a "typing" effect however.. we don't need this as we already have an audio player! const [typedStoryText, setTypedStoryText] = useState("") const [typedStoryCharacterIndex, setTypedStoryCharacterIndex] = useState(0) useEffect(() => { if (storyText && typedStoryCharacterIndex < storyText.length) { setTimeout(() => { setTypedStoryText(typedStoryText + story.text[typedStoryCharacterIndex]) setTypedStoryCharacterIndex(typedStoryCharacterIndex + 1) console.log("boom") }, 40) } }, [storyText, typedStoryCharacterIndex]) */ const { progressPercent, remainingTimeInSec } = useCountdown({ isActive: isLocked, timerId: runs, // everytime we change this, the timer will reset durationInSec: /*stage === "interpolate" ? 30 :*/ 35, // 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 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 { console.log("playing audio!") await audio(currentLineAudio) // play console.log("audio has ended, I think? let's go next!") setCurrentLineIndex(currentLineIndexRef.current += 1) // TODO change the line } catch (err) { console.error(err) } } fn() return () => { audio() // stop } }, [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( `px-4 h-16`, `rounded-full`, `transition-all duration-300 ease-in-out`, `backdrop-blur-sm`, isLocked ? `bg-orange-200/50 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`, `text-2xl `, `border`, headingFont.className, // `transition-all duration-300`, // `hover:animate-bounce` )} disabled={isLocked} onClick={handleSubmit} > {isLocked ? `Dreaming..` : "Dream" }
{assetUrl ?
{assetUrl && }
: null}
{storyLines.map((line, i) =>
{ line.text }
)}
) }