import { useEffect, useMemo, useRef, useState } from "react"; import axios, { AxiosError } from "axios"; import AcUnitIcon from "@mui/icons-material/AcUnit"; import LocalFireDepartmentIcon from "@mui/icons-material/LocalFireDepartment"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; import CodeIcon from "@mui/icons-material/Code"; import CodeOffIcon from "@mui/icons-material/CodeOff"; import VisibilityIcon from "@mui/icons-material/Visibility"; import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import ReplayIcon from "@mui/icons-material/Replay"; import MoneyIcon from "@mui/icons-material/Money"; import TollIcon from "@mui/icons-material/Toll"; import TextField from "@mui/material/TextField"; import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Accordion from "@mui/material/Accordion"; import Typography from "@mui/material/Typography"; import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import { nanoid } from "nanoid"; import AppBar from "@mui/material/AppBar"; import Toolbar from "@mui/material/Toolbar"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import { useHost } from "esdeka/react"; import CircularProgress from "@mui/material/CircularProgress"; import Slider from "@mui/material/Slider"; import { useAtom } from "jotai"; import Button from "@mui/material/Button"; import dynamic from "next/dynamic"; import FormControl from "@mui/material/FormControl"; import InputLabel from "@mui/material/InputLabel"; import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import { useColorScheme } from "@mui/material/styles"; import { getTheme, prettify } from "@/utils"; import { answersAtom, showCodeAtom } from "@/store/atoms"; import { COMMAND_ADD_FEATURE, COMMAND_CREATE_GAME, COMMAND_EXTEND_FEATURE, COMMAND_FIX_BUG, COMMAND_LABEL_ADD_FEATURE, COMMAND_LABEL_CREATE_GAME, COMMAND_LABEL_EXTEND_FEATURE, COMMAND_LABEL_FIX_BUG, COMMAND_LABEL_REMOVE_FEATURE, COMMAND_REMOVE_FEATURE, } from "@/constants"; import { baseGame } from "@/constants/baseGame"; import { fontMono } from "@/lib/theme"; import { Codesandbox } from "@/components/Codesandbox"; import ExampleButton from "@/components/base/ExampleButton"; import { Alert, ButtonGroup, ListSubheader } from "@mui/material"; import Secret from "@/components/base/secret"; import { toOpenAI } from "@/services/api"; import { createClient } from "@/services/api/openai"; import { RainbowListItemButton } from "./base/boxes"; import { CustomAxiosError } from "@/services/api/axios"; const MonacoEditor = dynamic(import("@monaco-editor/react"), { ssr: false }); export interface ShareProps { title: string; content: string; } export default function GameCreator() { const ref = useRef(null); const abortController = useRef(null); const [prompt, setPrompt] = useState(""); const [template, setTemplate] = useState(prettify(baseGame.default)); const [runningId, setRunningId] = useState("1"); const [activeId, setActiveId] = useState("1"); const [answers, setAnswers] = useAtom(answersAtom); const [showCode, setShowCode] = useAtom(showCodeAtom); const [loading, setLoading] = useState(false); const [loadingLive, setLoadingLive] = useState(true); const [errorMessage, setErrorMessage] = useState(""); const { mode, systemMode } = useColorScheme(); const { call, subscribe } = useHost(ref, "2DGameCreator"); const connection = useRef(false); const [tries, setTries] = useState(1); // Send a connection request useEffect(() => { const current = answers.find(({ id }) => id === runningId); if (connection.current || tries <= 0) { return () => { /* Consistency */ }; } const timeout = setTimeout(() => { if (current) { call({ template: current.content }); } setTries(tries - 1); }, 1_000); return () => { clearTimeout(timeout); }; }, [call, tries, answers, runningId]); useEffect(() => { if (!connection.current && loadingLive) { const unsubscribe = subscribe(event => { const { action } = event.data; switch (action.type) { case "answer": connection.current = true; setLoadingLive(false); break; default: break; } }); return () => { unsubscribe(); }; } return () => { /* Consistency */ }; }, [subscribe, loadingLive]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.target as HTMLFormElement); const formObject = Object.fromEntries(formData); try { setLoading(true); abortController.current = new AbortController(); const { command, prompt, temperature, template, model, maxTokens } = formObject; const client = createClient(formObject.openAIAPIKey as string); const answer = await toOpenAI({ command: command as string, prompt: prompt as string, temperature: temperature as string, template: template as string, model: model as string, maxTokens: maxTokens as string, client, signal: abortController.current.signal, }); setAnswers(previousAnswers => [answer, ...previousAnswers]); setRunningId(answer.id); setActiveId(answer.id); setTemplate(prettify(answer.content)); setErrorMessage(""); reload(); } catch (error) { const err = error as CustomAxiosError; console.error(err); let errorMessage = ""; // If error is not canceled (from AbortController) if (err.message !== "canceled") { // If we have an error message from the data.error.message, use that if (err.data?.error?.message && err.data.error.message !== "") { errorMessage = err.data.error.message; } // If there's no message but there's a code, use the code else if (err.data?.error?.code) { errorMessage = err.data.error.code; } // If there's neither a message nor a code, use the error's own message else if (err.message) { errorMessage = err.message; } else { errorMessage = "UNKNOWN_ERROR"; } } setErrorMessage(errorMessage); } finally { setLoading(false); } }; const handleSubmitServer = async (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.target as HTMLFormElement); const formObject = Object.fromEntries(formData); try { setLoading(true); abortController.current = new AbortController(); const { data } = await axios.post("/api/generate", formObject, { signal: abortController.current.signal, }); const answer = data; setAnswers(previousAnswers => [answer, ...previousAnswers]); setRunningId(answer.id); setActiveId(answer.id); setTemplate(prettify(answer.content)); setErrorMessage(""); reload(); } catch (error) { if ((error as { message?: string }).message !== "canceled") { const err = error as AxiosError; console.error(err); setErrorMessage(err.response?.data?.message ?? err.message); } } finally { setLoading(false); } }; const handleCancel = async () => { if (abortController.current) { abortController.current.abort(); } setLoading(false); reload(); }; const sortedAnswers = useMemo(() => { return [...answers].sort((a, b) => { if (a.id === "1") return -1; if (b.id === "1") return 1; return 0; }); }, [answers]); const current = answers.find(({ id }) => id === activeId); function reload() { connection.current = false; if (ref.current) { ref.current.src = `/live?${nanoid()}`; setLoadingLive(true); setTries(1); } } return ( <> 2D GameCreator {process.env.NEXT_PUBLIC_VERSION} { setShowCode(previousState => !previousState); }} > {showCode ? : } {showCode && ( { if (event.key === "s" && event.metaKey) { event.preventDefault(); setAnswers(previousAnswers => previousAnswers.map(previousAnswer => { return previousAnswer.id === activeId ? { ...previousAnswer, content: template, } : previousAnswer; }) ); setTemplate(previousState => prettify(previousState)); reload(); } }} > { setTemplate(value ?? ""); }} /> )} setPrompt(e.target.value)} minRows={3} InputProps={{ style: fontMono.style, }} /> Command {errorMessage && {errorMessage}} } aria-controls="gtp-options-content" id="gtp-options-header" sx={{ bgcolor: "background.paper", color: "text.primary", }} > Options Model { setTemplate(event.target.value); }} /> Examples {/* */} Games {sortedAnswers.map((answer, index) => { return ( {answer.id === "1" ? undefined : ( { setAnswers(previousAnswers => previousAnswers.filter( ({ id }) => id !== answer.id ) ); if (runningId === answer.id) { const previous = answers[index + 1]; if (previous) { setActiveId( previous.id ); setRunningId( previous.id ); setTemplate( prettify( previous.content ) ); reload(); } } }} > )} } disablePadding > {activeId === answer.id ? ( { setActiveId(answer.id); setRunningId(answer.id); setTemplate(prettify(answer.content)); reload(); }} > {runningId === answer.id ? ( ) : ( )} ) : ( { setActiveId(answer.id); setRunningId(answer.id); setTemplate(prettify(answer.content)); reload(); }} > {runningId === answer.id ? ( ) : ( )} )} ); })} Game Preview { reload(); }} > {current && current.id !== "1" && ( <> Share on )} {loadingLive && ( )} { if (current) { setLoadingLive(true); setTries(1); connection.current = false; call({ template: current.content }); } }} src="/live" /> ); }