awacke1 commited on
Commit
d4007b0
Β·
verified Β·
1 Parent(s): b599290

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -578
app.py CHANGED
@@ -1,10 +1,6 @@
1
  import streamlit as st
2
- import asyncio
3
- import websockets
4
- import uuid
5
- from datetime import datetime
6
  import os
7
- import random
8
  import time
9
  import hashlib
10
  import glob
@@ -15,16 +11,9 @@ import edge_tts
15
  import nest_asyncio
16
  import re
17
  import pytz
18
- import shutil
19
  from PyPDF2 import PdfReader
20
  import threading
21
- import json
22
- import zipfile
23
- from dotenv import load_dotenv
24
- from streamlit_marquee import streamlit_marquee
25
- from collections import defaultdict, Counter
26
- import pandas as pd
27
- from streamlit_js_eval import streamlit_js_eval
28
  from PIL import Image
29
 
30
  # ==============================================================================
@@ -61,7 +50,8 @@ SAVED_WORLDS_DIR = "saved_worlds"
61
  HISTORY_LOG_DIR = "history_logs"
62
  PLOT_WIDTH = 50.0
63
  PLOT_DEPTH = 50.0
64
- WORLD_STATE_FILE_MD_PREFIX = "🌍_"
 
65
 
66
  FILE_EMOJIS = {"md": "πŸ“", "mp3": "🎡", "png": "πŸ–ΌοΈ", "mp4": "πŸŽ₯", "zip": "πŸ“¦", "json": "πŸ“„"}
67
 
@@ -75,9 +65,7 @@ PRIMITIVE_MAP = {
75
  for d in [CHAT_DIR, AUDIO_DIR, AUDIO_CACHE_DIR, SAVED_WORLDS_DIR, HISTORY_LOG_DIR]:
76
  os.makedirs(d, exist_ok=True)
77
 
78
- world_objects_lock = threading.Lock()
79
- world_objects = defaultdict(dict)
80
- connected_clients = set()
81
 
82
  # ==============================================================================
83
  # Utility Functions
@@ -98,21 +86,6 @@ def clean_filename_part(text, max_len=30):
98
  text = re.sub(r'[^\w\-.]', '', text)
99
  return text[:max_len]
100
 
101
- def run_async(async_func, *args, **kwargs):
102
- try:
103
- loop = asyncio.get_running_loop()
104
- return loop.create_task(async_func(*args, **kwargs))
105
- except RuntimeError:
106
- print(f"Warning: Running async func {async_func.__name__} in new event loop.")
107
- try:
108
- return asyncio.run(async_func(*args, **kwargs))
109
- except Exception as e:
110
- print(f"Error running async func {async_func.__name__} in new loop: {e}")
111
- return None
112
- except Exception as e:
113
- print(f"Error scheduling async task {async_func.__name__}: {e}")
114
- return None
115
-
116
  def ensure_dir(dir_path):
117
  os.makedirs(dir_path, exist_ok=True)
118
 
@@ -122,121 +95,70 @@ def generate_filename(content, username, extension):
122
  clean_username = clean_filename_part(username)
123
  return f"{clean_username}_{timestamp}_{content_hash}.{extension}"
124
 
125
- def format_timestamp_prefix(prefix):
126
- return f"{prefix}_{get_current_time_str()}"
127
-
128
  # ==============================================================================
129
- # World State File Handling (Markdown + JSON)
130
  # ==============================================================================
131
 
132
- def generate_world_save_filename(name="World"):
133
- timestamp = get_current_time_str()
134
- clean_name = clean_filename_part(name)
135
- rand_hash = hashlib.md5(str(time.time()).encode() + name.encode()).hexdigest()[:6]
136
- return f"{WORLD_STATE_FILE_MD_PREFIX}{clean_name}_{timestamp}_{rand_hash}.md"
137
-
138
- def parse_world_filename(filename):
139
- basename = os.path.basename(filename)
140
- if basename.startswith(WORLD_STATE_FILE_MD_PREFIX) and basename.endswith(".md"):
141
- core_name = basename[len(WORLD_STATE_FILE_MD_PREFIX):-3]
142
- parts = core_name.split('_')
143
- if len(parts) >= 3:
144
- timestamp_str = parts[-2]
145
- name_parts = parts[:-2]
146
- name = "_".join(name_parts) if name_parts else "Untitled"
147
- dt_obj = None
148
- try:
149
- dt_obj = datetime.strptime(timestamp_str, '%Y%m%d_%H%M%S')
150
- dt_obj = pytz.utc.localize(dt_obj)
151
- except (ValueError, pytz.exceptions.AmbiguousTimeError, pytz.exceptions.NonExistentTimeError):
152
- dt_obj = None
153
- return {"name": name.replace('_', ' '), "timestamp": timestamp_str, "dt": dt_obj, "filename": filename}
154
- dt_fallback = None
155
- try:
156
- mtime = os.path.getmtime(filename)
157
- dt_fallback = datetime.fromtimestamp(mtime, tz=pytz.utc)
158
- except Exception:
159
- pass
160
- return {"name": basename.replace('.md',''), "timestamp": "Unknown", "dt": dt_fallback, "filename": filename}
161
-
162
- def save_world_state_to_md(target_filename_base):
163
- global world_objects
164
- save_path = os.path.join(SAVED_WORLDS_DIR, target_filename_base)
165
- print(f"Acquiring lock to save world state to: {save_path}...")
166
- success = False
167
- with world_objects_lock:
168
- world_data_dict = dict(world_objects)
169
- print(f"Saving {len(world_data_dict)} objects...")
170
- parsed_info = parse_world_filename(save_path)
171
- timestamp_save = get_current_time_str()
172
- md_content = f"""# World State: {parsed_info['name']}
173
- * **File Saved:** {timestamp_save} (UTC)
174
- * **Source Timestamp:** {parsed_info['timestamp']}
175
- * **Objects:** {len(world_data_dict)}
176
-
177
- ```json
178
- {json.dumps(world_data_dict, indent=2)}
179
- ```"""
180
- try:
181
- ensure_dir(SAVED_WORLDS_DIR)
182
- with open(save_path, 'w', encoding='utf-8') as f:
183
- f.write(md_content)
184
- print(f"World state saved successfully to {target_filename_base}")
185
- success = True
186
- username = st.session_state.get('username', 'Anonymous')
187
- clean_username = clean_filename_part(username)
188
- player_filename_base = f"{clean_username}_{target_filename_base}"
189
- player_save_path = os.path.join(SAVED_WORLDS_DIR, player_filename_base)
190
- with open(player_save_path, 'w', encoding='utf-8') as f:
191
- f.write(md_content)
192
- print(f"Player-specific world state saved to {player_filename_base}")
193
- except Exception as e:
194
- print(f"Error saving world state to {save_path}: {e}")
195
- return success
196
-
197
- def load_world_state_from_md(filename_base):
198
- global world_objects
199
- load_path = os.path.join(SAVED_WORLDS_DIR, filename_base)
200
- print(f"Loading world state from MD file: {load_path}...")
201
- if not os.path.exists(load_path):
202
- st.error(f"World file not found: {filename_base}")
203
- return False
204
- try:
205
- with open(load_path, 'r', encoding='utf-8') as f:
206
- content = f.read()
207
- json_match = re.search(r"```json\s*(\{[\s\S]*?\})\s*```", content, re.IGNORECASE)
208
- if not json_match:
209
- st.error(f"Could not find valid JSON block in {filename_base}")
210
- return False
211
- world_data_dict = json.loads(json_match.group(1))
212
- print(f"Acquiring lock to update world state from {filename_base}...")
213
- with world_objects_lock:
214
- world_objects.clear()
215
- for k, v in world_data_dict.items():
216
- world_objects[str(k)] = v
217
- loaded_count = len(world_objects)
218
- print(f"Loaded {loaded_count} objects from {filename_base}. Lock released.")
219
- st.session_state.current_world_file = filename_base
220
- return True
221
- except json.JSONDecodeError as e:
222
- st.error(f"Invalid JSON found in {filename_base}: {e}")
223
- return False
224
- except Exception as e:
225
- st.error(f"Error loading world state from {filename_base}: {e}")
226
- st.exception(e)
227
- return False
228
 
229
- def get_saved_worlds():
 
230
  try:
231
- ensure_dir(SAVED_WORLDS_DIR)
232
- world_files = glob.glob(os.path.join(SAVED_WORLDS_DIR, "*.md"))
233
- parsed_worlds = [parse_world_filename(f) for f in world_files]
234
- parsed_worlds.sort(key=lambda x: x['dt'] if x['dt'] else datetime.min.replace(tzinfo=pytz.utc), reverse=True)
235
- return parsed_worlds
236
  except Exception as e:
237
- print(f"Error scanning saved worlds: {e}")
238
- st.error(f"Could not scan saved worlds: {e}")
239
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  def log_action(username, action_type, data):
242
  timestamp = get_current_time_str()
@@ -282,26 +204,22 @@ def load_username():
282
 
283
  def init_session_state():
284
  defaults = {
285
- 'server_running_flag': False, 'server_instance': None, 'server_task': None,
286
- 'active_connections': defaultdict(dict), 'last_chat_update': 0, 'message_input': "",
287
- 'audio_cache': {}, 'tts_voice': "en-US-AriaNeural", 'chat_history': [],
288
- 'marquee_settings': {"background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px", "animationDuration": "20s", "width": "100%", "lineHeight": "35px"},
289
- 'enable_audio': True, 'download_link_cache': {}, 'username': None, 'autosend': False,
290
- 'last_message': "", 'timer_start': time.time(), 'last_sent_transcript': "",
291
- 'last_refresh': time.time(), 'auto_refresh': False, 'refresh_rate': 30,
292
- 'selected_object': 'None', 'initial_world_state_loaded': False,
293
- 'current_world_file': None, 'operation_timings': {}, 'performance_metrics': defaultdict(list),
294
- 'paste_image_base64': "", 'new_world_name': "MyWorld"
 
 
295
  }
296
  for k, v in defaults.items():
297
  if k not in st.session_state:
298
  st.session_state[k] = v
299
- if not isinstance(st.session_state.active_connections, defaultdict):
300
- st.session_state.active_connections = defaultdict(dict)
301
- if not isinstance(st.session_state.chat_history, list):
302
- st.session_state.chat_history = []
303
- if not isinstance(st.session_state.marquee_settings, dict):
304
- st.session_state.marquee_settings = defaults['marquee_settings']
305
  if not isinstance(st.session_state.audio_cache, dict):
306
  st.session_state.audio_cache = {}
307
  if not isinstance(st.session_state.download_link_cache, dict):
@@ -309,6 +227,7 @@ def init_session_state():
309
  if 'username' not in st.session_state or not st.session_state.username:
310
  st.session_state.username = random.choice(list(FUN_USERNAMES.keys()))
311
  save_username(st.session_state.username)
 
312
 
313
  # ==============================================================================
314
  # Audio / TTS / Chat / File Handling Helpers
@@ -437,7 +356,7 @@ def create_zip_of_files(files_to_zip, prefix="Archive"):
437
  if not files_to_zip:
438
  st.warning("No files provided to zip.")
439
  return None
440
- timestamp = format_timestamp_prefix(f"Zip_{prefix}")
441
  zip_name = f"{prefix}_{timestamp}.zip"
442
  try:
443
  print(f"Creating zip: {zip_name}...")
@@ -456,7 +375,7 @@ def create_zip_of_files(files_to_zip, prefix="Archive"):
456
  return None
457
 
458
  def delete_files(file_patterns, exclude_files=None):
459
- protected = [STATE_FILE, "app.py", "index.html", "requirements.txt", "README.md"]
460
  if exclude_files:
461
  protected.extend(exclude_files)
462
  deleted_count = 0
@@ -496,7 +415,7 @@ async def save_pasted_image(image, username):
496
  return None
497
  try:
498
  img_hash = hashlib.md5(image.tobytes()).hexdigest()[:8]
499
- timestamp = format_timestamp_prefix(username)
500
  filename = f"{timestamp}_pasted_{img_hash}.png"
501
  filepath = os.path.join(MEDIA_DIR, filename)
502
  image.save(filepath, "PNG")
@@ -632,291 +551,28 @@ def process_pdf_tab(pdf_file, max_pages, voice):
632
  st.error(f"Err read PDF: {pdf_e}")
633
  st.exception(pdf_e)
634
 
635
- # ==============================================================================
636
- # WebSocket Server Logic
637
- # ==============================================================================
638
-
639
- async def register_client(websocket):
640
- client_id = str(websocket.id)
641
- connected_clients.add(client_id)
642
- if 'active_connections' not in st.session_state:
643
- st.session_state.active_connections = defaultdict(dict)
644
- st.session_state.active_connections[client_id] = websocket
645
- print(f"Client registered: {client_id}. Total: {len(connected_clients)}")
646
-
647
- async def unregister_client(websocket):
648
- client_id = str(websocket.id)
649
- connected_clients.discard(client_id)
650
- if 'active_connections' in st.session_state:
651
- st.session_state.active_connections.pop(client_id, None)
652
- print(f"Client unregistered: {client_id}. Remaining: {len(connected_clients)}")
653
-
654
- async def send_safely(websocket, message, client_id):
655
- try:
656
- await websocket.send(message)
657
- except websockets.ConnectionClosed:
658
- print(f"WS Send failed (Closed) client {client_id}")
659
- raise
660
- except RuntimeError as e:
661
- print(f"WS Send failed (Runtime {e}) client {client_id}")
662
- raise
663
- except Exception as e:
664
- print(f"WS Send failed (Other {e}) client {client_id}")
665
- raise
666
-
667
- async def broadcast_message(message, exclude_id=None):
668
- if not connected_clients:
669
- return
670
- tasks = []
671
- current_client_ids = list(connected_clients)
672
- active_connections_copy = st.session_state.active_connections.copy()
673
- for client_id in current_client_ids:
674
- if client_id == exclude_id:
675
- continue
676
- websocket = active_connections_copy.get(client_id)
677
- if websocket:
678
- tasks.append(asyncio.create_task(send_safely(websocket, message, client_id)))
679
- if tasks:
680
- await asyncio.gather(*tasks, return_exceptions=True)
681
-
682
- async def broadcast_world_update():
683
- with world_objects_lock:
684
- current_state_payload = dict(world_objects)
685
- update_msg = json.dumps({"type": "initial_state", "payload": current_state_payload})
686
- print(f"Broadcasting full world update ({len(current_state_payload)} objects)...")
687
- await broadcast_message(update_msg)
688
-
689
- async def websocket_handler(websocket, path):
690
- await register_client(websocket)
691
- client_id = str(websocket.id)
692
- username = st.session_state.get('username', f"User_{client_id[:4]}")
693
- try:
694
- with world_objects_lock:
695
- initial_state_payload = dict(world_objects)
696
- initial_state_msg = json.dumps({"type": "initial_state", "payload": initial_state_payload})
697
- await websocket.send(initial_state_msg)
698
- print(f"Sent initial state ({len(initial_state_payload)} objs) to {client_id}")
699
- await broadcast_message(json.dumps({"type": "user_join", "payload": {"username": username, "id": client_id}}), exclude_id=client_id)
700
- except Exception as e:
701
- print(f"Error initial phase {client_id}: {e}")
702
-
703
- try:
704
- async for message in websocket:
705
- try:
706
- data = json.loads(message)
707
- msg_type = data.get("type")
708
- payload = data.get("payload", {})
709
- sender_username = payload.get("username", username)
710
-
711
- if msg_type == "chat_message":
712
- chat_text = payload.get('message', '')
713
- voice = payload.get('voice', FUN_USERNAMES.get(sender_username, "en-US-AriaNeural"))
714
- run_async(save_chat_entry, sender_username, chat_text, voice)
715
- await broadcast_message(message, exclude_id=client_id)
716
-
717
- elif msg_type == "place_object":
718
- obj_data = payload.get("object_data")
719
- if obj_data and 'obj_id' in obj_data and 'type' in obj_data:
720
- with world_objects_lock:
721
- world_objects[obj_data['obj_id']] = obj_data
722
- broadcast_payload = json.dumps({"type": "object_placed", "payload": {"object_data": obj_data, "username": sender_username}})
723
- await broadcast_message(broadcast_payload, exclude_id=client_id)
724
- current_file = st.session_state.get('current_world_file')
725
- if current_file:
726
- run_async(save_world_state_to_md, current_file)
727
- else:
728
- new_filename = generate_world_save_filename(f"AutoSave_{sender_username}")
729
- if run_async(save_world_state_to_md, new_filename):
730
- st.session_state.current_world_file = new_filename
731
- log_action(sender_username, "place", obj_data)
732
-
733
- elif msg_type == "delete_object":
734
- obj_id = payload.get("obj_id")
735
- removed = False
736
- if obj_id:
737
- with world_objects_lock:
738
- if obj_id in world_objects:
739
- del world_objects[obj_id]
740
- removed = True
741
- if removed:
742
- broadcast_payload = json.dumps({"type": "object_deleted", "payload": {"obj_id": obj_id, "username": sender_username}})
743
- await broadcast_message(broadcast_payload, exclude_id=client_id)
744
- current_file = st.session_state.get('current_world_file')
745
- if current_file:
746
- run_async(save_world_state_to_md, current_file)
747
- else:
748
- new_filename = generate_world_save_filename(f"AutoSave_{sender_username}")
749
- if run_async(save_world_state_to_md, new_filename):
750
- st.session_state.current_world_file = new_filename
751
- log_action(sender_username, "delete", {"obj_id": obj_id})
752
-
753
- elif msg_type == "tool_selected":
754
- tool_type = payload.get("tool_type")
755
- if tool_type in PRIMITIVE_MAP.values() or tool_type == "None":
756
- st.session_state.selected_object = tool_type
757
- print(f"Tool updated to {tool_type} for {sender_username}")
758
- broadcast_payload = json.dumps({"type": "tool_selected", "payload": {"tool_type": tool_type, "username": sender_username}})
759
- await broadcast_message(broadcast_payload, exclude_id=client_id)
760
-
761
- except json.JSONDecodeError:
762
- print(f"WS Invalid JSON from {client_id}: {message[:100]}...")
763
- except Exception as e:
764
- print(f"WS Error processing msg from {client_id}: {e}")
765
- except websockets.ConnectionClosed:
766
- print(f"WS Client disconnected: {client_id} ({username})")
767
- except Exception as e:
768
- print(f"WS Unexpected handler error {client_id}: {e}")
769
- finally:
770
- await broadcast_message(json.dumps({"type": "user_leave", "payload": {"username": username, "id": client_id}}), exclude_id=client_id)
771
- await unregister_client(websocket)
772
-
773
- async def run_websocket_server():
774
- if st.session_state.get('server_running_flag', False):
775
- return
776
- st.session_state['server_running_flag'] = True
777
- print("Attempting start WS server 0.0.0.0:8765...")
778
- stop_event = asyncio.Event()
779
- st.session_state['websocket_stop_event'] = stop_event
780
- server = None
781
- try:
782
- server = await websockets.serve(websocket_handler, "0.0.0.0", 8765)
783
- st.session_state['server_instance'] = server
784
- print(f"WS server started: {server.sockets[0].getsockname()}. Waiting for stop signal...")
785
- await stop_event.wait()
786
- except OSError as e:
787
- print(f"### FAILED START WS SERVER: {e}")
788
- st.session_state['server_running_flag'] = False
789
- except Exception as e:
790
- print(f"### UNEXPECTED WS SERVER ERROR: {e}")
791
- st.session_state['server_running_flag'] = False
792
- finally:
793
- print("WS server task finishing...")
794
- if server:
795
- server.close()
796
- await server.wait_closed()
797
- print("WS server closed.")
798
- st.session_state['server_running_flag'] = False
799
- st.session_state['server_instance'] = None
800
- st.session_state['websocket_stop_event'] = None
801
-
802
- def start_websocket_server_thread():
803
- if st.session_state.get('server_task') and st.session_state.server_task.is_alive():
804
- return
805
- if st.session_state.get('server_running_flag', False):
806
- return
807
- print("Creating/starting new server thread.")
808
- def run_loop():
809
- loop = None
810
- try:
811
- loop = asyncio.get_running_loop()
812
- except RuntimeError:
813
- loop = asyncio.new_event_loop()
814
- asyncio.set_event_loop(loop)
815
- try:
816
- loop.run_until_complete(run_websocket_server())
817
- finally:
818
- if loop and not loop.is_closed():
819
- tasks = asyncio.all_tasks(loop)
820
- for task in tasks:
821
- task.cancel()
822
- try:
823
- loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
824
- except asyncio.CancelledError:
825
- pass
826
- finally:
827
- loop.close()
828
- print("Server thread loop closed.")
829
- else:
830
- print("Server thread loop already closed or None.")
831
- st.session_state.server_task = threading.Thread(target=run_loop, daemon=True)
832
- st.session_state.server_task.start()
833
- time.sleep(1.5)
834
- if not st.session_state.server_task.is_alive():
835
- print("### Server thread failed to stay alive!")
836
-
837
  # ==============================================================================
838
  # Streamlit UI Layout Functions
839
  # ==============================================================================
840
 
841
  def render_sidebar():
842
  with st.sidebar:
843
- st.header("πŸ’Ύ World Versions")
844
- st.caption("Load or save named world states.")
845
-
846
- saved_worlds = get_saved_worlds()
847
- world_options_display = {
848
- os.path.basename(w['filename']): (
849
- f"{w['name']} ({w['timestamp']})" if not w['filename'].startswith(clean_filename_part(st.session_state.get('username', '')))
850
- else f"{w['name']} ({w['timestamp']}) [Your Save]"
851
- ) for w in saved_worlds
852
- }
853
- radio_options_basenames = [None] + [os.path.basename(w['filename']) for w in saved_worlds]
854
- current_selection_basename = st.session_state.get('current_world_file', None)
855
- current_radio_index = 0
856
- if current_selection_basename and current_selection_basename in radio_options_basenames:
857
- try:
858
- current_radio_index = radio_options_basenames.index(current_selection_basename)
859
- except ValueError:
860
- current_radio_index = 0
861
-
862
- selected_basename = st.radio(
863
- "Load World:",
864
- options=radio_options_basenames,
865
- index=current_radio_index,
866
- format_func=lambda x: "Live State (Unsaved)" if x is None else world_options_display.get(x, x),
867
- key="world_selector_radio"
868
- )
869
-
870
- if selected_basename != current_selection_basename:
871
- st.session_state.current_world_file = selected_basename
872
- if selected_basename:
873
- with st.spinner(f"Loading {selected_basename}..."):
874
- if load_world_state_from_md(selected_basename):
875
- run_async(broadcast_world_update)
876
- st.toast("World loaded!", icon="βœ…")
877
- else:
878
- st.error("Failed to load world.")
879
- st.session_state.current_world_file = None
880
- else:
881
- print("Switched to live state.")
882
- st.toast("Switched to Live State.")
883
- st.rerun()
884
-
885
- st.caption("Download:")
886
- cols = st.columns([4, 1])
887
- with cols[0]:
888
- st.write("**Name** (Timestamp)")
889
- with cols[1]:
890
- st.write("**DL**")
891
- display_limit = 10
892
- for i, world_info in enumerate(saved_worlds):
893
- if i >= display_limit and len(saved_worlds) > display_limit + 1:
894
- with st.expander(f"Show {len(saved_worlds)-display_limit} more..."):
895
- for world_info_more in saved_worlds[display_limit:]:
896
- f_basename = os.path.basename(world_info_more['filename'])
897
- f_fullpath = os.path.join(SAVED_WORLDS_DIR, f_basename)
898
- display_name = world_info_more.get('name', f_basename)
899
- timestamp = world_info_more.get('timestamp', 'N/A')
900
- colA, colB = st.columns([4, 1])
901
- with colA:
902
- st.write(f"<small>{display_name} ({timestamp})</small>", unsafe_allow_html=True)
903
- with colB:
904
- st.markdown(get_download_link(f_fullpath, "md"), unsafe_allow_html=True)
905
- break
906
- f_basename = os.path.basename(world_info['filename'])
907
- f_fullpath = os.path.join(SAVED_WORLDS_DIR, f_basename)
908
- display_name = world_info.get('name', f_basename)
909
- timestamp = world_info.get('timestamp', 'N/A')
910
- col1, col2 = st.columns([4, 1])
911
- with col1:
912
- st.write(f"<small>{display_name} ({timestamp})</small>", unsafe_allow_html=True)
913
- with col2:
914
- st.markdown(get_download_link(f_fullpath, "md"), unsafe_allow_html=True)
915
 
916
  st.markdown("---")
917
  st.header("πŸ—οΈ Build Tools")
918
  st.caption("Select an object to place.")
919
-
920
  # CSS for tool buttons
921
  st.markdown("""
922
  <style>
@@ -954,31 +610,13 @@ def render_sidebar():
954
  if cols[col_idx % 5].button(emoji, key=button_key, help=name, type=button_type, use_container_width=True):
955
  if st.session_state.selected_object != name:
956
  st.session_state.selected_object = name
957
- # Send WebSocket message to update tool selection
958
- ws_message = json.dumps({
959
- "type": "tool_selected",
960
- "payload": {"tool_type": name, "username": st.session_state.username}
961
- })
962
- run_async(broadcast_message, ws_message)
963
- # Update JavaScript tool state
964
- run_async(lambda: streamlit_js_eval(
965
- f"updateSelectedObjectType('{name}');",
966
- key=f"update_tool_js_{name}"
967
- ))
968
  col_idx += 1
969
  st.markdown("---")
970
  if st.button("🚫 Clear Tool", key="clear_tool", use_container_width=True):
971
  if st.session_state.selected_object != 'None':
972
  st.session_state.selected_object = 'None'
973
- ws_message = json.dumps({
974
- "type": "tool_selected",
975
- "payload": {"tool_type": "None", "username": st.session_state.username}
976
- })
977
- run_async(broadcast_message, ws_message)
978
- run_async(lambda: streamlit_js_eval(
979
- "updateSelectedObjectType('None');",
980
- key="update_tool_js_none"
981
- ))
982
 
983
  st.markdown("---")
984
  st.header("πŸ—£οΈ Voice & User")
@@ -992,11 +630,14 @@ def render_sidebar():
992
  new_username = st.selectbox("Change Name/Voice", options=username_options, index=current_index, key="username_select", format_func=lambda x: x.split(" ")[0])
993
  if new_username != st.session_state.username:
994
  old_username = st.session_state.username
995
- change_msg = json.dumps({"type":"user_rename", "payload": {"old_username": old_username, "new_username": new_username}})
996
- run_async(broadcast_message, change_msg)
 
 
997
  st.session_state.username = new_username
998
  st.session_state.tts_voice = FUN_USERNAMES[new_username]
999
  save_username(st.session_state.username)
 
1000
  st.rerun()
1001
  st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
1002
 
@@ -1007,33 +648,19 @@ def render_main_content():
1007
 
1008
  with tab_world:
1009
  st.header("Shared 3D World")
1010
- st.caption("Click to place objects with the selected tool. Right-click to delete. Changes are shared live!")
1011
- current_file_basename = st.session_state.get('current_world_file', None)
1012
- if current_file_basename:
1013
- parsed = parse_world_filename(os.path.join(SAVED_WORLDS_DIR, current_file_basename))
1014
- st.info(f"Current World: **{parsed['name']}** (`{current_file_basename}`)")
1015
- else:
1016
- st.info("Live State Active (Unsaved changes only persist if saved)")
1017
-
1018
  html_file_path = 'index.html'
1019
  try:
1020
  with open(html_file_path, 'r', encoding='utf-8') as f:
1021
  html_template = f.read()
1022
- ws_url = "ws://localhost:8765"
1023
- try:
1024
- from streamlit.web.server.server import Server
1025
- session_info = Server.get_current()._get_session_info(st.runtime.scriptrunner.get_script_run_ctx().session_id)
1026
- server_host = session_info.ws.stream.request.host.split(':')[0]
1027
- ws_url = f"ws://{server_host}:8765"
1028
- except Exception as e:
1029
- print(f"WS URL detection failed ({e}), using localhost.")
1030
  js_injection_script = f"""<script>
1031
  window.USERNAME = {json.dumps(st.session_state.username)};
1032
- window.WEBSOCKET_URL = {json.dumps(ws_url)};
1033
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
1034
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
1035
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
1036
- console.log("Streamlit State Injected:", {{ username: window.USERNAME, websocketUrl: window.WEBSOCKET_URL, selectedObject: window.SELECTED_OBJECT_TYPE }});
 
1037
  </script>"""
1038
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
1039
  components.html(html_content_with_state, height=700, scrolling=False)
@@ -1045,12 +672,11 @@ def render_main_content():
1045
 
1046
  with tab_chat:
1047
  st.header(f"{START_ROOM} Chat")
1048
- chat_history_task = run_async(load_chat_history)
1049
- chat_history = chat_history_task.result() if chat_history_task else st.session_state.chat_history
1050
  chat_container = st.container(height=500)
1051
  with chat_container:
1052
- if chat_history:
1053
- st.markdown("----\n".join(reversed(chat_history[-50:])))
1054
  else:
1055
  st.caption("No chat messages yet.")
1056
 
@@ -1063,9 +689,8 @@ def render_main_content():
1063
  if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
1064
  st.session_state.last_message = message_to_send
1065
  voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
1066
- ws_message = json.dumps({"type": "chat_message", "payload": {"username": st.session_state.username, "message": message_to_send, "voice": voice}})
1067
- run_async(broadcast_message, ws_message)
1068
- run_async(save_chat_entry, st.session_state.username, message_to_send, voice)
1069
  st.session_state.message_input = ""
1070
  st.rerun()
1071
  elif send_button_clicked:
@@ -1084,80 +709,19 @@ def render_main_content():
1084
  with tab_files:
1085
  st.header("πŸ“‚ Files & Settings")
1086
  st.subheader("πŸ’Ύ World State Management")
1087
- current_file_basename = st.session_state.get('current_world_file', None)
1088
- if current_file_basename:
1089
- parsed = parse_world_filename(os.path.join(SAVED_WORLDS_DIR, current_file_basename))
1090
- save_label = f"Save Changes to '{parsed['name']}'"
1091
- if st.button(save_label, key="save_current_world", help=f"Overwrite '{current_file_basename}'"):
1092
- with st.spinner(f"Overwriting {current_file_basename}..."):
1093
- if save_world_state_to_md(current_file_basename):
1094
- st.success("Current world saved!")
1095
- else:
1096
- st.error("Failed to save world state.")
1097
- else:
1098
- st.info("Load a world from the sidebar to enable saving changes to it.")
1099
-
1100
- st.subheader("Save As New Version")
1101
- new_name_files = st.text_input("New World Name:", key="new_world_name_files", value=st.session_state.get('new_world_name', 'MyWorld'))
1102
- if st.button("πŸ’Ύ Save Live State as New Version", key="save_new_version_files"):
1103
- if new_name_files.strip():
1104
- new_filename_base = generate_world_save_filename(new_name_files)
1105
- with st.spinner(f"Saving new version '{new_name_files}'..."):
1106
- if save_world_state_to_md(new_filename_base):
1107
- st.success(f"Saved as {new_filename_base}")
1108
- st.session_state.current_world_file = new_filename_base
1109
- st.session_state.new_world_name = "MyWorld"
1110
- st.rerun()
1111
- else:
1112
- st.error("Failed to save new version.")
1113
- else:
1114
- st.warning("Please enter a name.")
1115
-
1116
- st.subheader("βš™οΈ Server Status")
1117
- col_ws, col_clients = st.columns(2)
1118
- with col_ws:
1119
- server_alive = st.session_state.get('server_task') and st.session_state.server_task.is_alive()
1120
- ws_status = "Running" if server_alive else "Stopped"
1121
- st.metric("WebSocket Server", ws_status)
1122
- if not server_alive and st.button("Restart Server Thread", key="restart_ws"):
1123
- start_websocket_server_thread()
1124
- st.rerun()
1125
- with col_clients:
1126
- st.metric("Connected Clients", len(connected_clients))
1127
-
1128
- st.subheader("πŸ—‘οΈ Delete Files")
1129
- st.warning("Deletion is permanent!", icon="⚠️")
1130
- col_del1, col_del2, col_del3, col_del4 = st.columns(4)
1131
- with col_del1:
1132
- if st.button("πŸ—‘οΈ Chats", key="del_chat_md"):
1133
- delete_files([os.path.join(CHAT_DIR, "*.md")])
1134
- st.session_state.chat_history = []
1135
- st.rerun()
1136
- with col_del2:
1137
- if st.button("πŸ—‘οΈ Audio", key="del_audio_mp3"):
1138
- delete_files([os.path.join(AUDIO_DIR, "*.mp3"), os.path.join(AUDIO_CACHE_DIR, "*.mp3")])
1139
- st.session_state.audio_cache = {}
1140
- st.rerun()
1141
- with col_del3:
1142
- if st.button("πŸ—‘οΈ Worlds", key="del_worlds_md"):
1143
- delete_files([os.path.join(SAVED_WORLDS_DIR, f"{WORLD_STATE_FILE_MD_PREFIX}*.md")])
1144
- st.session_state.current_world_file = None
1145
- st.rerun()
1146
- with col_del4:
1147
- if st.button("πŸ—‘οΈ All Gen", key="del_all_gen"):
1148
- delete_files([os.path.join(CHAT_DIR, "*.md"), os.path.join(AUDIO_DIR, "*.mp3"), os.path.join(AUDIO_CACHE_DIR, "*.mp3"), os.path.join(SAVED_WORLDS_DIR, "*.md"), "*.zip"])
1149
- st.session_state.chat_history = []
1150
- st.session_state.audio_cache = {}
1151
- st.session_state.current_world_file = None
1152
- st.rerun()
1153
 
1154
  st.subheader("πŸ“¦ Download Archives")
1155
- zip_files = sorted(glob.glob(os.path.join(MEDIA_DIR,"*.zip")), key=os.path.getmtime, reverse=True)
1156
  if zip_files:
1157
  col_zip1, col_zip2, col_zip3 = st.columns(3)
1158
  with col_zip1:
1159
  if st.button("Zip Worlds"):
1160
- create_zip_of_files([w['filename'] for w in get_saved_worlds()], "Worlds")
1161
  with col_zip2:
1162
  if st.button("Zip Chats"):
1163
  create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")
@@ -1174,35 +738,7 @@ def render_main_content():
1174
  # Main Execution Logic
1175
  # ==============================================================================
1176
 
1177
- def initialize_world():
1178
- if not st.session_state.get('initial_world_state_loaded', False):
1179
- print("Performing initial world load for session...")
1180
- saved_worlds = get_saved_worlds()
1181
- loaded_successfully = False
1182
- if saved_worlds:
1183
- latest_world_file_basename = os.path.basename(saved_worlds[0]['filename'])
1184
- print(f"Loading most recent world on startup: {latest_world_file_basename}")
1185
- if load_world_state_from_md(latest_world_file_basename):
1186
- loaded_successfully = True
1187
- else:
1188
- print("Failed to load most recent world, starting empty.")
1189
- else:
1190
- print("No saved worlds found, starting with empty state.")
1191
- if not loaded_successfully:
1192
- with world_objects_lock:
1193
- world_objects.clear()
1194
- st.session_state.current_world_file = None
1195
- st.session_state.initial_world_state_loaded = True
1196
- print("Initial world load process complete.")
1197
-
1198
  if __name__ == "__main__":
1199
  init_session_state()
1200
- server_thread = st.session_state.get('server_task')
1201
- server_alive = server_thread is not None and server_thread.is_alive()
1202
- if not st.session_state.get('server_running_flag', False) and not server_alive:
1203
- start_websocket_server_thread()
1204
- elif server_alive and not st.session_state.get('server_running_flag', False):
1205
- st.session_state.server_running_flag = True
1206
- initialize_world()
1207
  render_sidebar()
1208
  render_main_content()
 
1
  import streamlit as st
 
 
 
 
2
  import os
3
+ import json
4
  import time
5
  import hashlib
6
  import glob
 
11
  import nest_asyncio
12
  import re
13
  import pytz
14
+ from datetime import datetime
15
  from PyPDF2 import PdfReader
16
  import threading
 
 
 
 
 
 
 
17
  from PIL import Image
18
 
19
  # ==============================================================================
 
50
  HISTORY_LOG_DIR = "history_logs"
51
  PLOT_WIDTH = 50.0
52
  PLOT_DEPTH = 50.0
53
+ WORLD_STATE_FILE = os.path.join(SAVED_WORLDS_DIR, "history.json")
54
+ PLAYER_TIMEOUT_SECONDS = 15 * 60 # 15 minutes
55
 
56
  FILE_EMOJIS = {"md": "πŸ“", "mp3": "🎡", "png": "πŸ–ΌοΈ", "mp4": "πŸŽ₯", "zip": "πŸ“¦", "json": "πŸ“„"}
57
 
 
65
  for d in [CHAT_DIR, AUDIO_DIR, AUDIO_CACHE_DIR, SAVED_WORLDS_DIR, HISTORY_LOG_DIR]:
66
  os.makedirs(d, exist_ok=True)
67
 
68
+ state_lock = threading.Lock()
 
 
69
 
70
  # ==============================================================================
71
  # Utility Functions
 
86
  text = re.sub(r'[^\w\-.]', '', text)
87
  return text[:max_len]
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def ensure_dir(dir_path):
90
  os.makedirs(dir_path, exist_ok=True)
91
 
 
95
  clean_username = clean_filename_part(username)
96
  return f"{clean_username}_{timestamp}_{content_hash}.{extension}"
97
 
 
 
 
98
  # ==============================================================================
99
+ # History File Management
100
  # ==============================================================================
101
 
102
+ def initialize_history_file():
103
+ ensure_dir(SAVED_WORLDS_DIR)
104
+ if not os.path.exists(WORLD_STATE_FILE):
105
+ initial_state = {"objects": {}, "players": {}}
106
+ with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
107
+ json.dump(initial_state, f, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ def read_history_file():
110
+ initialize_history_file()
111
  try:
112
+ with open(WORLD_STATE_FILE, 'r', encoding='utf-8') as f:
113
+ return json.load(f)
 
 
 
114
  except Exception as e:
115
+ print(f"Error reading history file: {e}")
116
+ return {"objects": {}, "players": {}}
117
+
118
+ def write_history_file(state):
119
+ with state_lock:
120
+ try:
121
+ with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
122
+ json.dump(state, f, indent=2)
123
+ except Exception as e:
124
+ print(f"Error writing history file: {e}")
125
+
126
+ def prune_inactive_players(state):
127
+ current_time = time.time()
128
+ players = state.get("players", {})
129
+ updated_players = {}
130
+ for username, data in players.items():
131
+ last_action = data.get("last_action_timestamp", 0)
132
+ if current_time - last_action <= PLAYER_TIMEOUT_SECONDS:
133
+ updated_players[username] = data
134
+ state["players"] = updated_players
135
+ return state
136
+
137
+ def update_player_state(username, position=None):
138
+ state = read_history_file()
139
+ state = prune_inactive_players(state)
140
+ players = state.get("players", {})
141
+ if username not in players:
142
+ players[username] = {"position": position or {"x": PLOT_WIDTH / 2, "y": 0.5, "z": PLOT_DEPTH / 2}, "last_action_timestamp": time.time()}
143
+ else:
144
+ if position:
145
+ players[username]["position"] = position
146
+ players[username]["last_action_timestamp"] = time.time()
147
+ state["players"] = players
148
+ write_history_file(state)
149
+
150
+ def add_object_to_state(obj_data):
151
+ state = read_history_file()
152
+ state = prune_inactive_players(state)
153
+ state["objects"][obj_data["obj_id"]] = obj_data
154
+ write_history_file(state)
155
+
156
+ def remove_object_from_state(obj_id):
157
+ state = read_history_file()
158
+ state = prune_inactive_players(state)
159
+ if obj_id in state["objects"]:
160
+ del state["objects"][obj_id]
161
+ write_history_file(state)
162
 
163
  def log_action(username, action_type, data):
164
  timestamp = get_current_time_str()
 
204
 
205
  def init_session_state():
206
  defaults = {
207
+ 'message_input': "",
208
+ 'audio_cache': {},
209
+ 'tts_voice': "en-US-AriaNeural",
210
+ 'chat_history': [],
211
+ 'enable_audio': True,
212
+ 'download_link_cache': {},
213
+ 'username': None,
214
+ 'autosend': False,
215
+ 'last_message': "",
216
+ 'selected_object': 'None',
217
+ 'paste_image_base64': "",
218
+ 'new_world_name': "MyWorld"
219
  }
220
  for k, v in defaults.items():
221
  if k not in st.session_state:
222
  st.session_state[k] = v
 
 
 
 
 
 
223
  if not isinstance(st.session_state.audio_cache, dict):
224
  st.session_state.audio_cache = {}
225
  if not isinstance(st.session_state.download_link_cache, dict):
 
227
  if 'username' not in st.session_state or not st.session_state.username:
228
  st.session_state.username = random.choice(list(FUN_USERNAMES.keys()))
229
  save_username(st.session_state.username)
230
+ update_player_state(st.session_state.username)
231
 
232
  # ==============================================================================
233
  # Audio / TTS / Chat / File Handling Helpers
 
356
  if not files_to_zip:
357
  st.warning("No files provided to zip.")
358
  return None
359
+ timestamp = get_current_time_str()
360
  zip_name = f"{prefix}_{timestamp}.zip"
361
  try:
362
  print(f"Creating zip: {zip_name}...")
 
375
  return None
376
 
377
  def delete_files(file_patterns, exclude_files=None):
378
+ protected = [STATE_FILE, "app.py", "index.html", "requirements.txt", "README.md", WORLD_STATE_FILE]
379
  if exclude_files:
380
  protected.extend(exclude_files)
381
  deleted_count = 0
 
415
  return None
416
  try:
417
  img_hash = hashlib.md5(image.tobytes()).hexdigest()[:8]
418
+ timestamp = get_current_time_str()
419
  filename = f"{timestamp}_pasted_{img_hash}.png"
420
  filepath = os.path.join(MEDIA_DIR, filename)
421
  image.save(filepath, "PNG")
 
551
  st.error(f"Err read PDF: {pdf_e}")
552
  st.exception(pdf_e)
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  # ==============================================================================
555
  # Streamlit UI Layout Functions
556
  # ==============================================================================
557
 
558
  def render_sidebar():
559
  with st.sidebar:
560
+ st.header("πŸ’Ύ World State")
561
+ st.caption("Manage the shared world state.")
562
+
563
+ state = read_history_file()
564
+ players = state.get("players", {})
565
+ st.subheader("Active Players")
566
+ current_time = time.time()
567
+ for username, data in players.items():
568
+ last_action = data.get("last_action_timestamp", 0)
569
+ minutes_ago = (current_time - last_action) / 60
570
+ st.write(f"{username}: Last active {minutes_ago:.1f} minutes ago")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  st.markdown("---")
573
  st.header("πŸ—οΈ Build Tools")
574
  st.caption("Select an object to place.")
575
+
576
  # CSS for tool buttons
577
  st.markdown("""
578
  <style>
 
610
  if cols[col_idx % 5].button(emoji, key=button_key, help=name, type=button_type, use_container_width=True):
611
  if st.session_state.selected_object != name:
612
  st.session_state.selected_object = name
613
+ update_player_state(st.session_state.username)
 
 
 
 
 
 
 
 
 
 
614
  col_idx += 1
615
  st.markdown("---")
616
  if st.button("🚫 Clear Tool", key="clear_tool", use_container_width=True):
617
  if st.session_state.selected_object != 'None':
618
  st.session_state.selected_object = 'None'
619
+ update_player_state(st.session_state.username)
 
 
 
 
 
 
 
 
620
 
621
  st.markdown("---")
622
  st.header("πŸ—£οΈ Voice & User")
 
630
  new_username = st.selectbox("Change Name/Voice", options=username_options, index=current_index, key="username_select", format_func=lambda x: x.split(" ")[0])
631
  if new_username != st.session_state.username:
632
  old_username = st.session_state.username
633
+ state = read_history_file()
634
+ if old_username in state["players"]:
635
+ state["players"][new_username] = state["players"].pop(old_username)
636
+ write_history_file(state)
637
  st.session_state.username = new_username
638
  st.session_state.tts_voice = FUN_USERNAMES[new_username]
639
  save_username(st.session_state.username)
640
+ update_player_state(st.session_state.username)
641
  st.rerun()
642
  st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
643
 
 
648
 
649
  with tab_world:
650
  st.header("Shared 3D World")
651
+ st.caption("Click to place objects with the selected tool. Right-click to delete. State is saved in history.json.")
652
+ state = read_history_file()
 
 
 
 
 
 
653
  html_file_path = 'index.html'
654
  try:
655
  with open(html_file_path, 'r', encoding='utf-8') as f:
656
  html_template = f.read()
 
 
 
 
 
 
 
 
657
  js_injection_script = f"""<script>
658
  window.USERNAME = {json.dumps(st.session_state.username)};
 
659
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
660
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
661
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
662
+ window.WORLD_STATE = {json.dumps(state)};
663
+ console.log("Streamlit State Injected:", {{ username: window.USERNAME, selectedObject: window.SELECTED_OBJECT_TYPE, worldState: window.WORLD_STATE }});
664
  </script>"""
665
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
666
  components.html(html_content_with_state, height=700, scrolling=False)
 
672
 
673
  with tab_chat:
674
  st.header(f"{START_ROOM} Chat")
675
+ chat_history_task = asyncio.run(load_chat_history())
 
676
  chat_container = st.container(height=500)
677
  with chat_container:
678
+ if chat_history_task:
679
+ st.markdown("----\n".join(reversed(chat_history_task[-50:])))
680
  else:
681
  st.caption("No chat messages yet.")
682
 
 
689
  if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
690
  st.session_state.last_message = message_to_send
691
  voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
692
+ asyncio.run(save_chat_entry(st.session_state.username, message_to_send, voice))
693
+ update_player_state(st.session_state.username)
 
694
  st.session_state.message_input = ""
695
  st.rerun()
696
  elif send_button_clicked:
 
709
  with tab_files:
710
  st.header("πŸ“‚ Files & Settings")
711
  st.subheader("πŸ’Ύ World State Management")
712
+ if st.button("Clear World State", key="clear_world_state"):
713
+ state = {"objects": {}, "players": {}}
714
+ write_history_file(state)
715
+ st.success("World state cleared!")
716
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
 
718
  st.subheader("πŸ“¦ Download Archives")
719
+ zip_files = sorted(glob.glob(os.path.join(MEDIA_DIR, "*.zip")), key=os.path.getmtime, reverse=True)
720
  if zip_files:
721
  col_zip1, col_zip2, col_zip3 = st.columns(3)
722
  with col_zip1:
723
  if st.button("Zip Worlds"):
724
+ create_zip_of_files([WORLD_STATE_FILE], "Worlds")
725
  with col_zip2:
726
  if st.button("Zip Chats"):
727
  create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")
 
738
  # Main Execution Logic
739
  # ==============================================================================
740
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  if __name__ == "__main__":
742
  init_session_state()
 
 
 
 
 
 
 
743
  render_sidebar()
744
  render_main_content()