jbilcke-hf HF staff commited on
Commit
ab75c71
·
1 Parent(s): 78a9e11

changing the URL again as the public server is being abused by some users

Browse files
.env CHANGED
@@ -1,3 +1,3 @@
1
  NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-fishtank.hf.space
2
  # NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
3
- RENDERING_ENGINE_API=https://jbilcke-hf-videochain-api.hf.space
 
1
  NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-fishtank.hf.space
2
  # NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
3
+ RENDERING_ENGINE_API=https://jbilcke-hf-videochain.hf.space
src/app/main.tsx CHANGED
@@ -2,7 +2,8 @@
2
 
3
  import { ReactNode, useEffect, useRef, useState, useTransition } from "react"
4
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
5
-
 
6
 
7
  import { SceneRenderer } from "@/components/renderer"
8
 
@@ -17,7 +18,7 @@ import { Switch } from "@/components/ui/switch"
17
  import { Label } from "@/components/ui/label"
18
 
19
  import { newRender, getRender } from "./render"
20
- import { InventoryEvent, InventoryItem, RenderedScene, SceneEvent } from "./types"
21
  import { Game, GameType } from "./games/types"
22
  import { defaultGame, games, getGame } from "./games"
23
  import { getBackground } from "@/app/queries/getBackground"
@@ -26,6 +27,8 @@ import { getActionnables } from "@/app/queries/getActionnables"
26
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
27
  import { normalizeActionnables } from "@/lib/normalizeActionnables"
28
  import { Inventory } from "@/components/inventory"
 
 
29
 
30
  const getInitialRenderedScene = (): RenderedScene => ({
31
  renderId: "",
@@ -57,6 +60,12 @@ export default function Main() {
57
 
58
  const [dialogue, setDialogue] = useState("")
59
 
 
 
 
 
 
 
60
  const [isBusy, setBusy] = useState<boolean>(true)
61
  const busyRef = useRef(true)
62
 
@@ -88,6 +97,8 @@ export default function Main() {
88
  console.log("got the first version of our scene!", newRendered)
89
 
90
  // detect if type game type changed while we were busy
 
 
91
  if (game?.type !== gameRef?.current) {
92
  console.log("game type changed! aborting..")
93
  return
@@ -160,35 +171,54 @@ export default function Main() {
160
  checkRenderedLoop()
161
  }, [])
162
 
163
- const handleClickOnActionnable = async (actionnable: string = "", userAction: string = "") => {
164
 
165
- setBusy(busyRef.current = true)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- // TODO: ask Llama2 what to do about it
168
- // we need a frame and some actionnables,
169
- // perhaps even some music or sound effects
170
 
171
  await startTransition(async () => {
172
 
173
- const game = getGame(gameRef.current)
 
 
 
 
174
 
175
- let newDialogue = ""
176
  try {
177
- newDialogue = await getDialogue({ game, situation, userAction })
178
- // console.log(`newDialogue:`, newDialogue)
179
- setDialogue(newDialogue)
180
  } catch (err) {
181
- console.log(`failed to generate dialogue (but it's only a nice to have, so..)`)
182
- setDialogue("")
183
  }
184
 
185
  try {
186
- const newActionnables = await getActionnables({ game, situation, userAction })
187
- console.log(`newActionnables:`, newActionnables)
188
 
189
  // todo rename this background/situation mess
190
  // it should be only one word
191
- const newBackground = await getBackground({ game, situation, userAction, newActionnables })
192
  console.log(`newBackground:`, newBackground)
193
  setSituation(newBackground)
194
 
@@ -197,11 +227,12 @@ export default function Main() {
197
 
198
  // todo we could also use useEffect
199
  } catch (err) {
200
- console.error(`failed to get one of the mandatory entites: ${err}`)
201
  }
202
  })
203
  }
204
 
 
205
  const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
206
 
207
  // console.log("segments:", rendered.segments)
@@ -273,15 +304,28 @@ export default function Main() {
273
  }
274
 
275
  const handleSceneEvent = (event: SceneEvent, actionnable?: string) => {
 
 
 
276
  const actionnableName = actionnable || "nothing"
277
  let newEvent = null
278
  let newEventString = ""
279
  if (event === "HoveringNothing") {
280
- newEvent = <>🔎 You are looking at: <span className="font-bold">Nothing</span></>
281
- newEventString = `User is looking at nothing.`
 
 
 
 
 
282
  } else if (event === "HoveringActionnable") {
283
- newEvent = <>🔎 You are looking at: <span className="font-bold">{actionnableName}</span></>
284
- newEventString = `User is looking at "${actionnableName}"`
 
 
 
 
 
285
  } else if (event === "ClickOnNothing") {
286
  newEvent = <>🔎 There is nothing here.</>
287
  newEventString = `User clicked on nothing.`
@@ -297,43 +341,39 @@ export default function Main() {
297
  }
298
 
299
  if (event === "ClickOnActionnable" || event === "ClickOnNothing") {
300
- handleClickOnActionnable(actionnable, newEventString)
 
301
  }
302
  }
303
 
304
- const askGameMasterForSomeDialogue = async (userAction: string) => {
305
- await startTransition(async () => {
306
- const game = getGame(gameRef.current)
307
- let newDialogue = ""
308
- try {
309
- newDialogue = await getDialogue({ game, situation, userAction })
310
- } catch (err) {
311
- console.log(`failed to generate dialoguee, let's try again maybe`)
312
- try {
313
- newDialogue = await getDialogue({ game, situation, userAction: `${userAction}.` })
314
- } catch (err) {
315
- console.log(`failed to generate dialogue.. again (but it's only a nice to have, so..)`)
316
- setDialogue("")
317
- return
318
- }
319
- }
320
-
321
- // try to remove whatever hallucination might come up next
322
- newDialogue = newDialogue.split("As the player")[0]
323
- newDialogue = newDialogue.split("As they use")[0]
324
- setDialogue(newDialogue)
325
- })
326
- }
327
-
328
- const handleInventoryEvent = (event: InventoryEvent, item: InventoryItem, target?: InventoryItem) => {
329
  let newEvent = null
330
  let newEventString = ""
331
  if (newEvent === "Grabbing") {
332
  newEventString = `Player just grabbed "${item.name}".`
333
- newEvent = <>You just grabbed <span className="font-bold">{item.name}</span></>
 
 
 
 
 
 
 
 
 
 
 
334
  } else if (event === "DroppedOnAnotherItem") {
335
- newEventString = `Player is trying to use those object from their own inventory: "${item.name}" with "${target?.name}". What do you think could the combination of ${item.name} and ${target?.name} lead to? Invent a funny outcome!`
336
- newEvent = <>You tried to combine <span className="font-bold">&quot;{item.name}&quot;</span> with <span className="font-bold">&quot;{target?.name}&quot;</span></>
 
 
 
 
 
 
 
 
337
  } else if (event === "ClickOnItem") {
338
  newEventString = `Player is inspecting "${item.name}" from their inventory, which has the following description: "${item.description}". Can you invent a funny back story?`
339
  newEvent = <>🔎 You are inspecting <span className="font-bold">&quot;{item.name}&quot;.</span> {item.description}</>
@@ -345,11 +385,16 @@ export default function Main() {
345
  setLastEvent(newEvent)
346
  }
347
 
348
- if (event === "DroppedOnAnotherItem" || event === "ClickOnItem") {
349
- askGameMasterForSomeDialogue(newEventString)
 
 
 
 
350
  }
351
  }
352
 
 
353
  // determine when to show the spinner
354
  const isLoading = isBusy || rendered.status === "pending"
355
 
@@ -401,41 +446,44 @@ export default function Main() {
401
  </div>
402
  </div>
403
 
404
- <div className={[
405
- "flex flex-col w-full pt-4 space-y-3 text-gray-50 dark:text-gray-50",
406
- getGame(gameRef.current).className // apply the game theme
407
- ].join(" ")}>
408
- <div className="flex flex-row">
409
- <div className="text-xl px-2">{lastEvent}</div>
410
- </div>
411
- <Inventory game={game} onEvent={handleInventoryEvent} />
412
- <div className="flex flex-row">
413
- <div className="text-xl mr-2">
414
- {rendered.segments.length
415
- ? <span>💡 Try to click on:</span>
416
- : <span>⌛ Generating clickable areas..</span>
417
- }
 
 
 
 
 
 
 
 
418
  </div>
419
- {clickables.map((clickable, i) =>
420
- <div key={i} className="flex flex-row text-xl mr-2">
421
- <div className="">{clickable}</div>
422
- {i < (clickables.length - 1) ? <div>,</div> : null}
423
- </div>)}
 
 
 
 
 
 
 
 
424
  </div>
425
- <SceneRenderer
426
- rendered={rendered}
427
- onEvent={handleSceneEvent}
428
- isLoading={isLoading}
429
- game={game}
430
- engine={engine}
431
- debug={debug}
432
- />
433
- <div
434
- className="text-xl rounded-xl backdrop-blur-sm bg-white/10 p-4"
435
- style={{
436
- textShadow: "1px 0px 2px #000000ab"
437
- }}>{dialogue}</div>
438
- </div>
439
  </div>
440
  )
441
  }
 
2
 
3
  import { ReactNode, useEffect, useRef, useState, useTransition } from "react"
4
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
5
+ import { DndProvider } from "react-dnd"
6
+ import { HTML5Backend } from "react-dnd-html5-backend"
7
 
8
  import { SceneRenderer } from "@/components/renderer"
9
 
 
18
  import { Label } from "@/components/ui/label"
19
 
20
  import { newRender, getRender } from "./render"
21
+ import { InventoryEvent, InventoryItem, OnInventoryEvent, RenderedScene, SceneEvent } from "./types"
22
  import { Game, GameType } from "./games/types"
23
  import { defaultGame, games, getGame } from "./games"
24
  import { getBackground } from "@/app/queries/getBackground"
 
27
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
28
  import { normalizeActionnables } from "@/lib/normalizeActionnables"
29
  import { Inventory } from "@/components/inventory"
30
+ import { store } from "./store"
31
+ import { defaultActionnables } from "@/lib/defaultActionnables"
32
 
33
  const getInitialRenderedScene = (): RenderedScene => ({
34
  renderId: "",
 
60
 
61
  const [dialogue, setDialogue] = useState("")
62
 
63
+ /*
64
+ const [grabbedItem, setGrabbedItem] = useState<InventoryItem>()
65
+ const grabbedItemRef = useRef<InventoryItem | undefined>()
66
+ grabbedItemRef.current = grabbedItem
67
+ */
68
+
69
  const [isBusy, setBusy] = useState<boolean>(true)
70
  const busyRef = useRef(true)
71
 
 
97
  console.log("got the first version of our scene!", newRendered)
98
 
99
  // detect if type game type changed while we were busy
100
+ // note that currently we reload the whol page when tha happens,
101
+ // so this code isn't that useful
102
  if (game?.type !== gameRef?.current) {
103
  console.log("game type changed! aborting..")
104
  return
 
171
  checkRenderedLoop()
172
  }, [])
173
 
 
174
 
175
+ const askGameMasterForEpicDialogue = async (lastEvent: string) => {
176
+
177
+ await startTransition(async () => {
178
+ // const game = getGame(gameRef.current)
179
+ let newDialogue = ""
180
+ try {
181
+ newDialogue = await getDialogue({ game, situation, lastEvent })
182
+ } catch (err) {
183
+ console.log(`failed to generate dialogue, let's try again maybe`)
184
+ try {
185
+ newDialogue = await getDialogue({ game, situation, lastEvent: `${lastEvent}.` })
186
+ } catch (err) {
187
+ console.log(`failed to generate dialogue.. again (but it's only a nice to have, so..)`)
188
+ setDialogue("")
189
+ return
190
+ }
191
+ }
192
+
193
+ // try to remove whatever hallucination might come up next
194
+ newDialogue = newDialogue.split("As the player")[0]
195
+ newDialogue = newDialogue.split("As they use")[0]
196
+ setDialogue(newDialogue)
197
+ })
198
+ }
199
 
200
+ const askGameMasterForEpicBackground = async (lastEvent: string = "") => {
 
 
201
 
202
  await startTransition(async () => {
203
 
204
+ setBusy(busyRef.current = true) // this will be set to false once the scene finish loading
205
+
206
+ // const game = getGame(gameRef.current)
207
+
208
+ let newActionnables = [...defaultActionnables]
209
 
 
210
  try {
211
+ newActionnables = await getActionnables({ game, situation, lastEvent })
212
+ console.log(`newActionnables:`, newActionnables)
 
213
  } catch (err) {
214
+ console.log(`failed to generate actionnables, using default value`)
 
215
  }
216
 
217
  try {
 
 
218
 
219
  // todo rename this background/situation mess
220
  // it should be only one word
221
+ const newBackground = await getBackground({ game, situation, lastEvent, newActionnables })
222
  console.log(`newBackground:`, newBackground)
223
  setSituation(newBackground)
224
 
 
227
 
228
  // todo we could also use useEffect
229
  } catch (err) {
230
+ console.error(`failed to get the scene: ${err}`)
231
  }
232
  })
233
  }
234
 
235
+
236
  const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
237
 
238
  // console.log("segments:", rendered.segments)
 
304
  }
305
 
306
  const handleSceneEvent = (event: SceneEvent, actionnable?: string) => {
307
+ // TODO use Zustand
308
+ const item = store.currentlyDraggedItem
309
+
310
  const actionnableName = actionnable || "nothing"
311
  let newEvent = null
312
  let newEventString = ""
313
  if (event === "HoveringNothing") {
314
+ if (item) {
315
+ newEvent = <>🔎 You are holding <span className="font-bold">&quot;{item.name}&quot;</span> and looking around, wondering how to use it.</>
316
+ newEventString = `User is holding "${item.name}" from their inventory and wonder how they can use it.`
317
+ } else {
318
+ newEvent = <>🔎 You are looking at the scene, looking for clues.</>
319
+ newEventString = `User is looking at the scene, looking for clues.`
320
+ }
321
  } else if (event === "HoveringActionnable") {
322
+ if (item) {
323
+ newEvent = <>🔎 You are holding <span className="font-bold">&quot;{item.name}&quot;</span>, waving it over <span className="font-bold">&quot;{actionnableName}&quot;</span></>
324
+ newEventString = `User is holding "${item.name}" from their inventory and wonder if they can use it on "${actionnableName}"`
325
+ } else {
326
+ newEvent = <>🔎 You are looking at <span className="font-bold">&quot;{actionnableName}&quot;</span></>
327
+ newEventString = `User is looking at "${actionnableName}"`
328
+ }
329
  } else if (event === "ClickOnNothing") {
330
  newEvent = <>🔎 There is nothing here.</>
331
  newEventString = `User clicked on nothing.`
 
341
  }
342
 
343
  if (event === "ClickOnActionnable" || event === "ClickOnNothing") {
344
+ askGameMasterForEpicDialogue(newEventString)
345
+ askGameMasterForEpicBackground(newEventString)
346
  }
347
  }
348
 
349
+ const handleInventoryEvent: OnInventoryEvent = async (event, item, target) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  let newEvent = null
351
  let newEventString = ""
352
  if (newEvent === "Grabbing") {
353
  newEventString = `Player just grabbed "${item.name}".`
354
+ newEvent = <>You just grabbed <span className="font-bold">&quot;{item.name}&quot;</span></>
355
+ } else if (event === "DroppedOnActionnable") {
356
+ newEventString = [
357
+ `Player is trying to use`,
358
+ `"${item.name}"`,
359
+ item.description ? `(described as: "${item.description}")` : "",
360
+ `from their inventory on "${target?.title}"`,
361
+ target?.description ? `(described as: "${target?.description}")` : "",
362
+ `within the scene.`,
363
+ `What do you think should be the outcome?`
364
+ ].filter(i => i).join(" ")
365
+ newEvent = <>You try to use <span className="font-bold">&quot;{item.name}&quot;</span> on <span className="font-bold">&quot;{target?.title}&quot;</span></>
366
  } else if (event === "DroppedOnAnotherItem") {
367
+ newEventString = [
368
+ `Player is trying to use`,
369
+ `"${item.name}"`,
370
+ item.description ? `(described as: "${item.description}")` : "",
371
+ `on "${target?.name}"`,
372
+ target?.description ? `(described as: "${target?.description}")` : "",
373
+ `What do you think could the use or combination of ${item.name} and ${target?.name} lead to?`,
374
+ `Invent a funny outcome!`
375
+ ].filter(i => i).join(" ")
376
+ newEvent = <>You try to combine <span className="font-bold">&quot;{item.name}&quot;</span> with <span className="font-bold">&quot;{target?.title}&quot;</span></>
377
  } else if (event === "ClickOnItem") {
378
  newEventString = `Player is inspecting "${item.name}" from their inventory, which has the following description: "${item.description}". Can you invent a funny back story?`
379
  newEvent = <>🔎 You are inspecting <span className="font-bold">&quot;{item.name}&quot;.</span> {item.description}</>
 
385
  setLastEvent(newEvent)
386
  }
387
 
388
+ if (event === "DroppedOnAnotherItem" || event === "ClickOnItem" || event === "DroppedOnActionnable") {
389
+ askGameMasterForEpicDialogue(newEventString)
390
+ }
391
+
392
+ if (event === "DroppedOnActionnable") {
393
+ askGameMasterForEpicBackground(newEventString)
394
  }
395
  }
396
 
397
+
398
  // determine when to show the spinner
399
  const isLoading = isBusy || rendered.status === "pending"
400
 
 
446
  </div>
447
  </div>
448
 
449
+ <DndProvider backend={HTML5Backend}>
450
+ <div className={[
451
+ "flex flex-col w-full pt-4 space-y-3 text-gray-50 dark:text-gray-50",
452
+ getGame(gameRef.current).className // apply the game theme
453
+ ].join(" ")}
454
+ >
455
+ <div className="flex flex-row">
456
+ <div className="text-xl px-2">{lastEvent}</div>
457
+ </div>
458
+ <Inventory game={game} onEvent={handleInventoryEvent} />
459
+ <div className="flex flex-row">
460
+ <div className="text-xl mr-2">
461
+ {rendered.segments.length
462
+ ? <span>💡 Try to click on:</span>
463
+ : <span>⌛ Generating areas for clicks and drag & drop, please wait..</span>
464
+ }
465
+ </div>
466
+ {clickables.map((clickable, i) =>
467
+ <div key={i} className="flex flex-row text-xl mr-2">
468
+ <div className="">{clickable}</div>
469
+ {i < (clickables.length - 1) ? <div>,</div> : null}
470
+ </div>)}
471
  </div>
472
+ <SceneRenderer
473
+ rendered={rendered}
474
+ onEvent={handleSceneEvent}
475
+ isLoading={isLoading}
476
+ game={game}
477
+ engine={engine}
478
+ debug={debug}
479
+ />
480
+ <div
481
+ className="text-xl rounded-xl backdrop-blur-sm bg-white/10 p-4"
482
+ style={{
483
+ textShadow: "1px 0px 2px #000000ab"
484
+ }}>{dialogue}</div>
485
  </div>
486
+ </DndProvider>
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  </div>
488
  )
489
  }
src/app/queries/getActionnables.ts CHANGED
@@ -9,14 +9,14 @@ import { normalizeActionnables } from "@/lib/normalizeActionnables"
9
  export const getActionnables = async ({
10
  game,
11
  situation = "",
12
- userAction = "",
13
  }: {
14
  game: Game;
15
  situation: string;
16
- userAction: string;
17
  }) => {
18
 
19
- const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, userAction })
20
 
21
  const basePrompt = initialPrompt !== currentPrompt
22
  ? `Here is some context information about the initial scene: ${initialPrompt}`
 
9
  export const getActionnables = async ({
10
  game,
11
  situation = "",
12
+ lastEvent = "",
13
  }: {
14
  game: Game;
15
  situation: string;
16
+ lastEvent: string;
17
  }) => {
18
 
19
+ const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
20
 
21
  const basePrompt = initialPrompt !== currentPrompt
22
  ? `Here is some context information about the initial scene: ${initialPrompt}`
src/app/queries/getBackground.ts CHANGED
@@ -7,12 +7,12 @@ import { predict } from "./predict"
7
  export const getBackground = async ({
8
  game,
9
  situation = "",
10
- userAction = "",
11
  newActionnables = [],
12
  }: {
13
  game: Game;
14
  situation: string;
15
- userAction: string;
16
  newActionnables: string[],
17
  }) => {
18
 
@@ -23,7 +23,7 @@ export const getBackground = async ({
23
  } = getBase({
24
  game,
25
  situation,
26
- userAction
27
  })
28
 
29
  const basePrompt = initialPrompt !== currentPrompt
@@ -38,9 +38,9 @@ Here is the original scene in which the user was located at first, which will in
38
  `You are the AI game master of a role video game.`,
39
  basePrompt,
40
  `You are going to receive new information about the current whereabouts of the player.`,
41
- `Please write a caption for the next plausible scene to display in intricate details: the environment, lights, era, characters, objects, textures, light etc.`,
42
  `You MUST include the following important objects that the user can click on: ${newActionnables}.`,
43
- `Be straight to the point, and do not say things like "As the player clicks on.." or "the scene shifts to" (the best is not not mention the player at all)`
44
  ].filter(item => item).join("\n")
45
  },
46
  {
 
7
  export const getBackground = async ({
8
  game,
9
  situation = "",
10
+ lastEvent = "",
11
  newActionnables = [],
12
  }: {
13
  game: Game;
14
  situation: string;
15
+ lastEvent: string;
16
  newActionnables: string[],
17
  }) => {
18
 
 
23
  } = getBase({
24
  game,
25
  situation,
26
+ lastEvent
27
  })
28
 
29
  const basePrompt = initialPrompt !== currentPrompt
 
38
  `You are the AI game master of a role video game.`,
39
  basePrompt,
40
  `You are going to receive new information about the current whereabouts of the player.`,
41
+ `Please write a photo caption for the next plausible scene to display in intricate details: the environment, lights, era, characters, objects, textures, light etc.`,
42
  `You MUST include the following important objects that the user can click on: ${newActionnables}.`,
43
+ `As this is a caption be synthetic: describe things, but don't comment on them. Be straight to the point, and do not say things like "As the player clicks on.." or "the scene shifts to" (the best is not not mention the player at all)`
44
  ].filter(item => item).join("\n")
45
  },
46
  {
src/app/queries/getBase.ts CHANGED
@@ -3,11 +3,11 @@ import { Game } from "@/app/games/types"
3
  export const getBase = ({
4
  game,
5
  situation = "",
6
- userAction = "",
7
  }: {
8
  game: Game;
9
  situation: string;
10
- userAction: string;
11
  }) => {
12
  const initialPrompt = [...game.getScenePrompt()].join(", ")
13
 
@@ -17,7 +17,7 @@ export const getBase = ({
17
 
18
  const userSituationPrompt = [
19
  `Player is currently in "${currentPrompt}".`,
20
- userAction
21
  ].join(" ")
22
 
23
  return { initialPrompt, currentPrompt, userSituationPrompt }
 
3
  export const getBase = ({
4
  game,
5
  situation = "",
6
+ lastEvent = "",
7
  }: {
8
  game: Game;
9
  situation: string;
10
+ lastEvent: string;
11
  }) => {
12
  const initialPrompt = [...game.getScenePrompt()].join(", ")
13
 
 
17
 
18
  const userSituationPrompt = [
19
  `Player is currently in "${currentPrompt}".`,
20
+ lastEvent
21
  ].join(" ")
22
 
23
  return { initialPrompt, currentPrompt, userSituationPrompt }
src/app/queries/getDialogue.ts CHANGED
@@ -8,17 +8,17 @@ import { predict } from "./predict"
8
  export const getDialogue = async ({
9
  game,
10
  situation = "",
11
- userAction = "",
12
  }: {
13
  game: Game;
14
  situation: string;
15
- userAction: string;
16
  }) => {
17
 
18
- const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, userAction })
19
 
20
  console.log("DEBUG", {
21
- game, situation, userAction,
22
  currentPrompt,
23
  initialPrompt,
24
  userSituationPrompt,
@@ -31,7 +31,7 @@ export const getDialogue = async ({
31
  */
32
 
33
  const basePrompt = initialPrompt !== currentPrompt
34
- ? ` You must imagine the most plausible next dialogue line from the game master, based on where the player was located before and is now, and also what the player did before and are doing now.
35
  Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
36
  : ""
37
 
 
8
  export const getDialogue = async ({
9
  game,
10
  situation = "",
11
+ lastEvent = "",
12
  }: {
13
  game: Game;
14
  situation: string;
15
+ lastEvent: string;
16
  }) => {
17
 
18
+ const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
19
 
20
  console.log("DEBUG", {
21
+ game, situation, lastEvent,
22
  currentPrompt,
23
  initialPrompt,
24
  userSituationPrompt,
 
31
  */
32
 
33
  const basePrompt = initialPrompt !== currentPrompt
34
+ ? `You must imagine the most plausible next dialogue line from the game master, based on where the player was located before and is now, and also what the player did before and are doing now.
35
  Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
36
  : ""
37
 
src/app/queries/getSound.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Game } from "@/app/games/types"
2
+ import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
3
+
4
+ import { getBase } from "./getBase"
5
+ import { predict } from "./predict"
6
+
7
+
8
+ export const getSound = async ({
9
+ game,
10
+ situation = "",
11
+ lastEvent = "",
12
+ }: {
13
+ game: Game;
14
+ situation: string;
15
+ lastEvent: string;
16
+ }) => {
17
+
18
+ const { currentPrompt, initialPrompt, userSituationPrompt } = getBase({ game, situation, lastEvent })
19
+
20
+ console.log("DEBUG", {
21
+ game, situation, lastEvent,
22
+ currentPrompt,
23
+ initialPrompt,
24
+ userSituationPrompt,
25
+
26
+ })
27
+ /*
28
+ const basePrompt = initialPrompt !== currentPrompt
29
+ ? `for your information, the initial game panel and scene was: ${initialPrompt}`
30
+ : ""
31
+ */
32
+
33
+ const basePrompt = initialPrompt !== currentPrompt
34
+ ? `Here is the original scene in which the user was located at first, which will inform you about the general settings to follow (you must respect this): "${initialPrompt}".`
35
+ : ""
36
+
37
+ const prompt = createLlamaPrompt([
38
+ {
39
+ role: "system",
40
+ content: [
41
+ `You are the AI game master of a role video game.`,
42
+ `You are going to receive new information about the current whereabouts and action of the player.`,
43
+ basePrompt,
44
+ `You must imagine a sound effect in reaction to the player action.`,
45
+ `Here are some examples, but don't copy them verbatim:\n`,
46
+ `- "An excited crowd cheering at a sports game"\n`,
47
+ `- "A cat is meowing for attention"\n`,
48
+ `- "Birds singing sweetly in a blooming garden"\n`,
49
+ `- "A modern synthesizer creating futuristic soundscapes"\n`,
50
+ `- "The vibrant beat of Brazilian samba drums"\n`,
51
+ `Here are some more instructions, to enhance the Qqality of your generated audio:`,
52
+ `1. Try to use more adjectives to describe your sound. For example: "A man is speaking clearly and slowly in a large room" is better than "A man is speaking".\n`,
53
+ `2. It's better to use general terms like 'man' or 'woman' instead of specific names for individuals or abstract objects that humans may not be familiar with, such as 'mummy'.\n`
54
+ ].filter(item => item).join("\n")
55
+ },
56
+ {
57
+ role: "user",
58
+ content: userSituationPrompt
59
+ }
60
+ ])
61
+
62
+
63
+ let result = ""
64
+ try {
65
+ result = await predict(prompt)
66
+ } catch (err) {
67
+ console.log(`prediction of the dialogue failed, trying again..`)
68
+ try {
69
+ result = await predict(prompt)
70
+ } catch (err) {
71
+ console.error(`prediction of the dialogue failed again!`)
72
+ throw new Error(`failed to generate the dialogue ${err}`)
73
+ }
74
+ }
75
+
76
+ return result
77
+ }
src/app/queries/getSoundTrack.ts DELETED
@@ -1,22 +0,0 @@
1
- import { Game } from "@/app/games/types"
2
- import { createLlamaPrompt } from "@/lib/createLlamaPrompt"
3
-
4
- import { getBase } from "./getBase"
5
- import { predict } from "./predict"
6
-
7
- export const getSoundTrack = async ({
8
- game,
9
- situation = "",
10
- actionnable = "",
11
- newDialogue = "",
12
- newActionnables = [],
13
- }: {
14
- game: Game;
15
- situation: string;
16
- actionnable: string;
17
- newDialogue: string;
18
- newActionnables: string[];
19
- }) => {
20
-
21
- return ""
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/store.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { InventoryItem } from "./types"
4
+
5
+ // could also be Zustand or something
6
+ export const store: {
7
+ currentlyDraggedItem?: InventoryItem
8
+ } = {
9
+ currentlyDraggedItem: undefined
10
+ }
src/app/types.ts CHANGED
@@ -46,9 +46,11 @@ export type RenderedSceneStatus =
46
  export type SceneEvent =
47
  | "HoveringNothing"
48
  | "HoveringActionnable"
 
49
  | "ClickOnNothing"
50
  | "ClickOnActionnable"
51
 
 
52
  export interface RenderedScene {
53
  renderId: string
54
  status: RenderedSceneStatus
@@ -69,9 +71,23 @@ export type InventoryEvent =
69
  | "DroppedOnAnotherItem" // the item has been dropped on another inventory item
70
  | "DroppedBackToInventory" // the drag & drop is cancelled, the item is back in the inventory
71
 
72
- export interface InventoryItem {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  name: string
74
- title: string
75
- caption: string
76
- description: string
77
- }
 
46
  export type SceneEvent =
47
  | "HoveringNothing"
48
  | "HoveringActionnable"
49
+ // | "ItemIsOverActionnable"
50
  | "ClickOnNothing"
51
  | "ClickOnActionnable"
52
 
53
+
54
  export interface RenderedScene {
55
  renderId: string
56
  status: RenderedSceneStatus
 
71
  | "DroppedOnAnotherItem" // the item has been dropped on another inventory item
72
  | "DroppedBackToInventory" // the drag & drop is cancelled, the item is back in the inventory
73
 
74
+ export interface InventoryItem {
75
+ name: string
76
+ title: string
77
+ caption: string
78
+ description: string
79
+ }
80
+
81
+ export interface DropZoneTarget {
82
+ type: "InventoryItem" | "Actionnable"
83
+ name: string
84
+ title?: string
85
+ caption?: string
86
+ description?: string
87
+ }
88
+
89
+ export type OnInventoryEvent = (event: InventoryEvent, item: InventoryItem, target?: {
90
  name: string
91
+ title?: string
92
+ description?: string
93
+ }) => void
 
src/components/inventory/draggable-item.tsx CHANGED
@@ -1,26 +1,39 @@
1
-
2
  import Image from "next/image"
3
  import { useDrag, useDrop } from "react-dnd"
4
 
5
  import { Game } from "@/app/games/types"
6
  import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
7
- import { InventoryEvent, InventoryItem } from "@/app/types"
8
- import { useEffect } from "react"
9
 
10
- type DropResult = InventoryItem
11
 
12
  export function DraggableItem({ game, item, onEvent }: {
13
- game: Game;
14
- item: InventoryItem;
15
- onEvent: (event: InventoryEvent, item: InventoryItem, target?: InventoryItem) => void
16
  }) {
17
  const [{ isDragging }, drag] = useDrag(() => ({
18
  type: "item",
19
  item,
20
  end: (item, monitor) => {
21
- const dropResult = monitor.getDropResult<InventoryItem>()
22
- if (item && dropResult) {
23
- onEvent("DroppedOnAnotherItem", item, dropResult)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
  },
26
  collect: (monitor) => ({
@@ -31,7 +44,12 @@ export function DraggableItem({ game, item, onEvent }: {
31
 
32
  const [{ isOver, canDrop }, drop] = useDrop({
33
  accept: "item",
34
- drop: () => (item),
 
 
 
 
 
35
  collect: (monitor) => ({
36
  isOver: monitor.isOver(),
37
  canDrop: monitor.canDrop(),
@@ -40,8 +58,10 @@ export function DraggableItem({ game, item, onEvent }: {
40
 
41
  useEffect(() => {
42
  if (isDragging) {
 
43
  onEvent("Grabbing", item)
44
  } else {
 
45
  onEvent("DroppedBackToInventory", item)
46
  }
47
  }, [isDragging])
 
1
+ import { useEffect, useRef } from "react"
2
  import Image from "next/image"
3
  import { useDrag, useDrop } from "react-dnd"
4
 
5
  import { Game } from "@/app/games/types"
6
  import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
7
+ import { DropZoneTarget, InventoryEvent, InventoryItem, OnInventoryEvent } from "@/app/types"
 
8
 
9
+ import { store } from "@/app/store"
10
 
11
  export function DraggableItem({ game, item, onEvent }: {
12
+ game: Game
13
+ item: InventoryItem
14
+ onEvent: OnInventoryEvent
15
  }) {
16
  const [{ isDragging }, drag] = useDrag(() => ({
17
  type: "item",
18
  item,
19
  end: (item, monitor) => {
20
+ const target = monitor.getDropResult<DropZoneTarget>()
21
+ if (item && target) {
22
+ if ( target.type === "InventoryItem" && item.name === target.name) {
23
+ // user is trying to drop the item on itself.. that's not possible
24
+ return
25
+ }
26
+ onEvent(
27
+ target.type === "Actionnable"
28
+ ? "DroppedOnActionnable"
29
+ : "DroppedOnAnotherItem",
30
+ item,
31
+ {
32
+ name: target.name,
33
+ title: target.title,
34
+ description: target.description
35
+ }
36
+ )
37
  }
38
  },
39
  collect: (monitor) => ({
 
44
 
45
  const [{ isOver, canDrop }, drop] = useDrop({
46
  accept: "item",
47
+ drop: (): DropZoneTarget => ({
48
+ type: "InventoryItem",
49
+ name: item.name,
50
+ title: item.title,
51
+ description: item.description
52
+ }),
53
  collect: (monitor) => ({
54
  isOver: monitor.isOver(),
55
  canDrop: monitor.canDrop(),
 
58
 
59
  useEffect(() => {
60
  if (isDragging) {
61
+ store.currentlyDraggedItem = item
62
  onEvent("Grabbing", item)
63
  } else {
64
+ store.currentlyDraggedItem = undefined
65
  onEvent("DroppedBackToInventory", item)
66
  }
67
  }, [isDragging])
src/components/inventory/index.tsx CHANGED
@@ -1,29 +1,24 @@
1
- import { DndProvider } from "react-dnd"
2
- import { HTML5Backend } from "react-dnd-html5-backend"
3
-
4
  import { Game } from "@/app/games/types"
5
  import { DraggableItem } from "./draggable-item"
6
- import { InventoryEvent, InventoryItem } from "@/app/types"
7
 
8
  export function Inventory({
9
  game,
10
  onEvent
11
  }: {
12
  game: Game;
13
- onEvent: (event: InventoryEvent, item: InventoryItem, target?: InventoryItem) => void
14
  }) {
15
  return (
16
- <DndProvider backend={HTML5Backend}>
17
- <div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12 gap-4 w-full bg-stone-500 p-4 rounded-xl">
18
- {game.inventory.map(item => (
19
- <DraggableItem
20
- key={item.name}
21
- game={game}
22
- item={item}
23
- onEvent={onEvent}
24
- />
25
- ))}
26
- </div>
27
- </DndProvider>
28
  )
29
  }
 
 
 
 
1
  import { Game } from "@/app/games/types"
2
  import { DraggableItem } from "./draggable-item"
3
+ import { OnInventoryEvent } from "@/app/types"
4
 
5
  export function Inventory({
6
  game,
7
  onEvent
8
  }: {
9
  game: Game;
10
+ onEvent: OnInventoryEvent;
11
  }) {
12
  return (
13
+ <div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 lg:grid-cols-12 gap-4 w-full bg-stone-500 p-4 rounded-xl">
14
+ {game.inventory.map(item => (
15
+ <DraggableItem
16
+ key={item.name}
17
+ game={game}
18
+ item={item}
19
+ onEvent={onEvent}
20
+ />
21
+ ))}
22
+ </div>
 
 
23
  )
24
  }
src/components/renderer/cartesian-image.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { useRef } from "react"
2
- import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
 
5
  export function CartesianImage({
@@ -9,12 +9,53 @@ export function CartesianImage({
9
  debug
10
  }: {
11
  rendered: RenderedScene
12
- onEvent: SceneEventHandler
13
  className?: string
14
  debug?: boolean
15
  }) {
16
 
17
  const ref = useRef<HTMLImageElement>(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
19
 
20
  if (!ref.current) {
@@ -35,6 +76,7 @@ export function CartesianImage({
35
  const relativeY = containerY / boundingRect.height
36
 
37
  const eventType = isClick ? "click" : "hover"
 
38
  onEvent(eventType, relativeX, relativeY)
39
  }
40
 
 
1
+ import { useEffect, useRef } from "react"
2
+ import { MouseEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
 
5
  export function CartesianImage({
 
9
  debug
10
  }: {
11
  rendered: RenderedScene
12
+ onEvent: MouseEventHandler
13
  className?: string
14
  debug?: boolean
15
  }) {
16
 
17
  const ref = useRef<HTMLImageElement>(null)
18
+
19
+ const cacheRef = useRef("")
20
+ useEffect(() => {
21
+ const listener = (e: DragEvent) => {
22
+ if (!ref.current) { return }
23
+
24
+ // TODO: check if we are currently dragging an object
25
+ // if yes, then we should check if clientX and clientY are matching the
26
+ const boundingRect = ref.current.getBoundingClientRect()
27
+
28
+ // abort if we are not currently dragging over our display area
29
+ if (e.clientX < boundingRect.left) { return }
30
+ if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
31
+ if (e.clientY < boundingRect.top) { return }
32
+ if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
33
+
34
+ const containerX = e.clientX - boundingRect.left
35
+ const containerY = e.clientY - boundingRect.top
36
+
37
+ const relativeX = containerX / boundingRect.width
38
+ const relativeY = containerY / boundingRect.height
39
+
40
+ const key = `${relativeX},${relativeY}`
41
+
42
+ // to avoid use
43
+ if (cacheRef.current === key) {
44
+ return
45
+ }
46
+ // console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
47
+
48
+ cacheRef.current = key
49
+ onEvent("hover", relativeX, relativeY)
50
+ }
51
+
52
+ document.addEventListener('drag', listener)
53
+
54
+ return () => {
55
+ document.removeEventListener('drag', listener)
56
+ }
57
+ }, [onEvent])
58
+
59
  const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
60
 
61
  if (!ref.current) {
 
76
  const relativeY = containerY / boundingRect.height
77
 
78
  const eventType = isClick ? "click" : "hover"
79
+
80
  onEvent(eventType, relativeX, relativeY)
81
  }
82
 
src/components/renderer/cartesian-video.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { useRef } from "react"
2
- import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
 
5
  export function CartesianVideo({
@@ -9,11 +9,53 @@ export function CartesianVideo({
9
  debug,
10
  }: {
11
  rendered: RenderedScene
12
- onEvent: SceneEventHandler
13
  className?: string
14
  debug?: boolean
15
  }) {
16
  const ref = useRef<HTMLVideoElement>(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
18
 
19
  if (!ref.current) {
 
1
+ import { useEffect, useRef } from "react"
2
+ import { MouseEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
 
5
  export function CartesianVideo({
 
9
  debug,
10
  }: {
11
  rendered: RenderedScene
12
+ onEvent: MouseEventHandler
13
  className?: string
14
  debug?: boolean
15
  }) {
16
  const ref = useRef<HTMLVideoElement>(null)
17
+
18
+
19
+ const cacheRef = useRef("")
20
+ useEffect(() => {
21
+ const listener = (e: DragEvent) => {
22
+ if (!ref.current) { return }
23
+
24
+ // TODO: check if we are currently dragging an object
25
+ // if yes, then we should check if clientX and clientY are matching the
26
+ const boundingRect = ref.current.getBoundingClientRect()
27
+
28
+ // abort if we are not currently dragging over our display area
29
+ if (e.clientX < boundingRect.left) { return }
30
+ if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
31
+ if (e.clientY < boundingRect.top) { return }
32
+ if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
33
+
34
+ const containerX = e.clientX - boundingRect.left
35
+ const containerY = e.clientY - boundingRect.top
36
+
37
+ const relativeX = containerX / boundingRect.width
38
+ const relativeY = containerY / boundingRect.height
39
+
40
+ const key = `${relativeX},${relativeY}`
41
+
42
+ // to avoid use
43
+ if (cacheRef.current === key) {
44
+ return
45
+ }
46
+ // console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
47
+
48
+ cacheRef.current = key
49
+ onEvent("hover", relativeX, relativeY)
50
+ }
51
+
52
+ document.addEventListener('drag', listener)
53
+
54
+ return () => {
55
+ document.removeEventListener('drag', listener)
56
+ }
57
+ }, [onEvent])
58
+
59
  const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
60
 
61
  if (!ref.current) {
src/components/renderer/index.tsx CHANGED
@@ -1,14 +1,16 @@
1
  import { useEffect, useRef, useState } from "react"
2
 
3
- import { ImageSegment, RenderedScene, SceneEvent } from "@/app/types"
4
  import { ProgressBar } from "../misc/progress"
5
  import { Game } from "@/app/games/types"
6
  import { Engine } from "@/app/engines"
7
  import { CartesianImage } from "./cartesian-image"
8
- import { SceneEventHandler, SceneEventType } from "./types"
9
  import { CartesianVideo } from "./cartesian-video"
10
  import { SphericalImage } from "./spherical-image"
11
  import { useImageDimension } from "@/lib/useImageDimension"
 
 
12
 
13
  export const SceneRenderer = ({
14
  rendered,
@@ -16,7 +18,7 @@ export const SceneRenderer = ({
16
  isLoading,
17
  game,
18
  engine,
19
- debug
20
  }: {
21
  rendered: RenderedScene
22
  onEvent: (event: SceneEvent, actionnable?: string) => void
@@ -29,11 +31,26 @@ export const SceneRenderer = ({
29
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
30
  const contextRef = useRef<CanvasRenderingContext2D | null>(null)
31
  const [actionnable, setActionnable] = useState<string>("")
 
32
  const [progressPercent, setProcessPercent] = useState(0)
33
  const progressRef = useRef(0)
34
  const isLoadingRef = useRef(isLoading)
35
  const maskDimension = useImageDimension(rendered.maskUrl)
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  useEffect(() => {
38
  if (!rendered.maskUrl) {
39
  return
@@ -104,7 +121,7 @@ export const SceneRenderer = ({
104
  }
105
 
106
  // note: coordinates must be between 0 and 1
107
- const handleMouseEvent: SceneEventHandler = async (type: SceneEventType, relativeX: number, relativeY: number) => {
108
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
109
  if (!rendered.maskUrl) return;
110
 
@@ -134,7 +151,7 @@ export const SceneRenderer = ({
134
  }
135
 
136
  // update the actionnable immediately, so we can show the hand / finger cursor pointer
137
- setActionnable(newSegment.label)
138
  }
139
 
140
  if (type === "click") {
@@ -181,7 +198,7 @@ export const SceneRenderer = ({
181
  }, [isLoading, rendered.assetUrl, engine?.type])
182
 
183
  return (
184
- <div className="w-full pt-2">
185
  <div
186
  className={[
187
  "relative border-2 border-gray-50 rounded-xl overflow-hidden min-h-[512px]",
 
1
  import { useEffect, useRef, useState } from "react"
2
 
3
+ import { DropZoneTarget, ImageSegment, RenderedScene, SceneEvent } from "@/app/types"
4
  import { ProgressBar } from "../misc/progress"
5
  import { Game } from "@/app/games/types"
6
  import { Engine } from "@/app/engines"
7
  import { CartesianImage } from "./cartesian-image"
8
+ import { MouseEventHandler, MouseEventType } from "./types"
9
  import { CartesianVideo } from "./cartesian-video"
10
  import { SphericalImage } from "./spherical-image"
11
  import { useImageDimension } from "@/lib/useImageDimension"
12
+ import { useDrop } from "react-dnd"
13
+ import { formatActionnableName } from "@/lib/formatActionnableName"
14
 
15
  export const SceneRenderer = ({
16
  rendered,
 
18
  isLoading,
19
  game,
20
  engine,
21
+ debug,
22
  }: {
23
  rendered: RenderedScene
24
  onEvent: (event: SceneEvent, actionnable?: string) => void
 
31
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
32
  const contextRef = useRef<CanvasRenderingContext2D | null>(null)
33
  const [actionnable, setActionnable] = useState<string>("")
34
+ const actionnableRef = useRef<string>("")
35
  const [progressPercent, setProcessPercent] = useState(0)
36
  const progressRef = useRef(0)
37
  const isLoadingRef = useRef(isLoading)
38
  const maskDimension = useImageDimension(rendered.maskUrl)
39
 
40
+ const [{ isOver, canDrop }, drop] = useDrop({
41
+ accept: "item",
42
+ drop: (): DropZoneTarget => ({
43
+ type: "Actionnable",
44
+ name: actionnable,
45
+ title: formatActionnableName(actionnable),
46
+ description: ""
47
+ }),
48
+ collect: (monitor) => ({
49
+ isOver: monitor.isOver(),
50
+ canDrop: monitor.canDrop(),
51
+ }),
52
+ })
53
+
54
  useEffect(() => {
55
  if (!rendered.maskUrl) {
56
  return
 
121
  }
122
 
123
  // note: coordinates must be between 0 and 1
124
+ const handleMouseEvent: MouseEventHandler = async (type: MouseEventType, relativeX: number, relativeY: number) => {
125
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
126
  if (!rendered.maskUrl) return;
127
 
 
151
  }
152
 
153
  // update the actionnable immediately, so we can show the hand / finger cursor pointer
154
+ setActionnable(actionnableRef.current = newSegment.label)
155
  }
156
 
157
  if (type === "click") {
 
198
  }, [isLoading, rendered.assetUrl, engine?.type])
199
 
200
  return (
201
+ <div className="w-full pt-2" ref={drop}>
202
  <div
203
  className={[
204
  "relative border-2 border-gray-50 rounded-xl overflow-hidden min-h-[512px]",
src/components/renderer/spherical-image.tsx CHANGED
@@ -4,7 +4,7 @@ import { LensflarePlugin, ReactPhotoSphereViewer } from "react-photo-sphere-view
4
 
5
  import { RenderedScene } from "@/app/types"
6
 
7
- import { SceneEventHandler } from "./types"
8
  import { useImageDimension } from "@/lib/useImageDimension"
9
  import { lightSourceNames } from "@/lib/lightSourceNames"
10
 
@@ -17,7 +17,7 @@ export function SphericalImage({
17
  debug,
18
  }: {
19
  rendered: RenderedScene
20
- onEvent: SceneEventHandler
21
  className?: string
22
  debug?: boolean
23
  }) {
@@ -56,6 +56,46 @@ export function SphericalImage({
56
  }
57
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  useEffect(() => {
60
  const task = async () => {
61
  // console.log("SphericalImage: useEffect")
 
4
 
5
  import { RenderedScene } from "@/app/types"
6
 
7
+ import { MouseEventHandler } from "./types"
8
  import { useImageDimension } from "@/lib/useImageDimension"
9
  import { lightSourceNames } from "@/lib/lightSourceNames"
10
 
 
17
  debug,
18
  }: {
19
  rendered: RenderedScene
20
+ onEvent: MouseEventHandler
21
  className?: string
22
  debug?: boolean
23
  }) {
 
56
  }
57
 
58
 
59
+ const cacheRef = useRef("")
60
+ useEffect(() => {
61
+ const listener = (e: DragEvent) => {
62
+ if (!rootContainerRef.current) { return }
63
+
64
+ // TODO: check if we are currently dragging an object
65
+ // if yes, then we should check if clientX and clientY are matching the
66
+ const boundingRect = rootContainerRef.current.getBoundingClientRect()
67
+
68
+ // abort if we are not currently dragging over our display area
69
+ if (e.clientX < boundingRect.left) { return }
70
+ if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
71
+ if (e.clientY < boundingRect.top) { return }
72
+ if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
73
+
74
+ const containerX = e.clientX - boundingRect.left
75
+ const containerY = e.clientY - boundingRect.top
76
+
77
+ const relativeX = containerX / boundingRect.width
78
+ const relativeY = containerY / boundingRect.height
79
+
80
+ const key = `${relativeX},${relativeY}`
81
+
82
+ // to avoid use
83
+ if (cacheRef.current === key) {
84
+ return
85
+ }
86
+ // console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
87
+
88
+ cacheRef.current = key
89
+ onEvent("hover", relativeX, relativeY)
90
+ }
91
+
92
+ document.addEventListener('drag', listener)
93
+
94
+ return () => {
95
+ document.removeEventListener('drag', listener)
96
+ }
97
+ }, [onEvent])
98
+
99
  useEffect(() => {
100
  const task = async () => {
101
  // console.log("SphericalImage: useEffect")
src/components/renderer/types.ts CHANGED
@@ -1,3 +1,3 @@
1
- export type SceneEventType = 'hover' | 'click' | 'back'
2
 
3
- export type SceneEventHandler = (type: SceneEventType, x: number, y: number) => Promise<void>
 
1
+ export type MouseEventType = "hover" | "click"
2
 
3
+ export type MouseEventHandler = (type: MouseEventType, x: number, y: number) => Promise<void>
src/lib/defaultActionnables.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const defaultActionnables = [
2
+ "door",
3
+ "rock",
4
+ "window",
5
+ "table",
6
+ "ground",
7
+ "sky",
8
+ "object",
9
+ "tree",
10
+ "wall",
11
+ "floor"
12
+ // but we still only want 10 here
13
+ ]
src/lib/formatActionnableName.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const formatActionnableName = (input: string) => {
2
+ input = input.replaceAll("-", " ")
3
+ return input.charAt(0).toUpperCase() + input.slice(1)
4
+ }
src/lib/normalizeActionnables.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { lightSourceNames } from "./lightSourceNames"
2
 
3
  export function normalizeActionnables(rawActionnables: string[]) {
@@ -10,16 +11,7 @@ export function normalizeActionnables(rawActionnables: string[]) {
10
  const deduplicated = new Set<string>([
11
  ...tmp,
12
  // in case result is too small, we add a reserve of useful words here
13
- "door",
14
- "rock",
15
- "window",
16
- "table",
17
- "ground",
18
- "sky",
19
- "object",
20
- "tree",
21
- "wall",
22
- "floor"
23
  // but we still only want 10 here
24
  ].slice(0, 10)
25
  )
 
1
+ import { defaultActionnables } from "./defaultActionnables"
2
  import { lightSourceNames } from "./lightSourceNames"
3
 
4
  export function normalizeActionnables(rawActionnables: string[]) {
 
11
  const deduplicated = new Set<string>([
12
  ...tmp,
13
  // in case result is too small, we add a reserve of useful words here
14
+ ...defaultActionnables,
 
 
 
 
 
 
 
 
 
15
  // but we still only want 10 here
16
  ].slice(0, 10)
17
  )