ArturoNereu commited on
Commit
0beba7d
·
1 Parent(s): 09bc1ec

Added option to create a cube at position the user is looking at

Browse files
Files changed (3) hide show
  1. app.py +29 -6
  2. chat_client.py +20 -2
  3. frontend/game_viewer.html +69 -0
app.py CHANGED
@@ -151,7 +151,7 @@ def get_gpt_client():
151
  return gpt_client
152
 
153
 
154
- def chat_response(message, history):
155
  """Handle chat messages using GPT with tool calling"""
156
  global current_scene_id
157
 
@@ -184,7 +184,8 @@ I'm an AI assistant that can help you build 3D scenes using natural language.
184
 
185
  try:
186
  client = get_gpt_client()
187
- response, action_data = client.chat(message)
 
188
  return response, action_data
189
  except Exception as e:
190
  import traceback
@@ -334,6 +335,16 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
334
  if (event.data && event.data.action === 'objectDeselected') {
335
  window.selectedObjectId = null;
336
  }
 
 
 
 
 
 
 
 
 
 
337
  });
338
 
339
  // Initialize toast container on load
@@ -363,6 +374,10 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
363
  # The flow: bot() returns JSON → action_data updates → .change() fires → JS sends postMessage to iframe
364
  action_data = gr.Textbox(value="", elem_id="action-data", visible=True, elem_classes=["hidden-action"])
365
 
 
 
 
 
366
  # Main container - side by side layout: Chat (left) | Viewer (right)
367
  with gr.Row(elem_id="main-container", equal_height=True):
368
  # Left column: Chat interface (scale=1 = ~25% width)
@@ -401,7 +416,7 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
401
  history.append({"role": "user", "content": user_message})
402
  return "", history
403
 
404
- def bot(history):
405
  """Generate bot response"""
406
  # Gradio 6: content can be a string or list of content blocks
407
  content = history[-1]["content"]
@@ -414,8 +429,16 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
414
  else:
415
  user_message = content
416
 
417
- # Process command (history not used by chat_response)
418
- bot_message, action_result = chat_response(user_message, [])
 
 
 
 
 
 
 
 
419
  history.append({"role": "assistant", "content": bot_message})
420
 
421
  # Handle action_result
@@ -507,7 +530,7 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
507
  queue=False
508
  ).then(
509
  bot,
510
- [chatbot],
511
  [chatbot, viewer, action_data]
512
  )
513
 
 
151
  return gpt_client
152
 
153
 
154
+ def chat_response(message, history, crosshair_position=None):
155
  """Handle chat messages using GPT with tool calling"""
156
  global current_scene_id
157
 
 
184
 
185
  try:
186
  client = get_gpt_client()
187
+ # Pass crosshair position to chat client for context-aware object placement
188
+ response, action_data = client.chat(message, crosshair_position=crosshair_position)
189
  return response, action_data
190
  except Exception as e:
191
  import traceback
 
335
  if (event.data && event.data.action === 'objectDeselected') {
336
  window.selectedObjectId = null;
337
  }
338
+
339
+ if (event.data && event.data.action === 'crosshairPosition') {
340
+ window.crosshairPosition = event.data.data;
341
+ // Update hidden Gradio textbox so Python can read it
342
+ const crosshairInput = document.querySelector('#crosshair-pos textarea');
343
+ if (crosshairInput) {
344
+ crosshairInput.value = event.data.data ? JSON.stringify(event.data.data) : '';
345
+ crosshairInput.dispatchEvent(new Event('input', { bubbles: true }));
346
+ }
347
+ }
348
  });
349
 
350
  // Initialize toast container on load
 
374
  # The flow: bot() returns JSON → action_data updates → .change() fires → JS sends postMessage to iframe
375
  action_data = gr.Textbox(value="", elem_id="action-data", visible=True, elem_classes=["hidden-action"])
376
 
377
+ # Hidden textbox to receive crosshair position from JavaScript
378
+ # JS updates this via: document.querySelector('#crosshair-pos textarea').value = JSON.stringify(pos)
379
+ crosshair_pos = gr.Textbox(value="", elem_id="crosshair-pos", visible=True, elem_classes=["hidden-action"])
380
+
381
  # Main container - side by side layout: Chat (left) | Viewer (right)
382
  with gr.Row(elem_id="main-container", equal_height=True):
383
  # Left column: Chat interface (scale=1 = ~25% width)
 
416
  history.append({"role": "user", "content": user_message})
417
  return "", history
418
 
419
+ def bot(history, crosshair_position):
420
  """Generate bot response"""
421
  # Gradio 6: content can be a string or list of content blocks
422
  content = history[-1]["content"]
 
429
  else:
430
  user_message = content
431
 
432
+ # Parse crosshair position from JSON string
433
+ crosshair_pos_dict = None
434
+ if crosshair_position:
435
+ try:
436
+ crosshair_pos_dict = json.loads(crosshair_position)
437
+ except:
438
+ pass
439
+
440
+ # Process command with crosshair context
441
+ bot_message, action_result = chat_response(user_message, [], crosshair_pos_dict)
442
  history.append({"role": "assistant", "content": bot_message})
443
 
444
  # Handle action_result
 
530
  queue=False
531
  ).then(
532
  bot,
533
+ [chatbot, crosshair_pos],
534
  [chatbot, viewer, action_data]
535
  )
536
 
chat_client.py CHANGED
@@ -767,10 +767,15 @@ Be concise but helpful. After making changes, briefly confirm what was done."""
767
  else:
768
  return {"error": f"Unknown tool: {name}"}
769
 
770
- def chat(self, user_message: str) -> tuple[str, Optional[Dict[str, Any]]]:
771
  """
772
  Process a user message and return the response.
773
 
 
 
 
 
 
774
  Returns:
775
  tuple: (response_text, action_data)
776
  - response_text: The assistant's response
@@ -782,8 +787,21 @@ Be concise but helpful. After making changes, briefly confirm what was done."""
782
  "content": user_message
783
  })
784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  # Build messages with system prompt
786
- messages = [{"role": "system", "content": self.system_prompt}] + self.conversation_history
787
 
788
  # Track actions for frontend
789
  actions = []
 
767
  else:
768
  return {"error": f"Unknown tool: {name}"}
769
 
770
+ def chat(self, user_message: str, crosshair_position: Optional[Dict[str, float]] = None) -> tuple[str, Optional[Dict[str, Any]]]:
771
  """
772
  Process a user message and return the response.
773
 
774
+ Args:
775
+ user_message: The user's message
776
+ crosshair_position: Optional dict with x, y, z coordinates where the crosshair
777
+ is pointing on the floor. Used for context-aware object placement.
778
+
779
  Returns:
780
  tuple: (response_text, action_data)
781
  - response_text: The assistant's response
 
787
  "content": user_message
788
  })
789
 
790
+ # Build system prompt with crosshair context
791
+ system_prompt = self.system_prompt
792
+ if crosshair_position:
793
+ system_prompt += f"""
794
+
795
+ IMPORTANT - Current crosshair position:
796
+ The player is looking at position ({crosshair_position['x']}, {crosshair_position['y']}, {crosshair_position['z']}) on the floor.
797
+ When the user asks to create/add an object WITHOUT specifying a position (e.g., "add a cube", "create a sphere"),
798
+ place it at the crosshair position: x={crosshair_position['x']}, y={crosshair_position['y']}, z={crosshair_position['z']}.
799
+ The y coordinate should be adjusted based on object size (e.g., y=0.5 for a cube of scale 1 so it sits on the floor).
800
+
801
+ If the user DOES specify a position (e.g., "add a cube at 0, 0, 0"), use their specified position instead."""
802
+
803
  # Build messages with system prompt
804
+ messages = [{"role": "system", "content": system_prompt}] + self.conversation_history
805
 
806
  # Track actions for frontend
807
  actions = []
frontend/game_viewer.html CHANGED
@@ -993,6 +993,9 @@
993
  // Update looked-at object (FPS mode only)
994
  updateLookedAtObject();
995
 
 
 
 
996
  // Render using composer (for outlines) instead of direct renderer
997
  if (composer) {
998
  composer.render();
@@ -1004,6 +1007,72 @@
1004
  if (stats) stats.end();
1005
  }
1006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  // ==================== Scene Control Functions ====================
1008
 
1009
  /**
 
993
  // Update looked-at object (FPS mode only)
994
  updateLookedAtObject();
995
 
996
+ // Update crosshair floor intersection and send to parent
997
+ updateCrosshairPosition();
998
+
999
  // Render using composer (for outlines) instead of direct renderer
1000
  if (composer) {
1001
  composer.render();
 
1007
  if (stats) stats.end();
1008
  }
1009
 
1010
+ // ==================== Crosshair Position Tracking ====================
1011
+
1012
+ let lastCrosshairPosition = null;
1013
+ let crosshairUpdateThrottle = 0;
1014
+
1015
+ /**
1016
+ * Update crosshair floor intersection and send to parent window
1017
+ * This allows the chat to know where to place objects when user doesn't specify position
1018
+ */
1019
+ function updateCrosshairPosition() {
1020
+ // Throttle updates to every 10 frames (~6 times per second at 60fps)
1021
+ crosshairUpdateThrottle++;
1022
+ if (crosshairUpdateThrottle < 10) return;
1023
+ crosshairUpdateThrottle = 0;
1024
+
1025
+ // Only track in FPS mode
1026
+ if (controlMode !== 'fps') {
1027
+ if (lastCrosshairPosition !== null) {
1028
+ lastCrosshairPosition = null;
1029
+ window.parent.postMessage({
1030
+ action: 'crosshairPosition',
1031
+ data: null
1032
+ }, '*');
1033
+ }
1034
+ return;
1035
+ }
1036
+
1037
+ // Raycast from camera center (crosshair)
1038
+ const crosshairRaycaster = new THREE.Raycaster();
1039
+ crosshairRaycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
1040
+
1041
+ // Find intersection with ground
1042
+ const groundObjects = scene.children.filter(obj => obj.userData.isGround);
1043
+ const intersects = crosshairRaycaster.intersectObjects(groundObjects);
1044
+
1045
+ if (intersects.length > 0) {
1046
+ const point = intersects[0].point;
1047
+ const newPosition = {
1048
+ x: Math.round(point.x * 100) / 100,
1049
+ y: Math.round(point.y * 100) / 100,
1050
+ z: Math.round(point.z * 100) / 100
1051
+ };
1052
+
1053
+ // Only send update if position changed significantly
1054
+ if (!lastCrosshairPosition ||
1055
+ Math.abs(newPosition.x - lastCrosshairPosition.x) > 0.1 ||
1056
+ Math.abs(newPosition.z - lastCrosshairPosition.z) > 0.1) {
1057
+
1058
+ lastCrosshairPosition = newPosition;
1059
+ window.parent.postMessage({
1060
+ action: 'crosshairPosition',
1061
+ data: newPosition
1062
+ }, '*');
1063
+ }
1064
+ } else {
1065
+ // No intersection - looking at sky or beyond floor
1066
+ if (lastCrosshairPosition !== null) {
1067
+ lastCrosshairPosition = null;
1068
+ window.parent.postMessage({
1069
+ action: 'crosshairPosition',
1070
+ data: null
1071
+ }, '*');
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
  // ==================== Scene Control Functions ====================
1077
 
1078
  /**