"use client"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useEffect, useRef, useState } from "react"; import { PlayCanvas, type Phase } from "@/components/PlayCanvas"; import { PRESETS } from "@/lib/presets"; import type { ClickIntent, InteractResponse, Session, StartResponse, StoryFrame, } from "@dada/types"; function PlayInner() { const router = useRouter(); const params = useSearchParams(); const [phase, setPhase] = useState("loading-first"); const [session, setSession] = useState(null); const [imageBase64, setImageBase64] = useState(null); const [frame, setFrame] = useState(null); const [intent, setIntent] = useState(null); const [pendingClick, setPendingClick] = useState<{ x: number; y: number; } | null>(null); const [turnNum, setTurnNum] = useState(0); const [error, setError] = useState(null); const startedRef = useRef(false); useEffect(() => { if (startedRef.current) return; startedRef.current = true; let payload: { worldSetting: string; styleGuide: string } | null = null; const presetId = params.get("preset"); if (presetId) { const p = PRESETS.find((x) => x.id === presetId); if (p) { payload = { worldSetting: p.worldSetting, styleGuide: p.styleGuide }; } } else if (params.get("custom") === "1") { const stored = sessionStorage.getItem("dada:custom"); if (stored) { try { payload = JSON.parse(stored); } catch { payload = null; } } } if (!payload) { router.replace("/"); return; } const finalPayload = payload; fetch("/api/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(finalPayload), }) .then(async (r) => { if (!r.ok) { const j = (await r.json().catch(() => ({}))) as { error?: string }; throw new Error(j.error ?? r.statusText); } return r.json() as Promise; }) .then((data) => { setSession({ id: data.sessionId, createdAt: Date.now(), worldSetting: finalPayload.worldSetting, styleGuide: finalPayload.styleGuide, history: [{ frame: data.frame }], }); setFrame(data.frame); setImageBase64(data.imageBase64); setPhase("ready"); setTurnNum(1); }) .catch((e) => setError(String(e))); }, [params, router]); async function handleClick(click: { x: number; y: number }) { if (phase !== "ready" || !session || !imageBase64) return; setPhase("interacting"); setPendingClick(click); setIntent(null); try { const res = await fetch("/api/interact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session, prevImageBase64: imageBase64, click, }), }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(j.error ?? res.statusText); } const data = (await res.json()) as InteractResponse; const updatedHistory = [ ...data.session.history, { frame: data.frame }, ]; setSession({ ...data.session, history: updatedHistory }); setFrame(data.frame); setImageBase64(data.imageBase64); setIntent(data.intent); setPendingClick(null); setTurnNum((t) => t + 1); setPhase("ready"); } catch (e) { setError(String(e)); setPendingClick(null); setPhase("ready"); } } if (error) { return (

An · error · occurred

{error}

Return
); } return (
Dada
Frame · {String(turnNum).padStart(3, "0")} · {session?.id.slice(2, 14) ?? "—"}
{phase === "loading-first" && (

Summoning · the · first · frame

)} {phase === "interacting" && (

AI · is · painting · the · next · moment

this usually takes 12–20 seconds

)} {phase === "ready" && intent?.targetLabel && (

Last · move · {intent.targetLabel}

)} {phase === "ready" && !intent && turnNum > 0 && (

Click · anywhere · to · respond

)}
Ⅰ · Ⅰ
); } export default function PlayPage() { return ( Loading } > ); }