Spaces:
Runtime error
Runtime error
Julian Bilcke
commited on
Commit
·
637dd5c
1
Parent(s):
c32c4ae
improve UI + add caching
Browse files- package-lock.json +6 -0
- package.json +1 -0
- src/app/games/city.ts +2 -0
- src/app/games/doom.ts +2 -0
- src/app/games/dungeon.ts +2 -0
- src/app/games/pirates.ts +2 -0
- src/app/games/types.ts +3 -0
- src/app/main.tsx +46 -25
- src/app/page.tsx +10 -3
- src/app/render.ts +53 -46
- src/components/business/image-renderer.tsx +52 -56
- src/components/misc/progress.tsx +8 -8
- src/fonts/Lugrasimo-Regular.woff2 +0 -0
- src/lib/fonts.ts +49 -0
- tailwind.config.js +8 -0
package-lock.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
"name": "video-quest",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
| 11 |
"@huggingface/agents": "^0.0.4",
|
| 12 |
"@huggingface/inference": "^2.6.1",
|
| 13 |
"@radix-ui/react-accordion": "^1.1.2",
|
|
@@ -189,6 +190,11 @@
|
|
| 189 |
"react-dom": ">=16.8.0"
|
| 190 |
}
|
| 191 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
"node_modules/@huggingface/agents": {
|
| 193 |
"version": "0.0.4",
|
| 194 |
"resolved": "https://registry.npmjs.org/@huggingface/agents/-/agents-0.0.4.tgz",
|
|
|
|
| 8 |
"name": "video-quest",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"@gorgonjs/gorgon": "^1.4.1",
|
| 12 |
"@huggingface/agents": "^0.0.4",
|
| 13 |
"@huggingface/inference": "^2.6.1",
|
| 14 |
"@radix-ui/react-accordion": "^1.1.2",
|
|
|
|
| 190 |
"react-dom": ">=16.8.0"
|
| 191 |
}
|
| 192 |
},
|
| 193 |
+
"node_modules/@gorgonjs/gorgon": {
|
| 194 |
+
"version": "1.4.1",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@gorgonjs/gorgon/-/gorgon-1.4.1.tgz",
|
| 196 |
+
"integrity": "sha512-XUTvRODad+uD89CVoLQEi3aOaJC/x9+KqLBKil4a+hKlrDRc6TAoEofn/Kje/S4Q+ylwJRbhZnb98QgiSZxIqw=="
|
| 197 |
+
},
|
| 198 |
"node_modules/@huggingface/agents": {
|
| 199 |
"version": "0.0.4",
|
| 200 |
"resolved": "https://registry.npmjs.org/@huggingface/agents/-/agents-0.0.4.tgz",
|
package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
"lint": "next lint"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
|
|
|
| 12 |
"@huggingface/agents": "^0.0.4",
|
| 13 |
"@huggingface/inference": "^2.6.1",
|
| 14 |
"@radix-ui/react-accordion": "^1.1.2",
|
|
|
|
| 9 |
"lint": "next lint"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
+
"@gorgonjs/gorgon": "^1.4.1",
|
| 13 |
"@huggingface/agents": "^0.0.4",
|
| 14 |
"@huggingface/inference": "^2.6.1",
|
| 15 |
"@radix-ui/react-accordion": "^1.1.2",
|
src/app/games/city.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { Game } from "./types"
|
| 2 |
|
| 3 |
const actions = [
|
|
@@ -43,6 +44,7 @@ const initialActionnables = [
|
|
| 43 |
export const game: Game = {
|
| 44 |
title: "City",
|
| 45 |
type: "city",
|
|
|
|
| 46 |
initialSituation,
|
| 47 |
initialActionnables,
|
| 48 |
getScenePrompt: (situation?: string) => [
|
|
|
|
| 1 |
+
import { edu } from "@/lib/fonts"
|
| 2 |
import { Game } from "./types"
|
| 3 |
|
| 4 |
const actions = [
|
|
|
|
| 44 |
export const game: Game = {
|
| 45 |
title: "City",
|
| 46 |
type: "city",
|
| 47 |
+
className: edu.className,
|
| 48 |
initialSituation,
|
| 49 |
initialActionnables,
|
| 50 |
getScenePrompt: (situation?: string) => [
|
src/app/games/doom.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { Game } from "./types"
|
| 2 |
|
| 3 |
const initialSituation = [
|
|
@@ -16,6 +17,7 @@ const initialActionnables = [
|
|
| 16 |
export const game: Game = {
|
| 17 |
title: "Doom",
|
| 18 |
type: "doom",
|
|
|
|
| 19 |
initialSituation,
|
| 20 |
initialActionnables,
|
| 21 |
getScenePrompt: (situation?: string) => [
|
|
|
|
| 1 |
+
import { orbitron } from "@/lib/fonts"
|
| 2 |
import { Game } from "./types"
|
| 3 |
|
| 4 |
const initialSituation = [
|
|
|
|
| 17 |
export const game: Game = {
|
| 18 |
title: "Doom",
|
| 19 |
type: "doom",
|
| 20 |
+
className: orbitron.className,
|
| 21 |
initialSituation,
|
| 22 |
initialActionnables,
|
| 23 |
getScenePrompt: (situation?: string) => [
|
src/app/games/dungeon.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { Game, Scene } from "./types"
|
| 2 |
|
| 3 |
const actions = [
|
|
@@ -47,6 +48,7 @@ const initialActionnables = [
|
|
| 47 |
export const game: Game = {
|
| 48 |
title: "Dungeon",
|
| 49 |
type: "dungeon",
|
|
|
|
| 50 |
initialSituation,
|
| 51 |
initialActionnables,
|
| 52 |
getScenePrompt: (situation?: string) => [
|
|
|
|
| 1 |
+
import { amatic } from "@/lib/fonts"
|
| 2 |
import { Game, Scene } from "./types"
|
| 3 |
|
| 4 |
const actions = [
|
|
|
|
| 48 |
export const game: Game = {
|
| 49 |
title: "Dungeon",
|
| 50 |
type: "dungeon",
|
| 51 |
+
className: amatic.className,
|
| 52 |
initialSituation,
|
| 53 |
initialActionnables,
|
| 54 |
getScenePrompt: (situation?: string) => [
|
src/app/games/pirates.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { Game } from "./types"
|
| 2 |
|
| 3 |
const actions = [
|
|
@@ -54,6 +55,7 @@ const initialSituation = [
|
|
| 54 |
export const game: Game = {
|
| 55 |
title: "Pirates",
|
| 56 |
type: "pirates",
|
|
|
|
| 57 |
initialSituation,
|
| 58 |
initialActionnables,
|
| 59 |
getScenePrompt: (situation?: string) => [
|
|
|
|
| 1 |
+
import { lugrasimo } from "@/lib/fonts"
|
| 2 |
import { Game } from "./types"
|
| 3 |
|
| 4 |
const actions = [
|
|
|
|
| 55 |
export const game: Game = {
|
| 56 |
title: "Pirates",
|
| 57 |
type: "pirates",
|
| 58 |
+
className: lugrasimo.className,
|
| 59 |
initialSituation,
|
| 60 |
initialActionnables,
|
| 61 |
getScenePrompt: (situation?: string) => [
|
src/app/games/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
export type GameType = 'pirates' | 'city' | 'dungeon' | 'doom'
|
| 2 |
|
| 3 |
export interface Scene {
|
|
@@ -8,6 +10,7 @@ export interface Scene {
|
|
| 8 |
export interface Game {
|
| 9 |
title: string
|
| 10 |
type: GameType
|
|
|
|
| 11 |
initialSituation: string
|
| 12 |
initialActionnables: string[]
|
| 13 |
getScenePrompt: (situation?: string) => string[]
|
|
|
|
| 1 |
+
import { FontName } from "@/lib/fonts"
|
| 2 |
+
|
| 3 |
export type GameType = 'pirates' | 'city' | 'dungeon' | 'doom'
|
| 4 |
|
| 5 |
export interface Scene {
|
|
|
|
| 10 |
export interface Game {
|
| 11 |
title: string
|
| 12 |
type: GameType
|
| 13 |
+
className: string
|
| 14 |
initialSituation: string
|
| 15 |
initialActionnables: string[]
|
| 16 |
getScenePrompt: (situation?: string) => string[]
|
src/app/main.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
import { useEffect, useRef, useState, useTransition } from "react"
|
| 4 |
-
import { useSearchParams } from
|
|
|
|
| 5 |
|
| 6 |
import { ImageRenderer } from "@/components/business/image-renderer"
|
| 7 |
|
|
@@ -16,7 +17,7 @@ import {
|
|
| 16 |
import { render } from "./render"
|
| 17 |
|
| 18 |
import { RenderedScene } from "./types"
|
| 19 |
-
import { GameType } from "./games/types"
|
| 20 |
import { defaultGame, games, getGame } from "./games"
|
| 21 |
import { getBackground } from "@/app/queries/getBackground"
|
| 22 |
import { getDialogue } from "@/app/queries/getDialogue"
|
|
@@ -30,11 +31,15 @@ export default function Main() {
|
|
| 30 |
maskBase64: "",
|
| 31 |
segments:[]
|
| 32 |
})
|
|
|
|
|
|
|
| 33 |
const searchParams = useSearchParams()
|
| 34 |
|
| 35 |
const requestedGame = (searchParams.get('game') as GameType) || defaultGame
|
| 36 |
-
console.log("requestedGame:"
|
| 37 |
const gameRef = useRef<GameType>(requestedGame)
|
|
|
|
|
|
|
| 38 |
const [situation, setSituation] = useState("")
|
| 39 |
const [scene, setScene] = useState("")
|
| 40 |
const [dialogue, setDialogue] = useState("")
|
|
@@ -46,12 +51,7 @@ export default function Main() {
|
|
| 46 |
setLoading(true)
|
| 47 |
|
| 48 |
await startTransition(async () => {
|
| 49 |
-
|
| 50 |
-
// console.log(`getting agent..`)
|
| 51 |
-
// note: we use a ref so that it can be changed in the background
|
| 52 |
-
const type = gameRef?.current
|
| 53 |
-
console.log("type:", type)
|
| 54 |
-
const game = getGame(type)
|
| 55 |
|
| 56 |
// console.log(`rendering scene..`)
|
| 57 |
const newRendered = await render(
|
|
@@ -65,10 +65,9 @@ export default function Main() {
|
|
| 65 |
).slice(0, 6) // too many can slow us down it seems
|
| 66 |
)
|
| 67 |
|
| 68 |
-
// detect if
|
| 69 |
-
if (type !== gameRef?.current) {
|
| 70 |
-
console.log("
|
| 71 |
-
setTimeout(() => { loadNextScene() }, 0)
|
| 72 |
return
|
| 73 |
}
|
| 74 |
|
|
@@ -130,23 +129,44 @@ export default function Main() {
|
|
| 130 |
|
| 131 |
const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
return (
|
| 134 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
<div className="flex flex-col space-y-3 px-2">
|
| 136 |
<div className="flex flex-row items-center space-x-3">
|
| 137 |
<label className="flex">Select a story:</label>
|
| 138 |
<Select
|
| 139 |
defaultValue={gameRef.current}
|
| 140 |
-
onValueChange={(value) => {
|
| 141 |
-
|
| 142 |
-
setRendered({
|
| 143 |
-
assetUrl: "",
|
| 144 |
-
error: "",
|
| 145 |
-
maskBase64: "",
|
| 146 |
-
segments:[]
|
| 147 |
-
})
|
| 148 |
-
}}>
|
| 149 |
-
<SelectTrigger className="w-[180px]">
|
| 150 |
<SelectValue placeholder="Type" />
|
| 151 |
</SelectTrigger>
|
| 152 |
<SelectContent>
|
|
@@ -156,7 +176,7 @@ export default function Main() {
|
|
| 156 |
</SelectContent>
|
| 157 |
</Select>
|
| 158 |
</div>
|
| 159 |
-
<p className="text-xl">
|
| 160 |
<div className="flex flex-row">
|
| 161 |
<div className="text-xl mr-2">🔎 Clickable items:</div>
|
| 162 |
{clickables.map((clickable, i) =>
|
|
@@ -172,6 +192,7 @@ export default function Main() {
|
|
| 172 |
onUserAction={handleUserAction}
|
| 173 |
onUserHover={setHoveredActionnable}
|
| 174 |
isLoading={isLoading}
|
|
|
|
| 175 |
/>
|
| 176 |
<p className="text-xl">{dialogue}</p>
|
| 177 |
</div>
|
|
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
import { useEffect, useRef, useState, useTransition } from "react"
|
| 4 |
+
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
| 5 |
+
|
| 6 |
|
| 7 |
import { ImageRenderer } from "@/components/business/image-renderer"
|
| 8 |
|
|
|
|
| 17 |
import { render } from "./render"
|
| 18 |
|
| 19 |
import { RenderedScene } from "./types"
|
| 20 |
+
import { Game, GameType } from "./games/types"
|
| 21 |
import { defaultGame, games, getGame } from "./games"
|
| 22 |
import { getBackground } from "@/app/queries/getBackground"
|
| 23 |
import { getDialogue } from "@/app/queries/getDialogue"
|
|
|
|
| 31 |
maskBase64: "",
|
| 32 |
segments:[]
|
| 33 |
})
|
| 34 |
+
const router = useRouter()
|
| 35 |
+
const pathname = usePathname()
|
| 36 |
const searchParams = useSearchParams()
|
| 37 |
|
| 38 |
const requestedGame = (searchParams.get('game') as GameType) || defaultGame
|
| 39 |
+
console.log("requestedGame: " + requestedGame)
|
| 40 |
const gameRef = useRef<GameType>(requestedGame)
|
| 41 |
+
console.log("gameRef.current: " + gameRef.current)
|
| 42 |
+
const [game, setGame] = useState<Game>(getGame(gameRef.current))
|
| 43 |
const [situation, setSituation] = useState("")
|
| 44 |
const [scene, setScene] = useState("")
|
| 45 |
const [dialogue, setDialogue] = useState("")
|
|
|
|
| 51 |
setLoading(true)
|
| 52 |
|
| 53 |
await startTransition(async () => {
|
| 54 |
+
console.log("Rendering a scene for " + game.type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
// console.log(`rendering scene..`)
|
| 57 |
const newRendered = await render(
|
|
|
|
| 65 |
).slice(0, 6) // too many can slow us down it seems
|
| 66 |
)
|
| 67 |
|
| 68 |
+
// detect if type game type changed while we were busy
|
| 69 |
+
if (game.type !== gameRef?.current) {
|
| 70 |
+
console.log("game type changed! aborting..")
|
|
|
|
| 71 |
return
|
| 72 |
}
|
| 73 |
|
|
|
|
| 129 |
|
| 130 |
const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
|
| 131 |
|
| 132 |
+
const handleSelectGame = (newGameType: GameType) => {
|
| 133 |
+
gameRef.current = newGameType
|
| 134 |
+
setGame(getGame(newGameType))
|
| 135 |
+
setRendered({
|
| 136 |
+
assetUrl: "",
|
| 137 |
+
error: "",
|
| 138 |
+
maskBase64: "",
|
| 139 |
+
segments:[]
|
| 140 |
+
})
|
| 141 |
+
setLoading(true)
|
| 142 |
+
|
| 143 |
+
const current = new URLSearchParams(Array.from(searchParams.entries()))
|
| 144 |
+
current.set("game", newGameType)
|
| 145 |
+
const search = current.toString()
|
| 146 |
+
const query = search ? `?${search}` : ""
|
| 147 |
+
|
| 148 |
+
// for some reason, this doesn't work?!
|
| 149 |
+
router.replace(`${pathname}${query}`, { })
|
| 150 |
+
|
| 151 |
+
// workaround.. but it is strange that router.replace doesn't work..
|
| 152 |
+
let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + search.toString()
|
| 153 |
+
window.history.pushState({path: newurl}, '', newurl)
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
return (
|
| 157 |
+
<div
|
| 158 |
+
className={[
|
| 159 |
+
"flex flex-col w-full pt-4",
|
| 160 |
+
getGame(gameRef.current).className // apply the game theme
|
| 161 |
+
].join(" ")}
|
| 162 |
+
>
|
| 163 |
<div className="flex flex-col space-y-3 px-2">
|
| 164 |
<div className="flex flex-row items-center space-x-3">
|
| 165 |
<label className="flex">Select a story:</label>
|
| 166 |
<Select
|
| 167 |
defaultValue={gameRef.current}
|
| 168 |
+
onValueChange={(value) => { handleSelectGame(value as GameType) }}>
|
| 169 |
+
<SelectTrigger className="text-xl w-[180px]">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
<SelectValue placeholder="Type" />
|
| 171 |
</SelectTrigger>
|
| 172 |
<SelectContent>
|
|
|
|
| 176 |
</SelectContent>
|
| 177 |
</Select>
|
| 178 |
</div>
|
| 179 |
+
<p className="text-xl">A stable diffusion exploration game. Click on an item to explore a new scene!</p>
|
| 180 |
<div className="flex flex-row">
|
| 181 |
<div className="text-xl mr-2">🔎 Clickable items:</div>
|
| 182 |
{clickables.map((clickable, i) =>
|
|
|
|
| 192 |
onUserAction={handleUserAction}
|
| 193 |
onUserHover={setHoveredActionnable}
|
| 194 |
isLoading={isLoading}
|
| 195 |
+
type={game.type}
|
| 196 |
/>
|
| 197 |
<p className="text-xl">{dialogue}</p>
|
| 198 |
</div>
|
src/app/page.tsx
CHANGED
|
@@ -4,15 +4,22 @@ import Head from "next/head"
|
|
| 4 |
|
| 5 |
import Main from "./main"
|
| 6 |
|
|
|
|
|
|
|
| 7 |
export default async function IndexPage({ params: { ownerId } }: { params: { ownerId: string }}) {
|
| 8 |
return (
|
| 9 |
-
|
| 10 |
<Head>
|
|
|
|
|
|
|
| 11 |
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
| 12 |
</Head>
|
| 13 |
-
<main className=
|
|
|
|
|
|
|
|
|
|
| 14 |
<Main />
|
| 15 |
</main>
|
| 16 |
-
|
| 17 |
)
|
| 18 |
}
|
|
|
|
| 4 |
|
| 5 |
import Main from "./main"
|
| 6 |
|
| 7 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
| 8 |
+
|
| 9 |
export default async function IndexPage({ params: { ownerId } }: { params: { ownerId: string }}) {
|
| 10 |
return (
|
| 11 |
+
<>
|
| 12 |
<Head>
|
| 13 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
| 14 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
| 15 |
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
| 16 |
</Head>
|
| 17 |
+
<main className={
|
| 18 |
+
`dark fixed inset-0 flex flex-col items-center
|
| 19 |
+
bg-stone-900 text-stone-10 overflow-y-scroll
|
| 20 |
+
`}>
|
| 21 |
<Main />
|
| 22 |
</main>
|
| 23 |
+
</>
|
| 24 |
)
|
| 25 |
}
|
src/app/render.ts
CHANGED
|
@@ -1,56 +1,63 @@
|
|
| 1 |
"use server"
|
| 2 |
|
|
|
|
|
|
|
| 3 |
import { RenderedScene } from "./types"
|
| 4 |
|
| 5 |
// note: there is no / at the end in the variable
|
| 6 |
// so we have to add it ourselves if needed
|
| 7 |
const apiUrl = process.env.RENDERING_ENGINE_API
|
| 8 |
|
|
|
|
|
|
|
|
|
|
| 9 |
export async function render(prompt: string, actionnables: string[] = []) {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
// console.log("response:", response)
|
| 51 |
-
return response
|
| 52 |
-
} catch (err) {
|
| 53 |
-
console.error(err)
|
| 54 |
-
return defaulResult
|
| 55 |
-
}
|
| 56 |
-
}
|
|
|
|
| 1 |
"use server"
|
| 2 |
|
| 3 |
+
import Gorgon from "@gorgonjs/gorgon"
|
| 4 |
+
|
| 5 |
import { RenderedScene } from "./types"
|
| 6 |
|
| 7 |
// note: there is no / at the end in the variable
|
| 8 |
// so we have to add it ourselves if needed
|
| 9 |
const apiUrl = process.env.RENDERING_ENGINE_API
|
| 10 |
|
| 11 |
+
|
| 12 |
+
const cacheDurationInSec = 30 * 60 // 30 minutes
|
| 13 |
+
|
| 14 |
export async function render(prompt: string, actionnables: string[] = []) {
|
| 15 |
+
return await Gorgon.get(`render/${JSON.stringify(prompt, actionnables)}`, async () => {
|
| 16 |
+
let defaulResult: RenderedScene = {
|
| 17 |
+
assetUrl: "",
|
| 18 |
+
maskBase64: "",
|
| 19 |
+
error: "",
|
| 20 |
+
segments: []
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
console.log(`calling ${apiUrl}/render with prompt: ${prompt}`)
|
| 25 |
+
const res = await fetch(`${apiUrl}/render`, {
|
| 26 |
+
method: "POST",
|
| 27 |
+
headers: {
|
| 28 |
+
Accept: "application/json",
|
| 29 |
+
"Content-Type": "application/json",
|
| 30 |
+
// Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
| 31 |
+
},
|
| 32 |
+
body: JSON.stringify({
|
| 33 |
+
prompt,
|
| 34 |
+
// nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
|
| 35 |
+
nbFrames: 1, // when nbFrames is 1, we will only generate static images
|
| 36 |
+
nbSteps: 20,
|
| 37 |
+
actionnables,
|
| 38 |
+
segmentation: "firstframe", // one day we will remove this param, to make it automatic
|
| 39 |
+
}),
|
| 40 |
+
cache: 'no-store',
|
| 41 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
| 42 |
+
// next: { revalidate: 1 }
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
// console.log("res:", res)
|
| 46 |
+
// The return value is *not* serialized
|
| 47 |
+
// You can return Date, Map, Set, etc.
|
| 48 |
+
|
| 49 |
+
// Recommendation: handle errors
|
| 50 |
+
if (res.status !== 200) {
|
| 51 |
+
// This will activate the closest `error.js` Error Boundary
|
| 52 |
+
throw new Error('Failed to fetch data')
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const response = (await res.json()) as RenderedScene
|
| 56 |
+
// console.log("response:", response)
|
| 57 |
+
return response
|
| 58 |
+
} catch (err) {
|
| 59 |
+
console.error(err)
|
| 60 |
+
return defaulResult
|
| 61 |
}
|
| 62 |
+
}, cacheDurationInSec * 1000)
|
| 63 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/components/business/image-renderer.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"
|
|
| 2 |
|
| 3 |
import { ImageSegment, RenderedScene } from "@/app/types"
|
| 4 |
import { ProgressBar } from "../misc/progress"
|
|
|
|
| 5 |
|
| 6 |
export const ImageRenderer = ({
|
| 7 |
rendered: {
|
|
@@ -11,12 +12,14 @@ export const ImageRenderer = ({
|
|
| 11 |
},
|
| 12 |
onUserAction,
|
| 13 |
onUserHover,
|
| 14 |
-
isLoading
|
|
|
|
| 15 |
}: {
|
| 16 |
rendered: RenderedScene
|
| 17 |
onUserAction: (actionnable: string) => void
|
| 18 |
onUserHover: (actionnable: string) => void
|
| 19 |
-
isLoading
|
|
|
|
| 20 |
}) => {
|
| 21 |
const timeoutRef = useRef<any>()
|
| 22 |
const imgRef = useRef<HTMLImageElement | null>(null)
|
|
@@ -24,7 +27,8 @@ export const ImageRenderer = ({
|
|
| 24 |
const contextRef = useRef<CanvasRenderingContext2D | null>(null)
|
| 25 |
const [actionnable, setActionnable] = useState<string>("")
|
| 26 |
const [progressPercent, setProcessPercent] = useState(0)
|
| 27 |
-
const
|
|
|
|
| 28 |
|
| 29 |
useEffect(() => {
|
| 30 |
if (maskBase64) {
|
|
@@ -147,49 +151,30 @@ export const ImageRenderer = ({
|
|
| 147 |
}
|
| 148 |
};
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
// note: when everything is fine, it takes about 45 seconds to render a new scene
|
| 155 |
-
|
| 156 |
-
const computeProgress = async () => {
|
| 157 |
-
if (!showLoaderRef.current) {
|
| 158 |
-
console.log("Asked to hide the loader")
|
| 159 |
-
progress = 100
|
| 160 |
-
setProcessPercent(100)
|
| 161 |
-
clearTimeout(timeoutRef.current)
|
| 162 |
-
return
|
| 163 |
-
}
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
setProcessPercent(progress)
|
| 170 |
-
|
| 171 |
-
timeoutRef.current = setTimeout(() => {
|
| 172 |
-
computeProgress()
|
| 173 |
-
}, 1000)
|
| 174 |
-
}
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
-
|
| 180 |
useEffect(() => {
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
if (!assetUrl) {
|
| 186 |
-
return <div className="flex w-full pt-8 items-center justify-center text-center">
|
| 187 |
-
<ProgressBar
|
| 188 |
-
text="⌛"
|
| 189 |
-
progressPercentage={progressPercent}
|
| 190 |
-
/>
|
| 191 |
-
</div>
|
| 192 |
-
}
|
| 193 |
|
| 194 |
/*
|
| 195 |
<img
|
|
@@ -226,26 +211,37 @@ export const ImageRenderer = ({
|
|
| 226 |
return (
|
| 227 |
<div className={[
|
| 228 |
"w-full py-8 px-2",
|
| 229 |
-
isLoading ? "animate-pulse" : ""
|
| 230 |
].join(" ")
|
| 231 |
}>
|
| 232 |
<div className="relative w-full">
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
/>
|
| 248 |
</div>
|
|
|
|
| 249 |
</div>
|
| 250 |
)
|
| 251 |
}
|
|
|
|
| 2 |
|
| 3 |
import { ImageSegment, RenderedScene } from "@/app/types"
|
| 4 |
import { ProgressBar } from "../misc/progress"
|
| 5 |
+
import { GameType } from "@/app/games/types"
|
| 6 |
|
| 7 |
export const ImageRenderer = ({
|
| 8 |
rendered: {
|
|
|
|
| 12 |
},
|
| 13 |
onUserAction,
|
| 14 |
onUserHover,
|
| 15 |
+
isLoading,
|
| 16 |
+
type,
|
| 17 |
}: {
|
| 18 |
rendered: RenderedScene
|
| 19 |
onUserAction: (actionnable: string) => void
|
| 20 |
onUserHover: (actionnable: string) => void
|
| 21 |
+
isLoading: boolean
|
| 22 |
+
type: GameType
|
| 23 |
}) => {
|
| 24 |
const timeoutRef = useRef<any>()
|
| 25 |
const imgRef = useRef<HTMLImageElement | null>(null)
|
|
|
|
| 27 |
const contextRef = useRef<CanvasRenderingContext2D | null>(null)
|
| 28 |
const [actionnable, setActionnable] = useState<string>("")
|
| 29 |
const [progressPercent, setProcessPercent] = useState(0)
|
| 30 |
+
const progressRef = useRef(0)
|
| 31 |
+
const isLoadingRef = useRef(isLoading)
|
| 32 |
|
| 33 |
useEffect(() => {
|
| 34 |
if (maskBase64) {
|
|
|
|
| 151 |
}
|
| 152 |
};
|
| 153 |
|
| 154 |
+
const updateProgressBar = () => {
|
| 155 |
+
const duration = 1000 // 1 sec
|
| 156 |
+
const frequency = 200 // 200ms
|
| 157 |
+
const nbUpdatesPerSec = duration / frequency // 5x per second
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
// normally it takes 45, and we will try to go below,
|
| 160 |
+
// but to be safe let's set the counter a 1 min
|
| 161 |
+
const nbSeconds = 45 // 1 min
|
| 162 |
+
const amountInPercent = 100 / (nbUpdatesPerSec * nbSeconds) // 0.333
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
+
progressRef.current = Math.min(100, progressRef.current + amountInPercent)
|
| 165 |
+
setProcessPercent(progressRef.current)
|
| 166 |
+
}
|
| 167 |
|
|
|
|
| 168 |
useEffect(() => {
|
| 169 |
+
clearInterval(timeoutRef.current)
|
| 170 |
+
isLoadingRef.current = isLoading
|
| 171 |
+
progressRef.current = 0
|
| 172 |
+
setProcessPercent(0)
|
| 173 |
+
if (isLoading) {
|
| 174 |
+
timeoutRef.current = setInterval(updateProgressBar, 200)
|
| 175 |
+
}
|
| 176 |
+
}, [isLoading, assetUrl, type])
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
/*
|
| 180 |
<img
|
|
|
|
| 211 |
return (
|
| 212 |
<div className={[
|
| 213 |
"w-full py-8 px-2",
|
| 214 |
+
// isLoading ? "animate-pulse" : ""
|
| 215 |
].join(" ")
|
| 216 |
}>
|
| 217 |
<div className="relative w-full">
|
| 218 |
+
{assetUrl
|
| 219 |
+
? <img
|
| 220 |
+
src={assetUrl}
|
| 221 |
+
// src={"data:image/png;base64," + maskBase64}
|
| 222 |
+
ref={imgRef}
|
| 223 |
+
width="1024px"
|
| 224 |
+
height="512px"
|
| 225 |
+
className={
|
| 226 |
+
[
|
| 227 |
+
// "absolute top-0 left-0",
|
| 228 |
+
actionnable && !isLoading ? "cursor-pointer" : ""
|
| 229 |
+
].join(" ")
|
| 230 |
+
}
|
| 231 |
+
onMouseDown={(event) => handleMouseEvent(event, true)}
|
| 232 |
+
onMouseMove={handleMouseEvent}
|
| 233 |
+
/>
|
| 234 |
+
: null}
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
{isLoading
|
| 238 |
+
? <div className="fixed flex w-20 h-20 bottom-8 right-0 mr-8">
|
| 239 |
+
<ProgressBar
|
| 240 |
+
text="⌛"
|
| 241 |
+
progressPercentage={progressPercent}
|
| 242 |
/>
|
| 243 |
</div>
|
| 244 |
+
: null}
|
| 245 |
</div>
|
| 246 |
)
|
| 247 |
}
|
src/components/misc/progress.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
-
import { CircularProgressbar } from "react-circular-progressbar"
|
| 4 |
import "react-circular-progressbar/dist/styles.css"
|
| 5 |
|
| 6 |
export function ProgressBar ({
|
|
@@ -23,20 +23,20 @@ export function ProgressBar ({
|
|
| 23 |
text={text || ""}
|
| 24 |
|
| 25 |
// Width of circular line relative to total width of component, a value from 0-100. Default: 8.
|
| 26 |
-
strokeWidth={
|
| 27 |
|
| 28 |
-
|
| 29 |
// As a convenience, you can use buildStyles to configure the most common style changes:
|
| 30 |
|
| 31 |
styles={buildStyles({
|
| 32 |
// Rotation of path and trail, in number of turns (0-1)
|
| 33 |
-
rotation: 0
|
| 34 |
|
| 35 |
// Whether to use rounded or flat corners on the ends - can use 'butt' or 'round'
|
| 36 |
-
strokeLinecap: '
|
| 37 |
|
| 38 |
// Text size
|
| 39 |
-
textSize: '
|
| 40 |
|
| 41 |
// How long animation takes to go from one percentage to another, in seconds
|
| 42 |
pathTransitionDuration: 0.5,
|
|
@@ -45,12 +45,12 @@ export function ProgressBar ({
|
|
| 45 |
// pathTransition: 'none',
|
| 46 |
|
| 47 |
// Colors
|
| 48 |
-
pathColor: `rgba(62, 152, 199, ${percentage / 100})`,
|
| 49 |
textColor: '#f88',
|
| 50 |
trailColor: '#d6d6d6',
|
| 51 |
backgroundColor: '#3e98c7',
|
| 52 |
})}
|
| 53 |
-
|
| 54 |
/>
|
| 55 |
</div>
|
| 56 |
)
|
|
|
|
| 1 |
"use client"
|
| 2 |
|
| 3 |
+
import { CircularProgressbar, buildStyles } from "react-circular-progressbar"
|
| 4 |
import "react-circular-progressbar/dist/styles.css"
|
| 5 |
|
| 6 |
export function ProgressBar ({
|
|
|
|
| 23 |
text={text || ""}
|
| 24 |
|
| 25 |
// Width of circular line relative to total width of component, a value from 0-100. Default: 8.
|
| 26 |
+
strokeWidth={12}
|
| 27 |
|
| 28 |
+
|
| 29 |
// As a convenience, you can use buildStyles to configure the most common style changes:
|
| 30 |
|
| 31 |
styles={buildStyles({
|
| 32 |
// Rotation of path and trail, in number of turns (0-1)
|
| 33 |
+
rotation: 0,
|
| 34 |
|
| 35 |
// Whether to use rounded or flat corners on the ends - can use 'butt' or 'round'
|
| 36 |
+
strokeLinecap: 'round',
|
| 37 |
|
| 38 |
// Text size
|
| 39 |
+
textSize: '40px',
|
| 40 |
|
| 41 |
// How long animation takes to go from one percentage to another, in seconds
|
| 42 |
pathTransitionDuration: 0.5,
|
|
|
|
| 45 |
// pathTransition: 'none',
|
| 46 |
|
| 47 |
// Colors
|
| 48 |
+
// pathColor: `rgba(62, 152, 199, ${percentage / 100})`,
|
| 49 |
textColor: '#f88',
|
| 50 |
trailColor: '#d6d6d6',
|
| 51 |
backgroundColor: '#3e98c7',
|
| 52 |
})}
|
| 53 |
+
|
| 54 |
/>
|
| 55 |
</div>
|
| 56 |
)
|
src/fonts/Lugrasimo-Regular.woff2
ADDED
|
Binary file (19.1 kB). View file
|
|
|
src/lib/fonts.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Inter, Edu_SA_Beginner, Orbitron, Amatic_SC } from "next/font/google"
|
| 2 |
+
import localFont from "next/font/local"
|
| 3 |
+
|
| 4 |
+
export const inter = Inter({
|
| 5 |
+
subsets: ["latin"],
|
| 6 |
+
variable: "--font-inter",
|
| 7 |
+
})
|
| 8 |
+
|
| 9 |
+
export const edu = Edu_SA_Beginner({
|
| 10 |
+
subsets: ["latin"],
|
| 11 |
+
variable: "--font-edu",
|
| 12 |
+
})
|
| 13 |
+
|
| 14 |
+
export const orbitron = Orbitron({
|
| 15 |
+
subsets: ["latin"],
|
| 16 |
+
variable: "--font-orbitron",
|
| 17 |
+
})
|
| 18 |
+
|
| 19 |
+
export const amatic = Amatic_SC({
|
| 20 |
+
subsets: ["latin"],
|
| 21 |
+
weight: "400",
|
| 22 |
+
variable: "--font-amatic"
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
export const lugrasimo = localFont({
|
| 26 |
+
src: "../fonts/Lugrasimo-Regular.woff2",
|
| 27 |
+
variable: "--font-lugrasimo"
|
| 28 |
+
})
|
| 29 |
+
|
| 30 |
+
// https://fonts.google.com/specimen/Amatic+SC
|
| 31 |
+
// https://fonts.google.com/specimen/Orbitron
|
| 32 |
+
// https://fonts.google.com/specimen/Edu+SA+Beginner
|
| 33 |
+
// https://fonts.google.com/specimen/Tektur
|
| 34 |
+
|
| 35 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
| 36 |
+
// If loading a variable font, you don"t need to specify the font weight
|
| 37 |
+
export const fontList = [
|
| 38 |
+
inter,
|
| 39 |
+
edu,
|
| 40 |
+
orbitron,
|
| 41 |
+
amatic,
|
| 42 |
+
lugrasimo,
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
export const classNames = fontList.map(font => font.className)
|
| 46 |
+
|
| 47 |
+
export const className = classNames.join(" ")
|
| 48 |
+
|
| 49 |
+
export type FontName = "font-inter" | "font-sans" | "font-edu" | "font-orbitron" | "font-amatic" | "font-lugrasimo"
|
tailwind.config.js
CHANGED
|
@@ -6,6 +6,7 @@ module.exports = {
|
|
| 6 |
'./components/**/*.{ts,tsx}',
|
| 7 |
'./app/**/*.{ts,tsx}',
|
| 8 |
'./src/**/*.{ts,tsx}',
|
|
|
|
| 9 |
],
|
| 10 |
theme: {
|
| 11 |
container: {
|
|
@@ -16,6 +17,13 @@ module.exports = {
|
|
| 16 |
},
|
| 17 |
},
|
| 18 |
extend: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
keyframes: {
|
| 20 |
"accordion-down": {
|
| 21 |
from: { height: 0 },
|
|
|
|
| 6 |
'./components/**/*.{ts,tsx}',
|
| 7 |
'./app/**/*.{ts,tsx}',
|
| 8 |
'./src/**/*.{ts,tsx}',
|
| 9 |
+
'./src/lib/fonts.ts'
|
| 10 |
],
|
| 11 |
theme: {
|
| 12 |
container: {
|
|
|
|
| 17 |
},
|
| 18 |
},
|
| 19 |
extend: {
|
| 20 |
+
fontFamily: {
|
| 21 |
+
sans: ['var(--font-inter)'],
|
| 22 |
+
edu: ['var(--font-edu)'],
|
| 23 |
+
orbitron: ['var(--font-orbitron)'],
|
| 24 |
+
amatic: ['var(--font-amatic)'],
|
| 25 |
+
lugrasimo: ['var(--font-lugrasimo)'],
|
| 26 |
+
},
|
| 27 |
keyframes: {
|
| 28 |
"accordion-down": {
|
| 29 |
from: { height: 0 },
|