awacke1 commited on
Commit
9e3fe96
ยท
verified ยท
1 Parent(s): aa5ba8e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -73
app.py CHANGED
@@ -16,6 +16,7 @@ import asyncio
16
  from datetime import datetime
17
  from PyPDF2 import PdfReader
18
  import threading
 
19
  from PIL import Image
20
  from streamlit_javascript import st_javascript
21
 
@@ -105,7 +106,7 @@ def generate_filename(content, username, extension):
105
  def initialize_history_file():
106
  ensure_dir(SAVED_WORLDS_DIR)
107
  if not os.path.exists(WORLD_STATE_FILE):
108
- initial_state = {"objects": {}, "players": {}}
109
  with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
110
  json.dump(initial_state, f, indent=2)
111
 
@@ -113,10 +114,15 @@ def read_history_file():
113
  initialize_history_file()
114
  try:
115
  with open(WORLD_STATE_FILE, 'r', encoding='utf-8') as f:
116
- return json.load(f)
 
 
 
 
 
117
  except Exception as e:
118
  print(f"Error reading history file: {e}")
119
- return {"objects": {}, "players": {}}
120
 
121
  def write_history_file(state):
122
  with state_lock:
@@ -149,26 +155,57 @@ def update_player_state(username, position=None):
149
  players[username]["last_action_timestamp"] = time.time()
150
  state["players"] = players
151
  write_history_file(state)
 
 
152
  return state
153
 
154
- def add_object_to_state(obj_data, username):
155
- state = read_history_file()
156
- state = prune_inactive_players(state)
157
- state["objects"][obj_data["obj_id"]] = obj_data
 
 
 
 
 
 
 
 
 
158
  write_history_file(state)
159
- log_action(username, "place", obj_data)
160
- return state
161
 
162
- def remove_object_from_state(obj_id, username):
 
 
 
 
 
163
  state = read_history_file()
164
  state = prune_inactive_players(state)
165
- if obj_id in state["objects"]:
166
- obj_data = state["objects"][obj_id]
167
- del state["objects"][obj_id]
168
- write_history_file(state)
169
- log_action(username, "delete", {"obj_id": obj_id})
170
- return state, obj_data
171
- return state, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  def log_action(username, action_type, data):
174
  timestamp = get_current_time_str()
@@ -192,6 +229,96 @@ def log_action(username, action_type, data):
192
  except Exception as e:
193
  print(f"Error writing to player history log {player_log_file}: {e}")
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  # ==============================================================================
196
  # JavaScript Message Handling
197
  # ==============================================================================
@@ -210,16 +337,15 @@ def handle_js_messages():
210
  payload = data.get("payload", {})
211
  username = payload.get("username", st.session_state.username)
212
  if action == "place_object":
213
- state = add_object_to_state(payload["object_data"], username)
214
  st.session_state.world_state = state
215
  st.rerun()
216
  elif action == "delete_object":
217
- state, obj_data = remove_object_from_state(payload["obj_id"], username)
218
  st.session_state.world_state = state
219
  st.rerun()
220
  elif action == "move_player":
221
  state = update_player_state(username, payload["position"])
222
- log_action(username, "move", payload["position"])
223
  st.session_state.world_state = state
224
  st.rerun()
225
  except json.JSONDecodeError:
@@ -253,6 +379,7 @@ def init_session_state():
253
  'audio_cache': {},
254
  'tts_voice': "en-US-AriaNeural",
255
  'chat_history': [],
 
256
  'enable_audio': True,
257
  'download_link_cache': {},
258
  'username': None,
@@ -261,7 +388,7 @@ def init_session_state():
261
  'selected_object': 'None',
262
  'paste_image_base64': "",
263
  'new_world_name': "MyWorld",
264
- 'world_state': {"objects": {}, "players": {}}
265
  }
266
  for k, v in defaults.items():
267
  if k not in st.session_state:
@@ -270,9 +397,17 @@ def init_session_state():
270
  st.session_state.audio_cache = {}
271
  if not isinstance(st.session_state.download_link_cache, dict):
272
  st.session_state.download_link_cache = {}
 
 
273
  if 'username' not in st.session_state or not st.session_state.username:
274
- st.session_state.username = random.choice(list(FUN_USERNAMES.keys()))
 
275
  save_username(st.session_state.username)
 
 
 
 
 
276
  update_player_state(st.session_state.username)
277
 
278
  # ==============================================================================
@@ -324,34 +459,6 @@ def get_download_link(file_path, file_type="md"):
324
  return f"<small>Err</small>"
325
  return st.session_state.download_link_cache.get(cache_key, "<small>CacheErr</small>")
326
 
327
- async def async_edge_tts_generate(text, voice, username):
328
- if not text:
329
- return None
330
- cache_key = hashlib.md5(f"{text[:150]}_{voice}".encode()).hexdigest()
331
- if 'audio_cache' not in st.session_state:
332
- st.session_state.audio_cache = {}
333
- cached_path = st.session_state.audio_cache.get(cache_key)
334
- if cached_path and os.path.exists(cached_path):
335
- return cached_path
336
- text_cleaned = clean_text_for_tts(text)
337
- if not text_cleaned or text_cleaned == "No text":
338
- return None
339
- filename_base = generate_filename(text_cleaned, username, "mp3")
340
- save_path = os.path.join(AUDIO_DIR, filename_base)
341
- ensure_dir(AUDIO_DIR)
342
- try:
343
- communicate = edge_tts.Communicate(text_cleaned, voice)
344
- await communicate.save(save_path)
345
- if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
346
- st.session_state.audio_cache[cache_key] = save_path
347
- return save_path
348
- else:
349
- print(f"Audio file {save_path} failed generation.")
350
- return None
351
- except Exception as e:
352
- print(f"Edge TTS Error: {e}")
353
- return None
354
-
355
  def play_and_download_audio(file_path):
356
  if file_path and os.path.exists(file_path):
357
  try:
@@ -361,24 +468,6 @@ def play_and_download_audio(file_path):
361
  except Exception as e:
362
  st.error(f"Audio display error for {os.path.basename(file_path)}: {e}")
363
 
364
- async def save_chat_entry(username, message, voice, is_markdown=False):
365
- if not message.strip():
366
- return None, None
367
- timestamp_str = get_current_time_str()
368
- entry = f"[{timestamp_str}] {username} ({voice}): {message}" if not is_markdown else f"[{timestamp_str}] {username} ({voice}):\n```markdown\n{message}\n```"
369
- md_filename_base = generate_filename(message, username, "md")
370
- md_file_path = os.path.join(CHAT_DIR, md_filename_base)
371
- md_file = create_file(entry, username, "md", save_path=md_file_path)
372
- if 'chat_history' not in st.session_state:
373
- st.session_state.chat_history = []
374
- st.session_state.chat_history.append(entry)
375
- audio_file = None
376
- if st.session_state.get('enable_audio', True):
377
- tts_message = message
378
- audio_file = await async_edge_tts_generate(tts_message, voice, username)
379
- log_action(username, "chat", {"message": message})
380
- return md_file, audio_file
381
-
382
  async def load_chat_history():
383
  if 'chat_history' not in st.session_state:
384
  st.session_state.chat_history = []
@@ -607,6 +696,7 @@ def render_sidebar():
607
  st.header("๐Ÿ’พ World State")
608
  st.caption("Manage the shared world state.")
609
 
 
610
  state = read_history_file()
611
  players = state.get("players", {})
612
  st.subheader("Active Players")
@@ -616,6 +706,57 @@ def render_sidebar():
616
  minutes_ago = (current_time - last_action) / 60
617
  st.write(f"{username}: Last active {minutes_ago:.1f} minutes ago")
618
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  st.markdown("---")
620
  st.header("๐Ÿ—๏ธ Build Tools")
621
  st.caption("Select an object to place.")
@@ -658,6 +799,7 @@ def render_sidebar():
658
  if st.session_state.selected_object != name:
659
  st.session_state.selected_object = name
660
  update_player_state(st.session_state.username)
 
661
  st.rerun()
662
  col_idx += 1
663
  st.markdown("---")
@@ -665,6 +807,7 @@ def render_sidebar():
665
  if st.session_state.selected_object != 'None':
666
  st.session_state.selected_object = 'None'
667
  update_player_state(st.session_state.username)
 
668
  st.rerun()
669
 
670
  st.markdown("---")
@@ -687,6 +830,7 @@ def render_sidebar():
687
  st.session_state.tts_voice = FUN_USERNAMES[new_username]
688
  save_username(st.session_state.username)
689
  update_player_state(st.session_state.username)
 
690
  st.rerun()
691
  st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
692
 
@@ -698,8 +842,7 @@ def render_main_content():
698
  with tab_world:
699
  st.header("Shared 3D World")
700
  st.caption("Click to place objects with the selected tool, or click to move player. Right-click to delete. State is saved in history.json.")
701
- state = read_history_file()
702
- st.session_state.world_state = state
703
  html_file_path = 'index.html'
704
  try:
705
  with open(html_file_path, 'r', encoding='utf-8') as f:
@@ -740,7 +883,7 @@ def render_main_content():
740
  if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
741
  st.session_state.last_message = message_to_send
742
  voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
743
- asyncio.run(save_chat_entry(st.session_state.username, message_to_send, voice))
744
  update_player_state(st.session_state.username)
745
  st.session_state.message_counter = st.session_state.get('message_counter', 0) + 1
746
  st.rerun()
@@ -752,9 +895,10 @@ def render_main_content():
752
  st.header("๐Ÿ“‚ Files & Settings")
753
  st.subheader("๐Ÿ’พ World State Management")
754
  if st.button("Clear World State", key="clear_world_state"):
755
- state = {"objects": {}, "players": {}}
756
  write_history_file(state)
757
  st.session_state.world_state = state
 
758
  st.success("World state cleared!")
759
  st.rerun()
760
 
@@ -764,7 +908,7 @@ def render_main_content():
764
  col_zip1, col_zip2, col_zip3 = st.columns(3)
765
  with col_zip1:
766
  if st.button("Zip Worlds"):
767
- create_zip_of_files([WORLD_STATE_FILE], "Worlds")
768
  with col_zip2:
769
  if st.button("Zip Chats"):
770
  create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")
 
16
  from datetime import datetime
17
  from PyPDF2 import PdfReader
18
  import threading
19
+ import pandas as pd
20
  from PIL import Image
21
  from streamlit_javascript import st_javascript
22
 
 
106
  def initialize_history_file():
107
  ensure_dir(SAVED_WORLDS_DIR)
108
  if not os.path.exists(WORLD_STATE_FILE):
109
+ initial_state = {"objects": {}, "players": {}, "action_history": []}
110
  with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
111
  json.dump(initial_state, f, indent=2)
112
 
 
114
  initialize_history_file()
115
  try:
116
  with open(WORLD_STATE_FILE, 'r', encoding='utf-8') as f:
117
+ state = json.load(f)
118
+ # Ensure all expected keys exist
119
+ state.setdefault("objects", {})
120
+ state.setdefault("players", {})
121
+ state.setdefault("action_history", [])
122
+ return state
123
  except Exception as e:
124
  print(f"Error reading history file: {e}")
125
+ return {"objects": {}, "players": {}, "action_history": []}
126
 
127
  def write_history_file(state):
128
  with state_lock:
 
155
  players[username]["last_action_timestamp"] = time.time()
156
  state["players"] = players
157
  write_history_file(state)
158
+ if position:
159
+ update_action_history(username, "move", {"position": position}, state)
160
  return state
161
 
162
+ # ๐Ÿ“Š Update Action History: Records actions in session state and history file
163
+ def update_action_history(username, action_type, data, state):
164
+ timestamp = get_current_time_str()
165
+ action_entry = {
166
+ "timestamp": timestamp,
167
+ "username": username,
168
+ "action": action_type,
169
+ "data": data
170
+ }
171
+ if 'action_history' not in st.session_state:
172
+ st.session_state.action_history = []
173
+ st.session_state.action_history.append(action_entry)
174
+ state["action_history"].append(action_entry)
175
  write_history_file(state)
 
 
176
 
177
+ # ๐Ÿ› ๏ธ Persist World Objects: Validates and saves object data to history.json
178
+ def persist_world_objects(obj_data, username, action_type):
179
+ if not obj_data or not isinstance(obj_data, dict) or 'obj_id' not in obj_data:
180
+ print(f"Invalid object data for {action_type}: {obj_data}")
181
+ return read_history_file()
182
+
183
  state = read_history_file()
184
  state = prune_inactive_players(state)
185
+
186
+ if action_type == "place":
187
+ if 'type' not in obj_data or obj_data['type'] not in PRIMITIVE_MAP.values():
188
+ print(f"Invalid object type: {obj_data.get('type', 'None')}")
189
+ return state
190
+ state["objects"][obj_data['obj_id']] = obj_data
191
+ update_action_history(username, "place", {
192
+ "obj_id": obj_data['obj_id'],
193
+ "type": obj_data['type'],
194
+ "position": obj_data['position']
195
+ }, state)
196
+ elif action_type == "delete":
197
+ if obj_data['obj_id'] in state["objects"]:
198
+ obj_info = state["objects"][obj_data['obj_id']]
199
+ del state["objects"][obj_data['obj_id']]
200
+ update_action_history(username, "delete", {
201
+ "obj_id": obj_data['obj_id'],
202
+ "type": obj_info['type'],
203
+ "position": obj_info['position']
204
+ }, state)
205
+
206
+ write_history_file(state)
207
+ log_action(username, action_type, obj_data)
208
+ return state
209
 
210
  def log_action(username, action_type, data):
211
  timestamp = get_current_time_str()
 
229
  except Exception as e:
230
  print(f"Error writing to player history log {player_log_file}: {e}")
231
 
232
+ # ๐Ÿ“ฉ Save and Log Chat: Saves chat message to chat_logs and logs to history_logs
233
+ async def save_and_log_chat(username, message, voice):
234
+ if not message.strip():
235
+ print("Empty chat message, skipping save.")
236
+ return None, None
237
+
238
+ timestamp_str = get_current_time_str()
239
+ entry = f"[{timestamp_str}] {username} ({voice}): {message}"
240
+ md_filename_base = generate_filename(message, username, "md")
241
+ md_file_path = os.path.join(CHAT_DIR, md_filename_base)
242
+
243
+ try:
244
+ ensure_dir(CHAT_DIR)
245
+ with open(md_file_path, 'w', encoding='utf-8') as f:
246
+ f.write(entry)
247
+ except Exception as e:
248
+ print(f"Error saving chat to {md_file_path}: {e}")
249
+ return None, None
250
+
251
+ if 'chat_history' not in st.session_state:
252
+ st.session_state.chat_history = []
253
+ st.session_state.chat_history.append(entry)
254
+
255
+ audio_file = None
256
+ if st.session_state.get('enable_audio', True):
257
+ text_cleaned = clean_text_for_tts(message)
258
+ if text_cleaned and text_cleaned != "No text":
259
+ filename_base = generate_filename(text_cleaned, username, "mp3")
260
+ save_path = os.path.join(AUDIO_DIR, filename_base)
261
+ ensure_dir(AUDIO_DIR)
262
+ try:
263
+ communicate = edge_tts.Communicate(text_cleaned, voice)
264
+ await communicate.save(save_path)
265
+ if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
266
+ audio_file = save_path
267
+ else:
268
+ print(f"Audio file {save_path} failed generation.")
269
+ except Exception as e:
270
+ print(f"Edge TTS Error: {e}")
271
+
272
+ state = read_history_file()
273
+ update_action_history(username, "chat", {"message": message}, state)
274
+ return md_file_path, audio_file
275
+
276
+ # ๐Ÿ’พ Save World State: Saves current state to a named file
277
+ def save_world_state(world_name):
278
+ if not world_name.strip():
279
+ st.error("World name cannot be empty.")
280
+ return False
281
+ clean_name = clean_filename_part(world_name)
282
+ timestamp = get_current_time_str()
283
+ filename = f"world_{clean_name}_{timestamp}.json"
284
+ save_path = os.path.join(SAVED_WORLDS_DIR, filename)
285
+
286
+ state = read_history_file()
287
+ try:
288
+ with open(save_path, 'w', encoding='utf-8') as f:
289
+ json.dump(state, f, indent=2)
290
+ print(f"Saved world state to {save_path}")
291
+ st.success(f"Saved world as {filename}")
292
+ return True
293
+ except Exception as e:
294
+ print(f"Error saving world state to {save_path}: {e}")
295
+ st.error(f"Failed to save world: {e}")
296
+ return False
297
+
298
+ # ๐Ÿ“‚ Load World State: Loads a saved state from a file
299
+ def load_world_state(filename):
300
+ load_path = os.path.join(SAVED_WORLDS_DIR, filename)
301
+ if not os.path.exists(load_path):
302
+ st.error(f"World file not found: {filename}")
303
+ return False
304
+ try:
305
+ with open(load_path, 'r', encoding='utf-8') as f:
306
+ state = json.load(f)
307
+ state.setdefault("objects", {})
308
+ state.setdefault("players", {})
309
+ state.setdefault("action_history", [])
310
+ write_history_file(state)
311
+ st.session_state.world_state = state
312
+ st.session_state.action_history = state["action_history"]
313
+ print(f"Loaded world state from {load_path}")
314
+ st.success(f"Loaded world {filename}")
315
+ st.rerun()
316
+ return True
317
+ except Exception as e:
318
+ print(f"Error loading world state from {load_path}: {e}")
319
+ st.error(f"Failed to load world: {e}")
320
+ return False
321
+
322
  # ==============================================================================
323
  # JavaScript Message Handling
324
  # ==============================================================================
 
337
  payload = data.get("payload", {})
338
  username = payload.get("username", st.session_state.username)
339
  if action == "place_object":
340
+ state = persist_world_objects(payload["object_data"], username, "place")
341
  st.session_state.world_state = state
342
  st.rerun()
343
  elif action == "delete_object":
344
+ state = persist_world_objects({"obj_id": payload["obj_id"]}, username, "delete")
345
  st.session_state.world_state = state
346
  st.rerun()
347
  elif action == "move_player":
348
  state = update_player_state(username, payload["position"])
 
349
  st.session_state.world_state = state
350
  st.rerun()
351
  except json.JSONDecodeError:
 
379
  'audio_cache': {},
380
  'tts_voice': "en-US-AriaNeural",
381
  'chat_history': [],
382
+ 'action_history': [],
383
  'enable_audio': True,
384
  'download_link_cache': {},
385
  'username': None,
 
388
  'selected_object': 'None',
389
  'paste_image_base64': "",
390
  'new_world_name': "MyWorld",
391
+ 'world_state': {"objects": {}, "players": {}, "action_history": []}
392
  }
393
  for k, v in defaults.items():
394
  if k not in st.session_state:
 
397
  st.session_state.audio_cache = {}
398
  if not isinstance(st.session_state.download_link_cache, dict):
399
  st.session_state.download_link_cache = {}
400
+ if not isinstance(st.session_state.action_history, list):
401
+ st.session_state.action_history = []
402
  if 'username' not in st.session_state or not st.session_state.username:
403
+ saved_username = load_username()
404
+ st.session_state.username = saved_username if saved_username else random.choice(list(FUN_USERNAMES.keys()))
405
  save_username(st.session_state.username)
406
+
407
+ # Load history on startup
408
+ state = read_history_file()
409
+ st.session_state.world_state = state
410
+ st.session_state.action_history = state.get("action_history", [])
411
  update_player_state(st.session_state.username)
412
 
413
  # ==============================================================================
 
459
  return f"<small>Err</small>"
460
  return st.session_state.download_link_cache.get(cache_key, "<small>CacheErr</small>")
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  def play_and_download_audio(file_path):
463
  if file_path and os.path.exists(file_path):
464
  try:
 
468
  except Exception as e:
469
  st.error(f"Audio display error for {os.path.basename(file_path)}: {e}")
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  async def load_chat_history():
472
  if 'chat_history' not in st.session_state:
473
  st.session_state.chat_history = []
 
696
  st.header("๐Ÿ’พ World State")
697
  st.caption("Manage the shared world state.")
698
 
699
+ # Active Players
700
  state = read_history_file()
701
  players = state.get("players", {})
702
  st.subheader("Active Players")
 
706
  minutes_ago = (current_time - last_action) / 60
707
  st.write(f"{username}: Last active {minutes_ago:.1f} minutes ago")
708
 
709
+ # Action History Dataset
710
+ st.subheader("Action History")
711
+ history_data = []
712
+ for entry in st.session_state.action_history:
713
+ data = entry["data"]
714
+ if entry["action"] in ["place", "delete"]:
715
+ pos = data.get("position", {})
716
+ position_str = f"({pos.get('x', 0):.1f}, {pos.get('y', 0):.1f}, {pos.get('z', 0):.1f})"
717
+ history_data.append({
718
+ "Time": entry["timestamp"],
719
+ "Player": entry["username"],
720
+ "Action": entry["action"].capitalize(),
721
+ "Object Type": data.get("type", "N/A"),
722
+ "Position": position_str
723
+ })
724
+ elif entry["action"] == "move":
725
+ pos = data.get("position", {})
726
+ position_str = f"({pos.get('x', 0):.1f}, {pos.get('y', 0):.1f}, {pos.get('z', 0):.1f})"
727
+ history_data.append({
728
+ "Time": entry["timestamp"],
729
+ "Player": entry["username"],
730
+ "Action": "Move",
731
+ "Object Type": "Player",
732
+ "Position": position_str
733
+ })
734
+ elif entry["action"] == "chat":
735
+ history_data.append({
736
+ "Time": entry["timestamp"],
737
+ "Player": entry["username"],
738
+ "Action": "Chat",
739
+ "Object Type": "Message",
740
+ "Position": data.get("message", "N/A")[:50]
741
+ })
742
+ if history_data:
743
+ st.dataframe(pd.DataFrame(history_data), height=200, use_container_width=True)
744
+ else:
745
+ st.caption("No actions recorded yet.")
746
+
747
+ # Save World
748
+ st.subheader("Save World")
749
+ world_name = st.text_input("World Name", value="MyWorld", key="save_world_name")
750
+ if st.button("๐Ÿ’พ Save World", key="save_world"):
751
+ save_world_state(world_name)
752
+
753
+ # Load World
754
+ st.subheader("Load World")
755
+ saved_worlds = [os.path.basename(f) for f in glob.glob(os.path.join(SAVED_WORLDS_DIR, "world_*.json"))]
756
+ selected_world = st.selectbox("Select Saved World", ["None"] + saved_worlds, key="load_world_select")
757
+ if selected_world != "None" and st.button("๐Ÿ“‚ Load World", key="load_world"):
758
+ load_world_state(selected_world)
759
+
760
  st.markdown("---")
761
  st.header("๐Ÿ—๏ธ Build Tools")
762
  st.caption("Select an object to place.")
 
799
  if st.session_state.selected_object != name:
800
  st.session_state.selected_object = name
801
  update_player_state(st.session_state.username)
802
+ update_action_history(st.session_state.username, "tool_change", {"tool": name}, read_history_file())
803
  st.rerun()
804
  col_idx += 1
805
  st.markdown("---")
 
807
  if st.session_state.selected_object != 'None':
808
  st.session_state.selected_object = 'None'
809
  update_player_state(st.session_state.username)
810
+ update_action_history(st.session_state.username, "tool_change", {"tool": "None"}, read_history_file())
811
  st.rerun()
812
 
813
  st.markdown("---")
 
830
  st.session_state.tts_voice = FUN_USERNAMES[new_username]
831
  save_username(st.session_state.username)
832
  update_player_state(st.session_state.username)
833
+ update_action_history(st.session_state.username, "rename", {"old_username": old_username, "new_username": new_username}, state)
834
  st.rerun()
835
  st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
836
 
 
842
  with tab_world:
843
  st.header("Shared 3D World")
844
  st.caption("Click to place objects with the selected tool, or click to move player. Right-click to delete. State is saved in history.json.")
845
+ state = st.session_state.world_state
 
846
  html_file_path = 'index.html'
847
  try:
848
  with open(html_file_path, 'r', encoding='utf-8') as f:
 
883
  if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
884
  st.session_state.last_message = message_to_send
885
  voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
886
+ asyncio.run(save_and_log_chat(st.session_state.username, message_to_send, voice))
887
  update_player_state(st.session_state.username)
888
  st.session_state.message_counter = st.session_state.get('message_counter', 0) + 1
889
  st.rerun()
 
895
  st.header("๐Ÿ“‚ Files & Settings")
896
  st.subheader("๐Ÿ’พ World State Management")
897
  if st.button("Clear World State", key="clear_world_state"):
898
+ state = {"objects": {}, "players": {}, "action_history": []}
899
  write_history_file(state)
900
  st.session_state.world_state = state
901
+ st.session_state.action_history = []
902
  st.success("World state cleared!")
903
  st.rerun()
904
 
 
908
  col_zip1, col_zip2, col_zip3 = st.columns(3)
909
  with col_zip1:
910
  if st.button("Zip Worlds"):
911
+ create_zip_of_files([WORLD_STATE_FILE] + glob.glob(os.path.join(SAVED_WORLDS_DIR, "world_*.json")), "Worlds")
912
  with col_zip2:
913
  if st.button("Zip Chats"):
914
  create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")