awacke1 commited on
Commit
239b4d8
ยท
verified ยท
1 Parent(s): f672794

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -59
app.py CHANGED
@@ -17,6 +17,8 @@ import json
17
  import streamlit.components.v1 as components
18
  from gradio_client import Client
19
  from streamlit_marquee import streamlit_marquee
 
 
20
 
21
  # Patch asyncio for nesting
22
  nest_asyncio.apply()
@@ -43,6 +45,13 @@ CHARACTERS = {
43
  }
44
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
45
 
 
 
 
 
 
 
 
46
  # Directories
47
  for d in ["chat_logs", "audio_logs"]:
48
  os.makedirs(d, exist_ok=True)
@@ -152,12 +161,12 @@ def init_session_state():
152
  'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
153
  'speech_processed': False, 'players': {}, 'last_update': time.time(),
154
  'update_interval': 20, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
155
- 'move_right': False, 'move_up': False, 'move_down': False
 
156
  }
157
  for k, v in defaults.items():
158
  if k not in st.session_state:
159
  st.session_state[k] = v
160
- # Ensure username is initialized immediately
161
  if st.session_state.username is None:
162
  saved_username = load_username()
163
  if saved_username and saved_username in CHARACTERS:
@@ -167,7 +176,6 @@ def init_session_state():
167
  asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Begins the Rocky Mountain Quest!", CHARACTERS[st.session_state.username]["voice"]))
168
  save_username(st.session_state.username)
169
 
170
- # Call init_session_state immediately to ensure username is set
171
  init_session_state()
172
 
173
  # ArXiv Integration
@@ -189,12 +197,23 @@ async def websocket_handler(websocket, path):
189
  st.session_state.active_connections[room_id] = {}
190
  st.session_state.active_connections[room_id][client_id] = websocket
191
  username = st.session_state.username
192
- st.session_state.players[client_id] = {
193
- "username": username,
194
- "x": random.uniform(-20, 20),
195
- "z": random.uniform(-50, 50),
196
- "color": CHARACTERS[username]["color"]
197
- }
 
 
 
 
 
 
 
 
 
 
 
198
  await save_chat_entry(username, f"๐Ÿ—บ๏ธ Joins the quest at {START_LOCATION}!", CHARACTERS[username]["voice"])
199
 
200
  try:
@@ -206,6 +225,15 @@ async def websocket_handler(websocket, path):
206
  _, x, z = content.split(":")
207
  st.session_state.players[client_id]["x"] = float(x)
208
  st.session_state.players[client_id]["z"] = float(z)
 
 
 
 
 
 
 
 
 
209
  else:
210
  await save_chat_entry(username, content, voice)
211
  await perform_arxiv_search(content, username)
@@ -213,6 +241,8 @@ async def websocket_handler(websocket, path):
213
  await save_chat_entry(username, "๐Ÿƒ Leaves the quest!", CHARACTERS[username]["voice"])
214
  if client_id in st.session_state.players:
215
  del st.session_state.players[client_id]
 
 
216
  finally:
217
  if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
218
  del st.session_state.active_connections[room_id][client_id]
@@ -220,12 +250,21 @@ async def websocket_handler(websocket, path):
220
  async def periodic_update():
221
  while True:
222
  if st.session_state.active_connections.get("quest"):
 
223
  player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!"
224
  message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
225
  player_data = json.dumps(list(st.session_state.players.values()))
226
  await broadcast_message(f"System|{message}", "quest")
227
  await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
228
- await save_chat_entry("System", message, "en-US-AriaNeural")
 
 
 
 
 
 
 
 
229
  await asyncio.sleep(st.session_state.update_interval)
230
 
231
  async def run_websocket_server():
@@ -240,8 +279,8 @@ def start_websocket_server():
240
  asyncio.set_event_loop(loop)
241
  loop.run_until_complete(run_websocket_server())
242
 
243
- # Game HTML with Map (Corrected JavaScript)
244
- html_code = f"""
245
  <!DOCTYPE html>
246
  <html lang="en">
247
  <head>
@@ -296,7 +335,7 @@ html_code = f"""
296
 
297
  let players = {{}};
298
  const playerMeshes = {{}};
299
- let xPos = 0; // Local JavaScript variables for position
300
  let zPos = 0;
301
 
302
  function updatePlayers(playerData) {{
@@ -341,7 +380,7 @@ html_code = f"""
341
  if (data.startsWith('MAP_UPDATE:')) {{
342
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
343
  updatePlayers(playerData);
344
- }} else {{
345
  const [sender, message] = data.split('|');
346
  const chatBox = document.getElementById('chatBox');
347
  chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
@@ -359,10 +398,165 @@ html_code = f"""
359
  </html>
360
  """
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  # Main Game Loop
363
  def main():
364
- st.title(f"๐ŸŽฎ {GAME_NAME}")
365
- st.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
  # Countdown Timer
368
  elapsed = time.time() - st.session_state.last_update
@@ -372,49 +566,6 @@ def main():
372
  st.session_state.last_update = time.time()
373
  st.rerun()
374
 
375
- # Voice Input Component
376
- mycomponent = components.declare_component("speech_component", path="./speech_component")
377
- val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
378
- if val and val != st.session_state.last_transcript:
379
- val_stripped = val.strip().replace('\n', ' ')
380
- if val_stripped:
381
- voice = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"]
382
- st.session_state['speech_processed'] = True
383
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
384
- if audio_file:
385
- play_and_download_audio(audio_file)
386
- st.rerun()
387
-
388
- # Render Map
389
- components.html(html_code, width=800, height=600)
390
-
391
- # Chat Interface
392
- chat_content = asyncio.run(load_chat())
393
- st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
394
-
395
- message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
396
- if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
397
- if message:
398
- voice = CHARACTERS[st.session_state.username]["voice"]
399
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
400
- if audio_file:
401
- play_and_download_audio(audio_file)
402
- st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
403
-
404
- # Sidebar: Game HUD
405
- st.sidebar.subheader("๐ŸŽฎ Adventurerโ€™s HUD")
406
- new_username = st.sidebar.selectbox("๐Ÿง™โ€โ™‚๏ธ Choose Your Hero", list(CHARACTERS.keys()), index=list(CHARACTERS.keys()).index(st.session_state.username))
407
- if new_username != st.session_state.username:
408
- asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ”„ Transforms into {new_username}!", CHARACTERS[st.session_state.username]["voice"]))
409
- st.session_state.username = new_username
410
- save_username(st.session_state.username)
411
- st.rerun()
412
- st.sidebar.write(f"๐Ÿ“œ {CHARACTERS[st.session_state.username]['desc']}")
413
- st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
414
- st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
415
- st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
416
- st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
417
-
418
  if not st.session_state.get('server_running', False):
419
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
420
  st.session_state.server_task.start()
 
17
  import streamlit.components.v1 as components
18
  from gradio_client import Client
19
  from streamlit_marquee import streamlit_marquee
20
+ import folium
21
+ from streamlit_folium import folium_static
22
 
23
  # Patch asyncio for nesting
24
  nest_asyncio.apply()
 
45
  }
46
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
47
 
48
+ # Prairie Simulator Locations
49
+ PRAIRIE_LOCATIONS = {
50
+ "Deadwood, SD": (44.3769, -103.7298),
51
+ "Wind Cave National Park": (43.6047, -103.4798),
52
+ "Wyoming Spring Creek": (41.6666, -106.6666)
53
+ }
54
+
55
  # Directories
56
  for d in ["chat_logs", "audio_logs"]:
57
  os.makedirs(d, exist_ok=True)
 
161
  'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
162
  'speech_processed': False, 'players': {}, 'last_update': time.time(),
163
  'update_interval': 20, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
164
+ 'move_right': False, 'move_up': False, 'move_down': False,
165
+ 'prairie_players': {} # New for prairie simulator
166
  }
167
  for k, v in defaults.items():
168
  if k not in st.session_state:
169
  st.session_state[k] = v
 
170
  if st.session_state.username is None:
171
  saved_username = load_username()
172
  if saved_username and saved_username in CHARACTERS:
 
176
  asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Begins the Rocky Mountain Quest!", CHARACTERS[st.session_state.username]["voice"]))
177
  save_username(st.session_state.username)
178
 
 
179
  init_session_state()
180
 
181
  # ArXiv Integration
 
197
  st.session_state.active_connections[room_id] = {}
198
  st.session_state.active_connections[room_id][client_id] = websocket
199
  username = st.session_state.username
200
+
201
+ # Handle both Rocky Mountain Quest and Prairie Simulator players
202
+ if "prairie" in path:
203
+ st.session_state.prairie_players[client_id] = {
204
+ "username": username,
205
+ "animal": "prairie_dog", # Default
206
+ "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
207
+ "color": CHARACTERS[username]["color"]
208
+ }
209
+ else:
210
+ st.session_state.players[client_id] = {
211
+ "username": username,
212
+ "x": random.uniform(-20, 20),
213
+ "z": random.uniform(-50, 50),
214
+ "color": CHARACTERS[username]["color"]
215
+ }
216
+
217
  await save_chat_entry(username, f"๐Ÿ—บ๏ธ Joins the quest at {START_LOCATION}!", CHARACTERS[username]["voice"])
218
 
219
  try:
 
225
  _, x, z = content.split(":")
226
  st.session_state.players[client_id]["x"] = float(x)
227
  st.session_state.players[client_id]["z"] = float(z)
228
+ elif content.startswith("PRAIRIE:"):
229
+ action, value = content.split(":", 1)
230
+ if action == "ANIMAL":
231
+ st.session_state.prairie_players[client_id]["animal"] = value
232
+ elif action == "MOVE":
233
+ target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
234
+ st.session_state.prairie_players[client_id]["location"] = target
235
+ action_msg = f"{username} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
236
+ await save_chat_entry(username, action_msg, voice)
237
  else:
238
  await save_chat_entry(username, content, voice)
239
  await perform_arxiv_search(content, username)
 
241
  await save_chat_entry(username, "๐Ÿƒ Leaves the quest!", CHARACTERS[username]["voice"])
242
  if client_id in st.session_state.players:
243
  del st.session_state.players[client_id]
244
+ if client_id in st.session_state.prairie_players:
245
+ del st.session_state.prairie_players[client_id]
246
  finally:
247
  if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
248
  del st.session_state.active_connections[room_id][client_id]
 
250
  async def periodic_update():
251
  while True:
252
  if st.session_state.active_connections.get("quest"):
253
+ # Rocky Mountain Quest update
254
  player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!"
255
  message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
256
  player_data = json.dumps(list(st.session_state.players.values()))
257
  await broadcast_message(f"System|{message}", "quest")
258
  await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
259
+
260
+ # Prairie Simulator update
261
+ prairie_list = ", ".join([f"{p['username']} ({p['animal']})" for p in st.session_state.prairie_players.values()]) or "No animals yet!"
262
+ prairie_message = f"๐ŸŒพ Prairie Update: {prairie_list}"
263
+ prairie_data = json.dumps(list(st.session_state.prairie_players.values()))
264
+ await broadcast_message(f"System|{prairie_message}", "quest")
265
+ await broadcast_message(f"PRAIRIE_UPDATE:{prairie_data}", "quest")
266
+
267
+ await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
268
  await asyncio.sleep(st.session_state.update_interval)
269
 
270
  async def run_websocket_server():
 
279
  asyncio.set_event_loop(loop)
280
  loop.run_until_complete(run_websocket_server())
281
 
282
+ # Rocky Mountain Quest Map HTML
283
+ rocky_map_html = f"""
284
  <!DOCTYPE html>
285
  <html lang="en">
286
  <head>
 
335
 
336
  let players = {{}};
337
  const playerMeshes = {{}};
338
+ let xPos = 0;
339
  let zPos = 0;
340
 
341
  function updatePlayers(playerData) {{
 
380
  if (data.startsWith('MAP_UPDATE:')) {{
381
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
382
  updatePlayers(playerData);
383
+ }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
384
  const [sender, message] = data.split('|');
385
  const chatBox = document.getElementById('chatBox');
386
  chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
 
398
  </html>
399
  """
400
 
401
+ # Prairie Simulator HTML
402
+ prairie_simulator_html = f"""
403
+ <!DOCTYPE html>
404
+ <html lang="en">
405
+ <head>
406
+ <meta charset="UTF-8">
407
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
408
+ <title>Prairie Simulator</title>
409
+ <style>
410
+ body {{ margin: 0; font-family: Arial, sans-serif; background: #f0f0f0; }}
411
+ #simContainer {{ width: 100%; height: 600px; position: relative; }}
412
+ #map {{ width: 100%; height: 400px; }}
413
+ #controls {{ padding: 10px; background: #fff; border-radius: 5px; }}
414
+ #status {{ color: #333; padding: 5px; }}
415
+ #chatBox {{ height: 100px; overflow-y: auto; background: #fff; border: 1px solid #ccc; padding: 5px; }}
416
+ </style>
417
+ </head>
418
+ <body>
419
+ <div id="simContainer">
420
+ <div id="map"></div>
421
+ <div id="controls">
422
+ <label>Animal: </label>
423
+ <select id="animal" onchange="updateAnimal()">
424
+ <option value="prairie_dog">Prairie Dog</option>
425
+ <option value="deer">Deer</option>
426
+ <option value="sheep">Sheep</option>
427
+ <option value="groundhog">Groundhog</option>
428
+ </select>
429
+ <label>Move to: </label>
430
+ <select id="location">
431
+ <option value="Deadwood, SD">Deadwood, SD</option>
432
+ <option value="Wind Cave National Park">Wind Cave National Park</option>
433
+ <option value="Wyoming Spring Creek">Wyoming Spring Creek</option>
434
+ </select>
435
+ <button onclick="move()">Move</button>
436
+ <div id="status">Players: 0</div>
437
+ <div id="chatBox"></div>
438
+ </div>
439
+ </div>
440
+
441
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
442
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
443
+ <script>
444
+ const playerName = "{st.session_state.username}";
445
+ let ws = new WebSocket('ws://localhost:8765/prairie');
446
+ const map = L.map('map').setView([44.0, -103.0], 7);
447
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {{
448
+ attribution: '&copy; OpenStreetMap contributors'
449
+ }}).addTo(map);
450
+
451
+ const locations = {{
452
+ "Deadwood, SD": [44.3769, -103.7298],
453
+ "Wind Cave National Park": [43.6047, -103.4798],
454
+ "Wyoming Spring Creek": [41.6666, -106.6666]
455
+ }};
456
+ for (let loc in locations) {{
457
+ L.marker(locations[loc]).addTo(map).bindPopup(loc);
458
+ }}
459
+
460
+ let players = {{}};
461
+ const markers = {{}};
462
+
463
+ function updatePlayers(playerData) {{
464
+ playerData.forEach(player => {{
465
+ if (!markers[player.username]) {{
466
+ markers[player.username] = L.circleMarker(player.location, {{
467
+ color: `#${{player.color.toString(16).padStart(6, '0')}}`,
468
+ radius: 5
469
+ }}).addTo(map).bindPopup(`${{player.username}} (${{player.animal}})`);
470
+ }} else {{
471
+ markers[player.username].setLatLng(player.location);
472
+ }}
473
+ }});
474
+ document.getElementById('status').textContent = `Players: ${{Object.keys(markers).length}}`;
475
+ }}
476
+
477
+ function updateAnimal() {{
478
+ const animal = document.getElementById('animal').value;
479
+ ws.send(`${{playerName}}|PRAIRIE:ANIMAL:${{animal}}`);
480
+ }}
481
+
482
+ function move() {{
483
+ const location = document.getElementById('location').value;
484
+ ws.send(`${{playerName}}|PRAIRIE:MOVE:${{location}}`);
485
+ }}
486
+
487
+ ws.onmessage = function(event) {{
488
+ const data = event.data;
489
+ if (data.startsWith('PRAIRIE_UPDATE:')) {{
490
+ const playerData = JSON.parse(data.split('PRAIRIE_UPDATE:')[1]);
491
+ updatePlayers(playerData);
492
+ }} else if (!data.startsWith('MAP_UPDATE:')) {{
493
+ const [sender, message] = data.split('|');
494
+ const chatBox = document.getElementById('chatBox');
495
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
496
+ chatBox.scrollTop = chatBox.scrollHeight;
497
+ }}
498
+ }};
499
+ </script>
500
+ </body>
501
+ </html>
502
+ """
503
+
504
  # Main Game Loop
505
  def main():
506
+ # Sidebar Titles and HUD
507
+ st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
508
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
509
+ st.sidebar.write(f"๐Ÿ“œ {CHARACTERS[st.session_state.username]['desc']}")
510
+ st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
511
+ st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
512
+ st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
513
+ st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
514
+
515
+ new_username = st.sidebar.selectbox("๐Ÿง™โ€โ™‚๏ธ Choose Your Hero", list(CHARACTERS.keys()), index=list(CHARACTERS.keys()).index(st.session_state.username))
516
+ if new_username != st.session_state.username:
517
+ asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ”„ Transforms into {new_username}!", CHARACTERS[st.session_state.username]["voice"]))
518
+ st.session_state.username = new_username
519
+ save_username(st.session_state.username)
520
+ st.rerun()
521
+
522
+ # Two-column layout
523
+ left_col, right_col = st.columns([2, 1])
524
+
525
+ # Left Column: Rocky Mountain Quest
526
+ with left_col:
527
+ # Render Rocky Mountain Map
528
+ components.html(rocky_map_html, width=800, height=600)
529
+
530
+ # Chat Interface
531
+ chat_content = asyncio.run(load_chat())
532
+ st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
533
+
534
+ message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
535
+ if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
536
+ if message:
537
+ voice = CHARACTERS[st.session_state.username]["voice"]
538
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
539
+ if audio_file:
540
+ play_and_download_audio(audio_file)
541
+ st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
542
+
543
+ # Voice Input Component
544
+ mycomponent = components.declare_component("speech_component", path="./speech_component")
545
+ val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
546
+ if val and val != st.session_state.last_transcript:
547
+ val_stripped = val.strip().replace('\n', ' ')
548
+ if val_stripped:
549
+ voice = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"]
550
+ st.session_state['speech_processed'] = True
551
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
552
+ if audio_file:
553
+ play_and_download_audio(audio_file)
554
+ st.rerun()
555
+
556
+ # Right Column: Prairie Simulator
557
+ with right_col:
558
+ st.subheader("๐ŸŒพ Prairie Simulator")
559
+ components.html(prairie_simulator_html, width=600, height=600)
560
 
561
  # Countdown Timer
562
  elapsed = time.time() - st.session_state.last_update
 
566
  st.session_state.last_update = time.time()
567
  st.rerun()
568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  if not st.session_state.get('server_running', False):
570
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
571
  st.session_state.server_task.start()