Spaces:
Runtime error
Runtime error
Commit
Β·
e62f50c
1
Parent(s):
65b89b5
update
Browse files- package-lock.json +0 -0
- package.json +2 -0
- src/app/main.tsx +2 -2
- src/app/queries/getActionnables.ts +28 -29
- src/app/queries/getBackground.ts +10 -8
- src/app/queries/getDialogue.ts +2 -2
- src/components/renderer/index.tsx +113 -22
- src/components/renderer/scene-menu.tsx +43 -0
- src/components/renderer/scene-tooltip.tsx +41 -0
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">"{item.name}"</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,
|
319 |
-
newEventString = `User is looking at the scene,
|
320 |
}
|
321 |
} else if (event === "HoveringActionnable") {
|
322 |
if (item) {
|
|
|
315 |
newEvent = <>π You are holding <span className="font-bold">"{item.name}"</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 |
-
|
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
|
31 |
-
Here is the original scene in which the user was located at first, which will inform you about the general
|
32 |
: ""
|
33 |
|
34 |
const prompt = createLlamaPrompt([
|
35 |
{
|
36 |
role: "system",
|
37 |
content: [
|
38 |
-
`You are the
|
39 |
basePrompt,
|
40 |
-
`You are going to receive new information about the current
|
41 |
-
`Please write a photo caption for the next plausible scene
|
42 |
-
`
|
43 |
-
`
|
|
|
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 |
-
|
|
|
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
|
35 |
-
Here is the original
|
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 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
|
133 |
-
|
134 |
-
|
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 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
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 |
-
? "
|
208 |
: "w-full",
|
209 |
|
210 |
isLoading
|
211 |
? "cursor-wait"
|
212 |
: actionnable
|
213 |
-
?
|
|
|
|
|
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 |
+
}
|