jbilcke-hf HF staff commited on
Commit
e62f50c
Β·
1 Parent(s): 65b89b5
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -55,12 +55,14 @@
55
  "pick": "^0.0.1",
56
  "postcss": "8.4.26",
57
  "react": "18.2.0",
 
58
  "react-circular-progressbar": "^2.1.0",
59
  "react-day-picker": "^8.8.0",
60
  "react-dnd": "^16.0.1",
61
  "react-dnd-html5-backend": "^16.0.1",
62
  "react-dom": "18.2.0",
63
  "react-photo-sphere-viewer": "^3.3.5-psv5.1.4",
 
64
  "tailwind-merge": "^1.13.2",
65
  "tailwindcss": "3.3.3",
66
  "tailwindcss-animate": "^1.0.6",
 
55
  "pick": "^0.0.1",
56
  "postcss": "8.4.26",
57
  "react": "18.2.0",
58
+ "react-circular-menu": "^2.4.2",
59
  "react-circular-progressbar": "^2.1.0",
60
  "react-day-picker": "^8.8.0",
61
  "react-dnd": "^16.0.1",
62
  "react-dnd-html5-backend": "^16.0.1",
63
  "react-dom": "18.2.0",
64
  "react-photo-sphere-viewer": "^3.3.5-psv5.1.4",
65
+ "styled-components": "^6.0.7",
66
  "tailwind-merge": "^1.13.2",
67
  "tailwindcss": "3.3.3",
68
  "tailwindcss-animate": "^1.0.6",
src/app/main.tsx CHANGED
@@ -315,8 +315,8 @@ export default function Main() {
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) {
 
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, searching for clues.</>
319
+ newEventString = `User is looking at the scene, searching for clues.`
320
  }
321
  } else if (event === "HoveringActionnable") {
322
  if (item) {
src/app/queries/getActionnables.ts CHANGED
@@ -6,6 +6,27 @@ import { getBase } from "./getBase"
6
  import { predict } from "./predict"
7
  import { normalizeActionnables } from "@/lib/normalizeActionnables"
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  export const getActionnables = async ({
10
  game,
11
  situation = "",
@@ -39,43 +60,21 @@ export const getActionnables = async ({
39
  ])
40
 
41
  let rawStringOutput = ""
42
-
 
43
  try {
44
  rawStringOutput = await predict(prompt)
 
45
  } catch (err) {
46
  console.log(`prediction of the actionnables failed, trying again..`)
47
  try {
48
- rawStringOutput = await predict(prompt)
 
49
  } catch (err) {
50
- console.error(`prediction of the actionnables failed again!`)
51
- throw new Error(`failed to generate the actionnables ${err}`)
52
  }
53
  }
54
 
55
- let result: string[] = []
56
-
57
- try {
58
- result = parseJsonList(rawStringOutput)
59
-
60
- if (!result.length) {
61
- throw new Error("no actionnables")
62
- }
63
- } catch (err) {
64
- console.log("failed to find a valid JSON! attempting method 2..")
65
-
66
- try {
67
- const sanitized = rawStringOutput.replaceAll("[", "").replaceAll("]", "")
68
- result = (JSON.parse(`[${sanitized}]`) as string[])
69
-
70
- if (!result.length) {
71
- throw new Error("no actionnables")
72
- }
73
- } catch (err) {
74
- console.log("failed to repair and recover a valid JSON! Using a generic fallback..")
75
-
76
- // throw new Error("failed to parse the actionnables")
77
- }
78
- }
79
-
80
  return normalizeActionnables(result)
81
  }
 
6
  import { predict } from "./predict"
7
  import { normalizeActionnables } from "@/lib/normalizeActionnables"
8
 
9
+ const parseActionnablesOrThrow = (input: string) => {
10
+ let result: string[] = []
11
+ try {
12
+ result = parseJsonList(input)
13
+
14
+ if (!result.length) {
15
+ throw new Error("no actionnables")
16
+ }
17
+ } catch (err) {
18
+ console.log("failed to find a valid JSON! attempting method 2..")
19
+
20
+ const sanitized = input.replaceAll("[", "").replaceAll("]", "")
21
+ result = (JSON.parse(`[${sanitized}]`) as string[])
22
+
23
+ if (!result.length) {
24
+ throw new Error("no actionnables")
25
+ }
26
+ }
27
+ return result
28
+ }
29
+
30
  export const getActionnables = async ({
31
  game,
32
  situation = "",
 
60
  ])
61
 
62
  let rawStringOutput = ""
63
+ let result: string[] = []
64
+
65
  try {
66
  rawStringOutput = await predict(prompt)
67
+ result = parseActionnablesOrThrow(rawStringOutput)
68
  } catch (err) {
69
  console.log(`prediction of the actionnables failed, trying again..`)
70
  try {
71
+ rawStringOutput = await predict(prompt+".")
72
+ result = parseActionnablesOrThrow(rawStringOutput)
73
  } catch (err) {
74
+ console.error(`prediction of the actionnables failed again! going to use default value`)
75
+ console.log("for reference, rawStringOutput was: ", rawStringOutput)
76
  }
77
  }
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  return normalizeActionnables(result)
80
  }
src/app/queries/getBackground.ts CHANGED
@@ -27,20 +27,21 @@ export const getBackground = async ({
27
  })
28
 
29
  const basePrompt = initialPrompt !== currentPrompt
30
- ? `You must imagine the most plausible next scene, based on where the player was located before and is now, and also what the player did before and are doing now.
31
- 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}".`
32
  : ""
33
 
34
  const prompt = createLlamaPrompt([
35
  {
36
  role: "system",
37
  content: [
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
  {
@@ -63,5 +64,6 @@ Here is the original scene in which the user was located at first, which will in
63
  }
64
  }
65
 
66
- return result
 
67
  }
 
27
  })
28
 
29
  const basePrompt = initialPrompt !== currentPrompt
30
+ ? `You must imagine a very short caption for a background photo image, based on current and past situation.
31
+ Here is the original scene in which the user was located at first, which will inform you about the general game mood to follow (you must respect this): "${initialPrompt}".`
32
  : ""
33
 
34
  const prompt = createLlamaPrompt([
35
  {
36
  role: "system",
37
  content: [
38
+ `You are the photo director of a role video game.`,
39
  basePrompt,
40
+ `You are going to receive new information about the current activity of the player.`,
41
+ `Please write in a single sentence a photo caption for the next plausible scene, using a few words for each of those categories: the environment, era, characters, objects, textures, lighting.`,
42
+ `Separate each of those category descriptions using a comma.`,
43
+ `You MUST mention the following important objects that the user can click on: ${newActionnables}.`,
44
+ `Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "As the player approaches.." or "As the player clicks.." or "the scene shifts to.." (the best is not not mention the player at all)`
45
  ].filter(item => item).join("\n")
46
  },
47
  {
 
64
  }
65
  }
66
 
67
+ const tmp = result.split("Caption:").pop() || result
68
+ return tmp.replaceAll("\n", ", ")
69
  }
src/app/queries/getDialogue.ts CHANGED
@@ -31,8 +31,8 @@ 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
 
38
  const prompt = createLlamaPrompt([
 
31
  */
32
 
33
  const basePrompt = initialPrompt !== currentPrompt
34
+ ? `You must imagine the most plausible next dialogue line from the game master, based on current and past situation.
35
+ Here is the original situation, which will inform you about the general game mood to follow (you must respect this): "${initialPrompt}".`
36
  : ""
37
 
38
  const prompt = createLlamaPrompt([
src/components/renderer/index.tsx CHANGED
@@ -11,6 +11,8 @@ 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,
@@ -28,6 +30,7 @@ export const SceneRenderer = ({
28
  debug: boolean
29
  }) => {
30
  const timeoutRef = useRef<any>()
 
31
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
32
  const contextRef = useRef<CanvasRenderingContext2D | null>(null)
33
  const [actionnable, setActionnable] = useState<string>("")
@@ -37,6 +40,17 @@ export const SceneRenderer = ({
37
  const isLoadingRef = useRef(isLoading)
38
  const maskDimension = useImageDimension(rendered.maskUrl)
39
 
 
 
 
 
 
 
 
 
 
 
 
40
  const [{ isOver, canDrop }, drop] = useDrop({
41
  accept: "item",
42
  drop: (): DropZoneTarget => ({
@@ -122,19 +136,21 @@ export const SceneRenderer = ({
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
 
128
- if (isLoading) {
129
- // we ignore all user interactions
130
- return
131
- }
 
 
 
 
 
 
 
132
 
133
- // sometimes we generate an image, but the segmentation fails
134
- // so if we click anywhere bug there are no segments,
135
- // we inform the rest of the app by passing nothing
136
- if (type === "click" && rendered.segments.length == 0) {
137
- onEvent("ClickOnNothing")
138
  return
139
  }
140
 
@@ -154,21 +170,77 @@ export const SceneRenderer = ({
154
  setActionnable(actionnableRef.current = newSegment.label)
155
  }
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  if (type === "click") {
 
158
  if (!newSegment.label) {
 
159
  return
160
  }
 
 
 
 
161
  console.log("User clicked on " + newSegment.label)
162
  onEvent("ClickOnActionnable", actionnable)
163
- } else {
164
- // only trigger hover events if there are segments,
165
- // otherwise it's best to stay silent
166
- if (rendered.segments.length) {
167
- if (actionnable) {
168
- onEvent("HoveringActionnable", actionnable)
169
- } else {
170
- onEvent("HoveringNothing")
171
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
  }
174
  };
@@ -200,17 +272,20 @@ export const SceneRenderer = ({
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]",
205
  engine.type === "cartesian_video"
206
  || engine.type === "cartesian_image"
207
- ? " w-full" // w-[1024px] h-[512px]"
208
  : "w-full",
209
 
210
  isLoading
211
  ? "cursor-wait"
212
  : actionnable
213
- ? "cursor-pointer"
 
 
214
  : ""
215
  ].join(" ")}>
216
  {engine.type === "cartesian_video"
@@ -234,6 +309,22 @@ export const SceneRenderer = ({
234
 
235
  </div>
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  {isLoading
238
  ? <div className="fixed flex w-20 h-20 bottom-8 right-0 mr-8 z-50">
239
  <ProgressBar
 
11
  import { useImageDimension } from "@/lib/useImageDimension"
12
  import { useDrop } from "react-dnd"
13
  import { formatActionnableName } from "@/lib/formatActionnableName"
14
+ import { SceneTooltip } from "./scene-tooltip"
15
+ import { SceneMenu } from "./scene-menu"
16
 
17
  export const SceneRenderer = ({
18
  rendered,
 
30
  debug: boolean
31
  }) => {
32
  const timeoutRef = useRef<any>()
33
+ const containerRef = useRef<HTMLDivElement>(null)
34
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
35
  const contextRef = useRef<CanvasRenderingContext2D | null>(null)
36
  const [actionnable, setActionnable] = useState<string>("")
 
40
  const isLoadingRef = useRef(isLoading)
41
  const maskDimension = useImageDimension(rendered.maskUrl)
42
 
43
+ const [isHover, setHover] = useState(false)
44
+
45
+ const tooltipTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
46
+ const menuTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
47
+ const [isTooltipVisible, setTooltipVisible] = useState(false)
48
+ const [isMenuVisible, setMenuVisible] = useState(false)
49
+ const [tooltipX, setTooltipX] = useState(0)
50
+ const [tooltipY, setTooltipY] = useState(0)
51
+ const [menuX, setMenuX] = useState(0)
52
+ const [menuY, setMenuY] = useState(0)
53
+
54
  const [{ isOver, canDrop }, drop] = useDrop({
55
  accept: "item",
56
  drop: (): DropZoneTarget => ({
 
136
 
137
  // note: coordinates must be between 0 and 1
138
  const handleMouseEvent: MouseEventHandler = async (type: MouseEventType, relativeX: number, relativeY: number) => {
 
 
139
 
140
+ const noMenu = !containerRef.current
141
+ const noContext = !contextRef.current
142
+ const noSegmentationMask = !rendered.maskUrl
143
+ const noSegmentsToClickOn = rendered.segments.length == 0
144
+
145
+ const mustAbort =
146
+ noMenu
147
+ || noContext
148
+ || noSegmentationMask
149
+ || noSegmentsToClickOn
150
+ || isLoading
151
 
152
+ if (mustAbort) {
153
+ // if (type === "click") { onEvent("ClickOnNothing") }
 
 
 
154
  return
155
  }
156
 
 
170
  setActionnable(actionnableRef.current = newSegment.label)
171
  }
172
 
173
+ const container = containerRef.current
174
+ const containerBox = container.getBoundingClientRect()
175
+
176
+ const absoluteMouseX = containerBox.left + relativeX * container.clientWidth
177
+ const absoluteMouseY = containerBox.top + relativeY * container.clientHeight
178
+
179
+ clearTimeout(tooltipTimeoutRef.current)
180
+ clearTimeout(menuTimeoutRef.current)
181
+ setTooltipVisible(false)
182
+ setMenuVisible(false)
183
+ setTooltipX(absoluteMouseX)
184
+ setTooltipY(absoluteMouseY)
185
+ setMenuX(absoluteMouseX)
186
+ setMenuY(absoluteMouseY)
187
+
188
+
189
  if (type === "click") {
190
+ setMenuVisible(false)
191
  if (!newSegment.label) {
192
+ // setMenuVisible(false)
193
  return
194
  }
195
+
196
+ setTooltipVisible(true)
197
+ setMenuVisible(true)
198
+
199
  console.log("User clicked on " + newSegment.label)
200
  onEvent("ClickOnActionnable", actionnable)
201
+ } else { // hover
202
+ if (actionnable) {
203
+ setHover(true)
204
+
205
+ tooltipTimeoutRef.current = setTimeout(() => {
206
+ if (tooltipTimeoutRef.current) {
207
+ clearTimeout(tooltipTimeoutRef.current)
208
+ tooltipTimeoutRef.current = undefined
209
+ setTooltipVisible(true)
210
+ }
211
+ }, 400)
212
+
213
+ menuTimeoutRef.current = setTimeout(() => {
214
+ if (menuTimeoutRef.current) {
215
+ clearTimeout(menuTimeoutRef.current)
216
+ menuTimeoutRef.current = undefined
217
+ setMenuVisible(true)
218
+ }
219
+ }, 500)
220
+
221
+ onEvent("HoveringActionnable", actionnable)
222
+ } else {
223
+ setHover(false)
224
+ onEvent("HoveringNothing")
225
+
226
+ /*
227
+ tooltipTimeoutRef.current = setTimeout(() => {
228
+ if (tooltipTimeoutRef.current) {
229
+ setTooltipVisible(false)
230
+ clearTimeout(tooltipTimeoutRef.current)
231
+ tooltipTimeoutRef.current = undefined
232
+ }
233
+ }, 500)
234
+
235
+ menuTimeoutRef.current = setTimeout(() => {
236
+ if (menuTimeoutRef.current) {
237
+ setMenuVisible(false)
238
+ clearTimeout(menuTimeoutRef.current)
239
+ menuTimeoutRef.current = undefined
240
+ }
241
+ }, 500)
242
+ */
243
+
244
  }
245
  }
246
  };
 
272
  return (
273
  <div className="w-full pt-2" ref={drop}>
274
  <div
275
+ ref={containerRef}
276
  className={[
277
  "relative border-2 border-gray-50 rounded-xl overflow-hidden min-h-[512px]",
278
  engine.type === "cartesian_video"
279
  || engine.type === "cartesian_image"
280
+ ? "w-full" // w-[1024px] h-[512px]"
281
  : "w-full",
282
 
283
  isLoading
284
  ? "cursor-wait"
285
  : actionnable
286
+ ? isHover
287
+ ? "cursor-crosshair"
288
+ : "cursor-crosshair"
289
  : ""
290
  ].join(" ")}>
291
  {engine.type === "cartesian_video"
 
309
 
310
  </div>
311
 
312
+ <SceneTooltip
313
+ isVisible={isTooltipVisible && !isLoading}
314
+ x={tooltipX}
315
+ y={tooltipY}>
316
+ {actionnable}
317
+ </SceneTooltip>
318
+
319
+ {/*
320
+ <SceneMenu
321
+ actions={["Go here", "Interact"]}
322
+ isVisible={isMenuVisible && !isLoading}
323
+ x={menuX}
324
+ y={menuY}
325
+ />
326
+ */}
327
+
328
  {isLoading
329
  ? <div className="fixed flex w-20 h-20 bottom-8 right-0 mr-8 z-50">
330
  <ProgressBar
src/components/renderer/scene-menu.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function SceneMenu({
2
+ actions,
3
+ isVisible,
4
+ x,
5
+ y,
6
+ }: {
7
+ actions: string[]
8
+ isVisible: boolean
9
+ x: number
10
+ y: number
11
+ }) {
12
+ return (
13
+ <div className={[
14
+ `z-20 fixed flex flex-col w-24 pt-8 px-2 pb-2`,
15
+ `translate-x-[-50%] translate-y-[-20px]`,
16
+ isVisible ? "" : "",
17
+ isVisible ? "" : "pointer-events-none"
18
+ ].join(" ")}
19
+ style={{
20
+ top: `${y}px`,
21
+ left: `${x}px`,
22
+ }}
23
+ >
24
+ {actions.map((action, i) =>
25
+ <div
26
+ key={action}
27
+ className={[
28
+ `flex items-center justify-center px-2 py-1 cursor-pointer`
29
+ ].join(" ")}>
30
+ <div
31
+ className={[
32
+ `transition-all duration-150`,
33
+ isVisible ? "opacity-100 scale-100" : "scale-0 opacity-0 pointer-events-none",
34
+ `flex items-center justify-center rounded-full h-8 px-4`,
35
+ `hover:bg-gray-50 bg-gray-100 hover:border-gray-800 border-gray-300 border`,
36
+ `rounded-2xl text-gray-800 text-md`,
37
+ ].join(" ")}>
38
+ {action}
39
+ </div>
40
+ </div>)}
41
+ </div>
42
+ )
43
+ }
src/components/renderer/scene-tooltip.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ export function SceneTooltip({
4
+ children,
5
+ isVisible,
6
+ x,
7
+ y,
8
+ }: {
9
+ children: ReactNode
10
+ isVisible: boolean
11
+ x: number
12
+ y: number
13
+ }) {
14
+ return (
15
+ <div className={[
16
+ `z-10 fixed flex flex-col space-y-2 w-24 h-16 px-2`,
17
+ `translate-x-[-50%] translate-y-[-40px]`,
18
+ isVisible ? "cursor-pointer" : "",
19
+ "pointer-events-none"
20
+ ].join(" ")}
21
+ style={{
22
+ top: `${y}px`,
23
+ left: `${x}px`,
24
+ }}
25
+ >
26
+ <div
27
+ className={[
28
+ `transition-all duration-150`,
29
+ isVisible ? "opacity-100 scale-100" : "scale-0 opacity-0 pointer-events-none",
30
+ `flex items-center justify-center rounded-full h-8 px-4`,
31
+ `text-gray-50 text-xl`,
32
+ `cursor-pointer capitalize`
33
+ ].join(" ")}
34
+ style={{
35
+ textShadow: "#000 0px 0px 1px, #000 0px 0px 1px, #000 0px 0px 1px"
36
+ }}>
37
+ {children}
38
+ </div>
39
+ </div>
40
+ )
41
+ }