Spaces:
Runtime error
Runtime error
"use client" | |
import { useEffect, useRef, useState, useTransition } from "react" | |
import { usePathname, useRouter, useSearchParams } from "next/navigation" | |
import { Renderer } from "@/components/business/renderer" | |
import { | |
Select, | |
SelectContent, | |
SelectItem, | |
SelectTrigger, | |
SelectValue, | |
} from "@/components/ui/select" | |
import { Switch } from "@/components/ui/switch" | |
import { Label } from "@/components/ui/label" | |
import { newRender, getRender } from "./render" | |
import { RenderedScene } from "./types" | |
import { Game, GameType } from "./games/types" | |
import { defaultGame, games, getGame } from "./games" | |
import { getBackground } from "@/app/queries/getBackground" | |
import { getDialogue } from "@/app/queries/getDialogue" | |
import { getActionnables } from "@/app/queries/getActionnables" | |
import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines" | |
const getInitialRenderedScene = (): RenderedScene => ({ | |
renderId: "", | |
status: "pending", | |
assetUrl: "", | |
error: "", | |
maskUrl: "", | |
segments: [] | |
}) | |
export default function Main() { | |
const [isPending, startTransition] = useTransition() | |
const [rendered, setRendered] = useState<RenderedScene>(getInitialRenderedScene()) | |
const historyRef = useRef<RenderedScene[]>([]) | |
const router = useRouter() | |
const pathname = usePathname() | |
const searchParams = useSearchParams() | |
const requestedGame = (searchParams.get('game') as GameType) || defaultGame | |
const gameRef = useRef<GameType>(requestedGame) | |
const [game, setGame] = useState<Game>(getGame(gameRef.current)) | |
const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine | |
const [engine, setEngine] = useState<Engine>(getEngine(requestedEngine)) | |
const requestedDebug = (searchParams.get('debug') === "true") | |
const [debug, setDebug] = useState<boolean>(requestedDebug) | |
const [situation, setSituation] = useState("") | |
const [dialogue, setDialogue] = useState("") | |
const [hoveredActionnable, setHoveredActionnable] = useState("") | |
const [isBusy, setBusy] = useState<boolean>(true) | |
const busyRef = useRef(true) | |
const loopRef = useRef<any>(null) | |
const loadNextScene = async (nextSituation?: string, nextActionnables?: string[]) => { | |
await startTransition(async () => { | |
console.log("Rendering a scene for " + game.type) | |
// console.log(`rendering scene..`) | |
const newRendered = await newRender({ | |
engine, | |
// SCENE PROMPT | |
prompt: game.getScenePrompt(nextSituation).join(", "), | |
// ACTIONNABLES | |
actionnables: (Array.isArray(nextActionnables) && nextActionnables.length | |
? nextActionnables | |
: game.initialActionnables | |
).slice(0, 10) // too many can slow us down it seems | |
}) | |
console.log("got the first version of our scene!", newRendered) | |
// detect if type game type changed while we were busy | |
if (game?.type !== gameRef?.current) { | |
console.log("game type changed! aborting..") | |
return | |
} | |
// we cheat a bit by displaying the previous image as a placeholder | |
// this is better than displaying a blank image! | |
newRendered.assetUrl = rendered.assetUrl | |
newRendered.maskUrl = rendered.maskUrl | |
historyRef.current.unshift(newRendered) | |
setRendered(newRendered) | |
}) | |
} | |
const checkRenderedLoop = async () => { | |
// console.log("checkRenderedLoop! rendered:", renderedRef.current) | |
clearTimeout(loopRef.current) | |
if (!historyRef.current[0]?.renderId || historyRef.current[0]?.status !== "pending") { | |
// console.log("let's try again in a moments") | |
loopRef.current = setTimeout(() => checkRenderedLoop(), 200) | |
return | |
} | |
// console.log("checking rendering..") | |
await startTransition(async () => { | |
// console.log(`getting latest updated scene..`) | |
try { | |
if (!historyRef.current[0]?.renderId) { | |
throw new Error(`missing renderId`) | |
} | |
// console.log(`calling getRender(${renderedRef.current.renderId})`) | |
const newRendered = await getRender(historyRef.current[0]?.renderId) | |
// console.log(`got latest updated scene:`, renderedRef.current) | |
// detect if type game type changed while we were busy | |
if (game?.type !== gameRef?.current) { | |
console.log("game type changed! aborting..") | |
return | |
} | |
const before = JSON.stringify(historyRef.current[0]) | |
const after = JSON.stringify(newRendered) | |
if (after !== before) { | |
console.log("updating scene..") | |
historyRef.current[0] = newRendered | |
setRendered(historyRef.current[0]) | |
if (newRendered.status === "completed") { | |
setBusy(busyRef.current = false) | |
} | |
} | |
} catch (err) { | |
console.error(err) | |
} | |
clearTimeout(loopRef.current) | |
loopRef.current = setTimeout(() => checkRenderedLoop(), 1000) | |
}) | |
} | |
useEffect(() => { | |
loadNextScene() | |
checkRenderedLoop() | |
}, []) | |
const handleUserAction = async (actionnable: string) => { | |
console.log("user clicked on:", actionnable) | |
setBusy(busyRef.current = true) | |
// TODO: ask Llama2 what to do about it | |
// we need a frame and some actionnables, | |
// perhaps even some music or sound effects | |
await startTransition(async () => { | |
const game = getGame(gameRef.current) | |
let newDialogue = "" | |
try { | |
newDialogue = await getDialogue({ game, situation, actionnable }) | |
console.log(`newDialogue:`, newDialogue) | |
setDialogue(newDialogue) | |
} catch (err) { | |
console.log(`failed to generate dialogue (but it's only a nice to have, so..)`) | |
setDialogue("") | |
} | |
try { | |
const newActionnables = await getActionnables({ game, situation, actionnable, newDialogue }) | |
console.log(`newActionnables:`, newActionnables) | |
const newBackground = await getBackground({ game, situation, actionnable, newDialogue, newActionnables }) | |
console.log(`newBackground:`, newBackground) | |
setSituation(newBackground) | |
console.log("loading next scene..") | |
await loadNextScene(newBackground, newActionnables) | |
// todo we could also use useEffect | |
} catch (err) { | |
console.error(`failed to get one of the mandatory entites: ${err}`) | |
} | |
}) | |
} | |
const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values()) | |
const handleToggleDebug = (isToggledOn: boolean) => { | |
const current = new URLSearchParams(Array.from(searchParams.entries())) | |
current.set("debug", `${isToggledOn}`) | |
const search = current.toString() | |
const query = search ? `?${search}` : "" | |
// for some reason, this doesn't work?! | |
router.replace(`${pathname}${query}`, { }) | |
// workaround.. but it is strange that router.replace doesn't work.. | |
let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() | |
window.history.pushState({path: newurl}, '', newurl) | |
setDebug(isToggledOn) | |
} | |
const handleSelectGame = (newGameType: GameType) => { | |
gameRef.current = newGameType | |
setGame(getGame(newGameType)) | |
/* | |
setRendered({ | |
renderId: "", | |
status: "pending", | |
assetUrl: "", | |
error: "", | |
maskUrl: "", | |
segments:[] | |
}) | |
*/ | |
const current = new URLSearchParams(Array.from(searchParams.entries())) | |
current.set("game", newGameType) | |
const search = current.toString() | |
const query = search ? `?${search}` : "" | |
// for some reason, this doesn't work?! | |
router.replace(`${pathname}${query}`, { }) | |
// workaround.. but it is strange that router.replace doesn't work.. | |
//let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() | |
//window.history.pushState({path: newurl}, '', newurl) | |
// actually we don't handle partial reload very well, so let's reload the whole page | |
window.location = `${window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString()}` as any | |
} | |
const handleSelectEngine = (newEngine: EngineType) => { | |
setEngine(getEngine(newEngine)) | |
const current = new URLSearchParams(Array.from(searchParams.entries())) | |
current.set("engine", newEngine) | |
const search = current.toString() | |
//const query = search ? `?${search}` : "" | |
// for some reason, this doesn't work?! | |
//router.replace(`${pathname}${query}`, { }) | |
// workaround.. but it is strange that router.replace doesn't work.. | |
//let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString() | |
//window.history.pushState({path: newurl}, '', newurl) | |
// actually we don't handle partial reload very well, so let's reload the whole page | |
window.location = `${window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString()}` as any | |
} | |
// determine when to show the spinner | |
const isLoading = isBusy || rendered.status === "pending" | |
return ( | |
<div | |
className="flex flex-col w-full max-w-5xl" | |
> | |
<div className="flex flex-row w-full justify-between items-center px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50 bg-gray-800 dark:bg-gray-800"> | |
<div className="flex flex-row items-center space-x-3 font-mono"> | |
<Label className="flex text-sm">Select a story:</Label> | |
<Select | |
defaultValue={gameRef.current} | |
onValueChange={(value) => { handleSelectGame(value as GameType) }}> | |
<SelectTrigger className="w-[180px]"> | |
<SelectValue className="text-sm" placeholder="Type" /> | |
</SelectTrigger> | |
<SelectContent> | |
{Object.entries(games).map(([key, game]) => | |
<SelectItem key={key} value={key}>{game.title}</SelectItem> | |
)} | |
</SelectContent> | |
</Select> | |
</div> | |
<div className="flex flex-row items-center space-x-3 font-mono"> | |
<Switch | |
checked={debug} | |
onCheckedChange={handleToggleDebug} | |
// we won't disable it, so we can occupy our using while loading | |
// disabled={isLoading} | |
/> | |
<Label>Debug</Label> | |
</div> | |
<div className="flex flex-row items-center space-x-3 font-mono"> | |
<Label className="flex text-sm">Rendering engine:</Label> | |
<Select | |
defaultValue={engine.type} | |
onValueChange={(value) => { handleSelectEngine(value as EngineType) }}> | |
<SelectTrigger className="w-[300px]"> | |
<SelectValue className="text-sm" placeholder="Type" /> | |
</SelectTrigger> | |
<SelectContent> | |
{Object.entries(engines) | |
.filter(([_, engine]) => engine.visible) | |
.map(([key, engine]) => | |
<SelectItem key={key} value={key} disabled={!engine.enabled}>{engine.label} ({engine.modelName})</SelectItem> | |
)} | |
</SelectContent> | |
</Select> | |
</div> | |
</div> | |
<div className={[ | |
"flex flex-col w-full pt-4 space-y-3 text-gray-50 dark:text-gray-50", | |
getGame(gameRef.current).className // apply the game theme | |
].join(" ")}> | |
<div className="flex flex-row"> | |
<div className="text-xl mr-2"> | |
{rendered.segments.length | |
? <span>🔎 Try to click on:</span> | |
: <span>⌛ Searching in the scene..</span> | |
} | |
</div> | |
{clickables.map((clickable, i) => | |
<div key={i} className="flex flex-row text-xl mr-2"> | |
<div className="">{clickable}</div> | |
{i < (clickables.length - 1) ? <div>,</div> : null} | |
</div>)} | |
</div> | |
<div className="text-xl p-4 rounded-xl backdrop-blur-sm bg-white/30"> | |
You are looking at: <span className="font-bold">{hoveredActionnable || "nothing"}</span> | |
</div> | |
<Renderer | |
rendered={rendered} | |
onUserAction={handleUserAction} | |
onUserHover={setHoveredActionnable} | |
isLoading={isLoading} | |
game={game} | |
engine={engine} | |
debug={debug} | |
/> | |
<div className="text-xl rounded-xl backdrop-blur-sm bg-white/30 p-4">{dialogue}</div> | |
</div> | |
</div> | |
) | |
} |