awacke1 commited on
Commit
34fcd26
·
verified ·
1 Parent(s): da40c9a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +513 -792
app.py CHANGED
@@ -1,824 +1,545 @@
1
- import gradio as gr
2
  import asyncio
3
  import websockets
4
- import json
5
  import uuid
6
  import argparse
7
- import urllib.parse
8
  from datetime import datetime
9
- import logging
10
- import sys
11
  import os
 
12
  import time
13
- from pathlib import Path
 
 
 
 
 
 
 
 
 
14
 
15
- # Configure logging
16
- logging.basicConfig(
17
- level=logging.INFO,
18
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
- handlers=[logging.StreamHandler(sys.stdout)]
 
 
 
 
 
 
 
 
20
  )
21
- logger = logging.getLogger("chat-node")
22
 
23
- # Dictionary to store active connections
24
- active_connections = {}
25
- # Dictionary to store message history for each chat room (in-memory cache)
26
- chat_history = {}
27
- # Dictionary to track file modification times
28
- file_modification_times = {}
29
- # Dictionary to track users in each room/sector
30
- sector_users = {}
31
- # Global variables for event loop and queue
32
- main_event_loop = None
33
- message_queue = []
34
-
35
- # Grid dimensions for 2D sector map
36
- GRID_WIDTH = 10
37
- GRID_HEIGHT = 10
38
-
39
- # Directory to store persistent chat history
40
- HISTORY_DIR = "chat_history"
41
-
42
- # Create history directory if it doesn't exist
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  os.makedirs(HISTORY_DIR, exist_ok=True)
44
 
45
- # README.md file that won't be listed or deleted
46
- README_PATH = os.path.join(HISTORY_DIR, "README.md")
47
- if not os.path.exists(README_PATH):
48
- with open(README_PATH, "w") as f:
49
- f.write("# Chat History\n\nThis directory contains persistent chat history files.\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- # Get node name from URL or command line
52
  def get_node_name():
 
 
 
 
53
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
54
- parser.add_argument('--node-name', type=str, default=None, help='Name for this chat node')
55
- parser.add_argument('--port', type=int, default=7860, help='Port to run the Gradio interface on')
56
- parser.add_argument('--ws-port', type=int, default=8765, help='Port to run the WebSocket server on')
57
-
58
  args = parser.parse_args()
59
- node_name = args.node_name
60
- port = args.port
61
- ws_port = args.ws_port
62
-
63
- # If no node name specified, generate a random one
64
- if not node_name:
65
- node_name = f"node-{uuid.uuid4().hex[:8]}"
66
-
67
- return node_name, port, ws_port
68
-
69
- def get_room_history_file(room_id):
70
- """Get the filename for a room's history."""
71
- # Create timestamp-based log files
72
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
73
- return os.path.join(HISTORY_DIR, f"{room_id}_{timestamp}.jsonl")
74
-
75
- def get_all_room_history_files(room_id):
76
- """Get all history files for a specific room."""
77
- files = []
78
- for file in os.listdir(HISTORY_DIR):
79
- if file.startswith(f"{room_id}_") and file.endswith(".jsonl"):
80
- files.append(os.path.join(HISTORY_DIR, file))
81
- # Sort by modification time (newest first)
82
- files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
83
- return files
84
-
85
- def get_all_history_files():
86
- """Get a list of all unique room IDs from history files, sorted by modification time (newest first)."""
87
- try:
88
- rooms = {} # room_id -> (newest_file_path, mod_time)
89
-
90
- for file in os.listdir(HISTORY_DIR):
91
- if file.endswith(".jsonl"):
92
- file_path = os.path.join(HISTORY_DIR, file)
93
- mod_time = os.path.getmtime(file_path)
94
-
95
- # Extract room_id from filename (format: roomid_timestamp.jsonl)
96
- parts = file.split('_', 1)
97
- if len(parts) > 0:
98
- room_id = parts[0]
99
-
100
- # Keep track of newest file for each room
101
- if room_id not in rooms or mod_time > rooms[room_id][1]:
102
- rooms[room_id] = (file_path, mod_time)
103
-
104
- # Convert to list and sort by modification time
105
- history_files = [(room_id, file_path, mod_time) for room_id, (file_path, mod_time) in rooms.items()]
106
- history_files.sort(key=lambda x: x[2], reverse=True)
107
-
108
- return history_files
109
- except Exception as e:
110
- logger.error(f"Error in get_all_history_files: {e}")
111
- return [] # Return empty list on error
112
-
113
- def load_room_history(room_id):
114
- """Load chat history for a room from all persistent storage files."""
115
- if room_id not in chat_history:
116
- chat_history[room_id] = []
117
-
118
- # Get all history files for this room
119
- history_files = get_all_room_history_files(room_id)
120
-
121
- # Track file modification times
122
- for file in history_files:
123
- if file not in file_modification_times:
124
- file_modification_times[file] = os.path.getmtime(file)
125
-
126
- # Load messages from all files
127
- messages = []
128
- for history_file in history_files:
129
- try:
130
- with open(history_file, 'r') as f:
131
- for line in f:
132
- line = line.strip()
133
- if line: # Skip empty lines
134
- try:
135
- data = json.loads(line)
136
- messages.append(data)
137
- except json.JSONDecodeError:
138
- logger.error(f"Error parsing JSON line in {history_file}")
139
- except Exception as e:
140
- logger.error(f"Error loading history from {history_file}: {e}")
141
-
142
- # Sort by timestamp
143
- messages.sort(key=lambda x: x.get("timestamp", ""), reverse=False)
144
- chat_history[room_id] = messages
145
-
146
- logger.info(f"Loaded {len(messages)} messages from {len(history_files)} files for room {room_id}")
147
-
148
- # Track users in this sector
149
- if room_id not in sector_users:
150
- sector_users[room_id] = set()
151
-
152
- return chat_history[room_id]
153
-
154
- def save_message_to_history(room_id, message):
155
- """Save a single message to the newest history file for a room."""
156
- # Get the newest history file or create a new one
157
- history_files = get_all_room_history_files(room_id)
158
-
159
- if not history_files:
160
- # Create a new file
161
- history_file = get_room_history_file(room_id)
162
- else:
163
- # Use the newest file if it's less than 1 MB, otherwise create a new one
164
- newest_file = history_files[0]
165
- if os.path.getsize(newest_file) > 1024 * 1024: # 1 MB
166
- history_file = get_room_history_file(room_id)
167
- else:
168
- history_file = newest_file
169
-
170
- try:
171
- # Append the message as a single line of JSON
172
- with open(history_file, 'a') as f:
173
- f.write(json.dumps(message) + '\n')
174
-
175
- # Update modification time
176
- file_modification_times[history_file] = os.path.getmtime(history_file)
177
-
178
- logger.debug(f"Saved message to {history_file}")
179
- except Exception as e:
180
- logger.error(f"Error saving message to {history_file}: {e}")
181
-
182
- def check_for_new_messages():
183
- """Check for new messages in all history files."""
184
- updated_rooms = set()
185
-
186
- # Check all files in the history directory
187
- for file in os.listdir(HISTORY_DIR):
188
- if file.endswith(".jsonl"):
189
- file_path = os.path.join(HISTORY_DIR, file)
190
- current_mtime = os.path.getmtime(file_path)
191
-
192
- # Check if this file is new or has been modified
193
- if file_path not in file_modification_times or current_mtime > file_modification_times[file_path]:
194
- # Extract room_id from filename
195
- parts = file.split('_', 1)
196
- if len(parts) > 0:
197
- room_id = parts[0]
198
- updated_rooms.add(room_id)
199
-
200
- # Update tracked modification time
201
- file_modification_times[file_path] = current_mtime
202
-
203
- # Reload history for updated rooms
204
- for room_id in updated_rooms:
205
- if room_id in chat_history:
206
- # Remember we had this room loaded
207
- old_history_len = len(chat_history[room_id])
208
- # Clear and reload
209
- chat_history[room_id] = []
210
- load_room_history(room_id)
211
- new_history_len = len(chat_history[room_id])
212
-
213
- if new_history_len > old_history_len:
214
- logger.info(f"Found {new_history_len - old_history_len} new messages for room {room_id}")
215
-
216
- return updated_rooms
217
-
218
- def get_sector_coordinates(room_id):
219
- """Convert a room ID to grid coordinates, or assign new ones."""
 
 
 
 
 
 
 
 
220
  try:
221
- # Try to parse room ID as "x,y"
222
- if ',' in room_id:
223
- x, y = map(int, room_id.split(','))
224
- return max(0, min(x, GRID_WIDTH-1)), max(0, min(y, GRID_HEIGHT-1))
225
- except:
 
 
 
 
 
 
 
 
 
226
  pass
227
-
228
- # Hash the room_id string to get stable coordinates
229
- hash_val = hash(room_id)
230
- x = abs(hash_val) % GRID_WIDTH
231
- y = abs(hash_val >> 8) % GRID_HEIGHT
232
-
233
- return x, y
234
-
235
- def generate_sector_map():
236
- """Generate an ASCII representation of the sector map."""
237
- # Initialize empty grid
238
- grid = [[' ' for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
239
-
240
- # Place active rooms with user counts
241
- for room_id, users in sector_users.items():
242
- if users: # Only show rooms with users
243
- x, y = get_sector_coordinates(room_id)
244
- user_count = len(users)
245
- grid[y][x] = str(min(user_count, 9)) if user_count < 10 else '+'
246
-
247
- # Create ASCII representation
248
- header = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
249
- map_str = header + '\n'
250
-
251
- for y in range(GRID_HEIGHT):
252
- row = f"{y % 10}|"
253
- for x in range(GRID_WIDTH):
254
- row += grid[y][x]
255
- row += '|'
256
- map_str += row + '\n'
257
-
258
- footer = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
259
- map_str += footer
260
-
261
- return f"```\n{map_str}\n```\n\nLegend: Number indicates users in sector. '+' means 10+ users."
262
-
263
- def list_available_rooms():
264
- """List all available chat rooms with their last activity time and user count."""
265
- try:
266
- history_files = get_all_history_files()
267
-
268
- if not history_files:
269
- return "No chat rooms available yet. Create one by joining a room!"
270
-
271
- room_list = "### Available Chat Rooms (Sectors)\n\n"
272
- room_list += "| Room ID | Sector | Users | Last Activity |\n"
273
- room_list += "|---------|--------|-------|---------------|\n"
274
-
275
- for room_id, file_path, mod_time in history_files:
276
- x, y = get_sector_coordinates(room_id)
277
- user_count = len(sector_users.get(room_id, set()))
278
- last_activity = datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
279
- room_list += f"| {room_id} | ({x},{y}) | {user_count} | {last_activity} |\n"
280
-
281
- room_list += "\n\n### Sector Map\n\n" + generate_sector_map()
282
-
283
- return room_list
284
- except Exception as e:
285
- logger.error(f"Error in list_available_rooms: {e}")
286
- return f"Error listing rooms: {str(e)}"
287
 
288
  async def broadcast_message(message, room_id):
289
- """Broadcast a message to all clients in a room."""
 
 
 
290
  if room_id in active_connections:
291
- disconnected_clients = []
292
-
293
- for client_id, websocket in active_connections[room_id].items():
294
  try:
295
- await websocket.send(json.dumps(message))
296
- except websockets.exceptions.ConnectionClosed:
297
- disconnected_clients.append(client_id)
298
-
299
- # Clean up disconnected clients
300
- for client_id in disconnected_clients:
301
  del active_connections[room_id][client_id]
302
 
303
- async def start_websocket_server(host='0.0.0.0', port=8765):
304
- """Start the WebSocket server."""
305
- try:
306
- server = await websockets.serve(websocket_handler, host, port)
307
- logger.info(f"WebSocket server started on ws://{host}:{port}")
308
- return server
309
- except OSError as e:
310
- if e.errno == 98: # Address already in use
311
- logger.warning(f"Port {port} already in use, trying port {port+1}")
312
- # Try a different port
313
- return await start_websocket_server(host, port+1)
314
- else:
315
- # If it's a different error, re-raise it
316
- raise
317
-
318
- def send_message(message, username, room_id):
319
- """Function to send a message from the Gradio interface."""
320
- if not message.strip():
321
- return None
322
-
323
- global message_queue
324
-
325
- msg_data = {
326
- "type": "chat",
327
- "content": message,
328
- "username": username,
329
- "room_id": room_id
330
- }
331
-
332
- # Add to queue for processing by the main loop
333
- message_queue.append(msg_data)
334
-
335
- # Format the message for display in the UI
336
- formatted_msg = f"{username}: {message}"
337
- return formatted_msg
338
-
339
- def send_clear_command():
340
- """Send a command to clear all chat history."""
341
- global message_queue
342
-
343
- msg_data = {
344
- "type": "command",
345
- "command": "clear_history",
346
- "username": "System"
347
- }
348
-
349
- # Add to queue for processing by the main loop
350
- message_queue.append(msg_data)
351
-
352
- return "🧹 Clearing all chat history..."
353
-
354
- async def clear_all_history():
355
- """Clear all chat history for all rooms."""
356
- global chat_history, sector_users
357
-
358
- # Clear in-memory history
359
- chat_history = {}
360
- sector_users = {}
361
-
362
- # Delete all history files except README.md
363
- for file in os.listdir(HISTORY_DIR):
364
- if file.endswith(".jsonl"):
365
- try:
366
- os.remove(os.path.join(HISTORY_DIR, file))
367
- except Exception as e:
368
- logger.error(f"Error removing file {file}: {e}")
369
-
370
- # Broadcast clear message to all rooms
371
- clear_msg = {
372
- "type": "system",
373
- "content": "🧹 All chat history has been cleared by a user",
374
- "timestamp": datetime.now().isoformat(),
375
- "sender": "system"
376
- }
377
-
378
- for room_id in list(active_connections.keys()):
379
- clear_msg["room_id"] = room_id
380
- await broadcast_message(clear_msg, room_id)
381
-
382
- logger.info("All chat history cleared")
383
- return "All chat history cleared"
384
-
385
- def join_room(room_id, chat_history_output):
386
- """Join a specific chat room."""
387
- if not room_id.strip():
388
- return "Please enter a valid room ID", chat_history_output
389
-
390
- # Sanitize the room ID
391
- room_id = urllib.parse.quote(room_id.strip())
392
-
393
- # Load room history from persistent storage
394
- history = load_room_history(room_id)
395
-
396
- # Get sector coordinates
397
- x, y = get_sector_coordinates(room_id)
398
-
399
- # Format existing messages
400
- formatted_history = [f"You are now in Sector ({x},{y}) - Room ID: {room_id}"]
401
- formatted_history.append(f"Sector Map:\n{generate_sector_map()}")
402
-
403
- for msg in history:
404
- if msg.get("type") == "chat":
405
- sender_node = f" [{msg.get('sender_node', 'unknown')}]" if "sender_node" in msg else ""
406
- time_str = ""
407
- if "timestamp" in msg:
408
- try:
409
- dt = datetime.fromisoformat(msg["timestamp"])
410
- time_str = f"[{dt.strftime('%H:%M:%S')}] "
411
- except:
412
- pass
413
- formatted_history.append(f"{time_str}{msg.get('username', 'Anonymous')}{sender_node}: {msg.get('content', '')}")
414
- elif msg.get("type") == "system":
415
- formatted_history.append(f"System: {msg.get('content', '')}")
416
-
417
- return f"Joined room: {room_id}", formatted_history
418
 
419
- async def websocket_handler(websocket, path):
420
- """Handle WebSocket connections."""
421
- client_id = str(uuid.uuid4())
422
- room_id = "default" # Default initialization to avoid reference errors
423
-
424
- try:
425
- # Extract room_id from path if present
426
- path_parts = path.strip('/').split('/')
427
- room_id = path_parts[0] if path_parts else "default"
428
-
429
- # Register the new client
430
- if room_id not in active_connections:
431
- active_connections[room_id] = {}
432
-
433
- active_connections[room_id][client_id] = websocket
434
-
435
- # Add user to sector map
436
- if room_id not in sector_users:
437
- sector_users[room_id] = set()
438
- sector_users[room_id].add(client_id)
439
-
440
- # Get sector coordinates
441
- x, y = get_sector_coordinates(room_id)
442
-
443
- # Load or initialize chat history
444
- room_history = load_room_history(room_id)
445
-
446
- # Send welcome message
447
- welcome_msg = {
448
- "type": "system",
449
- "content": f"Welcome to room '{room_id}' (Sector {x},{y})! Connected from node '{NODE_NAME}'",
450
- "timestamp": datetime.now().isoformat(),
451
- "sender": "system",
452
- "room_id": room_id
453
- }
454
- await websocket.send(json.dumps(welcome_msg))
455
-
456
- # Send sector map
457
- map_msg = {
458
- "type": "system",
459
- "content": f"Sector Map:\n{generate_sector_map()}",
460
- "timestamp": datetime.now().isoformat(),
461
- "sender": "system",
462
- "room_id": room_id
463
- }
464
- await websocket.send(json.dumps(map_msg))
465
-
466
- # Send chat history
467
- for msg in room_history:
468
- await websocket.send(json.dumps(msg))
469
-
470
- # Broadcast join notification
471
- join_msg = {
472
- "type": "system",
473
- "content": f"User joined the room (Sector {x},{y}) - {len(sector_users[room_id])} users now present",
474
- "timestamp": datetime.now().isoformat(),
475
- "sender": "system",
476
- "room_id": room_id
477
- }
478
- await broadcast_message(join_msg, room_id)
479
- save_message_to_history(room_id, join_msg)
480
-
481
- logger.info(f"New client {client_id} connected to room {room_id} (Sector {x},{y})")
482
-
483
- # Handle messages from this client
484
- async for message in websocket:
485
- try:
486
- data = json.loads(message)
487
-
488
- # Check for clear command
489
- if data.get("type") == "command" and data.get("command") == "clear_history":
490
- result = await clear_all_history()
491
- continue
492
-
493
- # Check for map request
494
- if data.get("type") == "command" and data.get("command") == "show_map":
495
- map_msg = {
496
- "type": "system",
497
- "content": f"Sector Map:\n{generate_sector_map()}",
498
- "timestamp": datetime.now().isoformat(),
499
- "sender": "system",
500
- "room_id": room_id
501
- }
502
- await websocket.send(json.dumps(map_msg))
503
- continue
504
-
505
- # Add metadata to the message
506
- data["timestamp"] = datetime.now().isoformat()
507
- data["sender_node"] = NODE_NAME
508
- data["room_id"] = room_id
509
-
510
- # Store in history
511
- chat_history[room_id].append(data)
512
- if len(chat_history[room_id]) > 500: # Increased limit to 500 messages
513
- chat_history[room_id] = chat_history[room_id][-500:]
514
-
515
- # Save to persistent storage
516
- save_message_to_history(room_id, data)
517
-
518
- # Broadcast to all clients in the room
519
- await broadcast_message(data, room_id)
520
-
521
- except json.JSONDecodeError:
522
- error_msg = {
523
- "type": "error",
524
- "content": "Invalid JSON format",
525
- "timestamp": datetime.now().isoformat(),
526
- "sender": "system",
527
- "room_id": room_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  }
529
- await websocket.send(json.dumps(error_msg))
530
-
531
- except websockets.exceptions.ConnectionClosed:
532
- logger.info(f"Client {client_id} disconnected from room {room_id}")
533
- finally:
534
- # Remove the client when disconnected
535
- if room_id in active_connections and client_id in active_connections[room_id]:
536
- del active_connections[room_id][client_id]
537
-
538
- # Remove user from sector map
539
- if room_id in sector_users and client_id in sector_users[room_id]:
540
- sector_users[room_id].remove(client_id)
541
-
542
- # Get sector coordinates
543
- x, y = get_sector_coordinates(room_id)
544
-
545
- # Broadcast leave notification
546
- leave_msg = {
547
- "type": "system",
548
- "content": f"User left the room (Sector {x},{y}) - {len(sector_users.get(room_id, set()))} users remaining",
549
- "timestamp": datetime.now().isoformat(),
550
- "sender": "system",
551
- "room_id": room_id
552
  }
553
- await broadcast_message(leave_msg, room_id)
554
- save_message_to_history(room_id, leave_msg)
555
-
556
- # Clean up empty rooms (but keep history)
557
- if not active_connections[room_id]:
558
- del active_connections[room_id]
559
-
560
- async def process_message_queue():
561
- """Process messages in the queue and broadcast them."""
562
- global message_queue
563
-
564
- while True:
565
- # Check if there are messages to process
566
- if message_queue:
567
- # Get the oldest message
568
- msg_data = message_queue.pop(0)
569
-
570
- # Check for clear command
571
- if msg_data.get("type") == "command" and msg_data.get("command") == "clear_history":
572
- await clear_all_history()
573
- elif "room_id" in msg_data:
574
- # Regular message - add to history and broadcast
575
- room_id = msg_data["room_id"]
576
-
577
- # Add timestamp if not present
578
- if "timestamp" not in msg_data:
579
- msg_data["timestamp"] = datetime.now().isoformat()
580
-
581
- # Add sender node if not present
582
- if "sender_node" not in msg_data:
583
- msg_data["sender_node"] = NODE_NAME
584
-
585
- # Store in memory history
586
- if room_id not in chat_history:
587
- chat_history[room_id] = []
588
- chat_history[room_id].append(msg_data)
589
-
590
- # Save to file
591
- save_message_to_history(room_id, msg_data)
592
-
593
- # Broadcast to all clients in the room
594
- await broadcast_message(msg_data, room_id)
595
-
596
- # Check for file changes every second
597
- updated_rooms = check_for_new_messages()
598
-
599
- # If any rooms were updated, notify clients
600
- for room_id in updated_rooms:
601
- if room_id in active_connections:
602
- # Find the newest messages that clients might not have seen
603
- # This is a simplification - in a real app, you'd track which messages each client has seen
604
- if room_id in chat_history and chat_history[room_id]:
605
- # Get the latest 5 messages as an example
606
- latest_messages = chat_history[room_id][-5:]
607
- for msg in latest_messages:
608
- # Only broadcast messages from other nodes (to avoid duplicates)
609
- if msg.get("sender_node") != NODE_NAME:
610
- await broadcast_message(msg, room_id)
611
-
612
- # Sleep to avoid busy-waiting
613
- await asyncio.sleep(1.0) # Check every second
614
-
615
- def create_gradio_interface():
616
- """Create and return the Gradio interface."""
617
- with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
618
- gr.Markdown(f"# Chat Node: {NODE_NAME}")
619
- gr.Markdown("Join a room by entering a room ID below or create a new one.")
620
-
621
- # Room list and management
622
- with gr.Row():
623
- with gr.Column(scale=3):
624
- room_list = gr.Markdown(value="Loading available rooms...")
625
- refresh_button = gr.Button("🔄 Refresh Room List")
626
- with gr.Column(scale=1):
627
- clear_button = gr.Button("🧹 Clear All Chat History", variant="stop")
628
-
629
- # Join room controls with 2D grid input
630
- with gr.Row():
631
- with gr.Column(scale=2):
632
- room_id_input = gr.Textbox(label="Room ID", placeholder="Enter room ID or use x,y coordinates")
633
- join_button = gr.Button("Join Room")
634
- with gr.Column(scale=1):
635
- with gr.Row():
636
- x_coord = gr.Number(label="X", value=0, minimum=0, maximum=GRID_WIDTH-1, step=1)
637
- y_coord = gr.Number(label="Y", value=0, minimum=0, maximum=GRID_HEIGHT-1, step=1)
638
- grid_join_button = gr.Button("Join by Coordinates")
639
-
640
- # Chat area with multiline support
641
- chat_history_output = gr.Textbox(label="Chat History", lines=20, max_lines=20)
642
-
643
- # Message controls with multiline support
644
- with gr.Row():
645
- username_input = gr.Textbox(label="Username", placeholder="Enter your username", value="User")
646
- with gr.Column(scale=3):
647
- message_input = gr.Textbox(
648
- label="Message",
649
- placeholder="Type your message here. Press Shift+Enter for new line, Enter to send.",
650
- lines=3
651
- )
652
- with gr.Column(scale=1):
653
- send_button = gr.Button("Send")
654
- map_button = gr.Button("🗺️ Show Map")
655
-
656
- # Current room display
657
- current_room_display = gr.Textbox(label="Current Room", value="Not joined any room yet")
658
-
659
- # Event handlers
660
- refresh_button.click(
661
- list_available_rooms,
662
- inputs=[],
663
- outputs=[room_list]
664
- )
665
-
666
- clear_button.click(
667
- send_clear_command,
668
- inputs=[],
669
- outputs=[room_list]
670
- )
671
-
672
- def join_by_coordinates(x, y):
673
- """Join a room using grid coordinates."""
674
- room_id = f"{int(x)},{int(y)}"
675
- return room_id
676
-
677
- # Link grid coordinates to room ID
678
- grid_join_button.click(
679
- join_by_coordinates,
680
- inputs=[x_coord, y_coord],
681
- outputs=[room_id_input]
682
- ).then(
683
- join_room,
684
- inputs=[room_id_input, chat_history_output],
685
- outputs=[current_room_display, chat_history_output]
686
- )
687
-
688
- join_button.click(
689
- join_room,
690
- inputs=[room_id_input, chat_history_output],
691
- outputs=[current_room_display, chat_history_output]
692
- )
693
-
694
- def send_and_clear(message, username, room_id):
695
- if not room_id.startswith("Joined room:"):
696
- return "Please join a room first", message
697
-
698
- actual_room_id = room_id.replace("Joined room: ", "").strip()
699
-
700
- # Support for multi-line messages
701
- message_lines = message.strip().split("\n")
702
- formatted_msg = ""
703
-
704
- for line in message_lines:
705
- if line.strip(): # Skip empty lines
706
- sent_msg = send_message(line.strip(), username, actual_room_id)
707
- if sent_msg:
708
- formatted_msg += sent_msg + "\n"
709
-
710
- if formatted_msg:
711
- return "", formatted_msg
712
- return message, None
713
-
714
- send_button.click(
715
- send_and_clear,
716
- inputs=[message_input, username_input, current_room_display],
717
- outputs=[message_input, chat_history_output]
718
- )
719
-
720
- def show_sector_map(room_id):
721
- if not room_id.startswith("Joined room:"):
722
- return "Please join a room first to view the map"
723
-
724
- return generate_sector_map()
725
-
726
- map_button.click(
727
- show_sector_map,
728
- inputs=[current_room_display],
729
- outputs=[chat_history_output]
730
- )
731
-
732
- # Handle Enter key for sending, Shift+Enter for new line
733
- def on_message_submit(message, username, room_id):
734
- # Simply call send_and_clear
735
- return send_and_clear(message, username, room_id)
736
-
737
- message_input.submit(
738
- on_message_submit,
739
- inputs=[message_input, username_input, current_room_display],
740
- outputs=[message_input, chat_history_output]
741
- )
742
-
743
- # On load, populate room list
744
- interface.load(
745
- list_available_rooms,
746
- inputs=[],
747
- outputs=[room_list]
748
- )
749
-
750
- return interface
751
 
752
  async def main():
753
- """Main function to start the application."""
754
- global NODE_NAME, main_event_loop
755
- NODE_NAME, port, ws_port = get_node_name()
756
-
757
- # Store the main event loop for later use
758
- main_event_loop = asyncio.get_running_loop()
759
-
760
- # Start WebSocket server
761
- try:
762
- server = await start_websocket_server(port=ws_port)
763
-
764
- # Start message queue processor
765
- asyncio.create_task(process_message_queue())
766
-
767
- # Create and launch Gradio interface
768
- interface = create_gradio_interface()
769
-
770
- # Custom middleware to extract node name from URL query parameters
771
- from starlette.middleware.base import BaseHTTPMiddleware
772
-
773
- class NodeNameMiddleware(BaseHTTPMiddleware):
774
- async def dispatch(self, request, call_next):
775
- global NODE_NAME
776
- query_params = dict(request.query_params)
777
- if "node_name" in query_params:
778
- NODE_NAME = query_params["node_name"]
779
- logger.info(f"Node name set to {NODE_NAME} from URL parameter")
780
-
781
- response = await call_next(request)
782
- return response
783
-
784
- # Apply middleware
785
- app = gr.routes.App.create_app(interface)
786
- app.add_middleware(NodeNameMiddleware)
787
-
788
- # Launch with the modified app
789
- gr.routes.mount_gradio_app(app, interface, path="/")
790
-
791
- # Run the FastAPI app with uvicorn
792
- import uvicorn
793
- config = uvicorn.Config(app, host="0.0.0.0", port=port)
794
-
795
- # Try to create the server with retries for port conflicts
796
- server_started = False
797
- max_retries = 5
798
- current_port = port
799
-
800
- for attempt in range(max_retries):
801
- try:
802
- config = uvicorn.Config(app, host="0.0.0.0", port=current_port)
803
- server = uvicorn.Server(config)
804
- logger.info(f"Starting Gradio interface on http://0.0.0.0:{current_port} with node name '{NODE_NAME}'")
805
- logger.info("Starting message queue processor")
806
- await server.serve()
807
- server_started = True
808
- break
809
- except OSError as e:
810
- if e.errno == 98: # Address already in use
811
- current_port += 1
812
- logger.warning(f"Port {current_port-1} already in use, trying port {current_port}")
813
- else:
814
- raise
815
-
816
- if not server_started:
817
- logger.error(f"Failed to start server after {max_retries} attempts")
818
-
819
- except Exception as e:
820
- logger.error(f"Error in main: {e}")
821
- raise
822
 
823
  if __name__ == "__main__":
824
  asyncio.run(main())
 
1
+ import streamlit as st
2
  import asyncio
3
  import websockets
 
4
  import uuid
5
  import argparse
 
6
  from datetime import datetime
 
 
7
  import os
8
+ import random
9
  import time
10
+ import hashlib
11
+ from PIL import Image
12
+ import glob
13
+ from urllib.parse import quote
14
+ import base64
15
+ import io
16
+ import streamlit.components.v1 as components
17
+ import edge_tts
18
+ from audio_recorder_streamlit import audio_recorder
19
+ import nest_asyncio
20
 
21
+ # Apply patch to allow nested asyncio.run() calls
22
+ nest_asyncio.apply()
23
+
24
+ # Initial App Configuration (static)
25
+ icons = '🤖🧠🔬📝'
26
+ START_ROOM = "Sector 🌌"
27
+
28
+ # Set page config once at the top
29
+ st.set_page_config(
30
+ page_title="🤖🧠MMO Chat Brain📝🔬", # Initial static title
31
+ page_icon=icons,
32
+ layout="wide",
33
+ initial_sidebar_state="auto"
34
  )
 
35
 
36
+ # Fun usernames with emojis and their paired voices
37
+ FUN_USERNAMES = {
38
+ "CosmicJester 🌌": "en-US-AriaNeural",
39
+ "PixelPanda 🐼": "en-US-JennyNeural",
40
+ "QuantumQuack 🦆": "en-GB-SoniaNeural",
41
+ "StellarSquirrel 🐿️": "en-AU-NatashaNeural",
42
+ "GizmoGuru ⚙️": "en-CA-ClaraNeural",
43
+ "NebulaNinja 🌠": "en-US-GuyNeural",
44
+ "ByteBuster 💾": "en-GB-RyanNeural",
45
+ "GalacticGopher 🌍": "en-AU-WilliamNeural",
46
+ "RocketRaccoon 🚀": "en-CA-LiamNeural",
47
+ "EchoElf 🧝": "en-US-AriaNeural",
48
+ "PhantomFox 🦊": "en-US-JennyNeural",
49
+ "WittyWizard 🧙": "en-GB-SoniaNeural",
50
+ "LunarLlama 🌙": "en-AU-NatashaNeural",
51
+ "SolarSloth ☀️": "en-CA-ClaraNeural",
52
+ "AstroAlpaca 🦙": "en-US-GuyNeural",
53
+ "CyberCoyote 🐺": "en-GB-RyanNeural",
54
+ "MysticMoose 🦌": "en-AU-WilliamNeural",
55
+ "GlitchGnome 🧚": "en-CA-LiamNeural",
56
+ "VortexViper 🐍": "en-US-AriaNeural",
57
+ "ChronoChimp 🐒": "en-US-JennyNeural"
58
+ }
59
+
60
+ # Directories and files
61
+ CHAT_DIR = "chat_logs"
62
+ VOTE_DIR = "vote_logs"
63
+ STATE_FILE = "user_state.txt"
64
+ AUDIO_DIR = "audio_logs"
65
+ HISTORY_DIR = "history_logs"
66
+ os.makedirs(CHAT_DIR, exist_ok=True)
67
+ os.makedirs(VOTE_DIR, exist_ok=True)
68
+ os.makedirs(AUDIO_DIR, exist_ok=True)
69
  os.makedirs(HISTORY_DIR, exist_ok=True)
70
 
71
+ CHAT_FILE = os.path.join(CHAT_DIR, "global_chat.md")
72
+ QUOTE_VOTES_FILE = os.path.join(VOTE_DIR, "quote_votes.md")
73
+ MEDIA_VOTES_FILE = os.path.join(VOTE_DIR, "media_votes.md")
74
+ HISTORY_FILE = os.path.join(HISTORY_DIR, "chat_history.md")
75
+
76
+ # Unicode digits and fonts
77
+ UNICODE_DIGITS = {i: f"{i}\uFE0F⃣" for i in range(10)}
78
+ UNICODE_FONTS = [
79
+ ("Normal", lambda x: x),
80
+ ("Bold", lambda x: "".join(chr(ord(c) + 0x1D400 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D41A - 0x61) if 'a' <= c <= 'z' else c for c in x)),
81
+ ]
82
+
83
+ server_running = False
84
+ server_task = None
85
+
86
+ # Helper Functions
87
+ def format_timestamp_prefix():
88
+ """📅 - Time Stamp Champ - Marks the clock, no flop!"""
89
+ return datetime.now().strftime("%Y%m%d_%H%M%S")
90
 
 
91
  def get_node_name():
92
+ """🌐 - Naming Node with Code - Spins a name, oh so bold!"""
93
+ action = "🌐 - Naming Node with Code - Spins a name, oh so bold!"
94
+ username = st.session_state.get('username', 'System 🌟')
95
+ log_action(username, action)
96
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
97
+ parser.add_argument('--node-name', type=str, default=None)
98
+ parser.add_argument('--port', type=int, default=8501)
 
 
99
  args = parser.parse_args()
100
+ return args.node_name or f"node-{uuid.uuid4().hex[:8]}", args.port
101
+
102
+ def log_action(username, action):
103
+ """📜 - Log Jog Blog - Tracks the deed, no greed!"""
104
+ if 'action_log' not in st.session_state:
105
+ st.session_state.action_log = {}
106
+ user_log = st.session_state.action_log.setdefault(username, {})
107
+ current_time = time.time()
108
+ user_log = {k: v for k, v in user_log.items() if current_time - v < 10}
109
+ st.session_state.action_log[username] = user_log
110
+ if action not in user_log:
111
+ with open(HISTORY_FILE, 'a') as f:
112
+ f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {username}: {action}\n")
113
+ user_log[action] = current_time
114
+
115
+ async def save_chat_entry(username, message):
116
+ """📝 - Chat Snap Trap - Logs your yap, no cap! ✍️🧠"""
117
+ action = "📝 - Chat Snap Trap - Logs your yap, no cap! ✍️🧠"
118
+ await asyncio.to_thread(log_action, username, action)
119
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
120
+ entry = f"[{timestamp}] {username}: {message}"
121
+ await asyncio.to_thread(lambda: open(CHAT_FILE, 'a').write(f"{entry}\n"))
122
+ # Generate and save audio for the message
123
+ voice = FUN_USERNAMES.get(username, "en-US-AriaNeural")
124
+ audio_file = await async_edge_tts_generate(message, voice)
125
+ if audio_file:
126
+ with open(HISTORY_FILE, 'a') as f:
127
+ f.write(f"[{timestamp}] {username}: Audio generated - {audio_file}\n")
128
+
129
+ async def load_chat():
130
+ """📖 - Chat Fetch Quest - Grabs the log, no jest!"""
131
+ action = "📖 - Chat Fetch Quest - Grabs the log, no jest!"
132
+ username = st.session_state.get('username', 'System 🌟')
133
+ await asyncio.to_thread(log_action, username, action)
134
+ if not os.path.exists(CHAT_FILE):
135
+ await asyncio.to_thread(lambda: open(CHAT_FILE, 'w').write(f"# {START_ROOM} Chat\n\nWelcome to the cosmic hub - start chatting! 🎤\n"))
136
+ with open(CHAT_FILE, 'r') as f:
137
+ content = await asyncio.to_thread(f.read)
138
+ return content
139
+
140
+ async def get_user_list(chat_content):
141
+ """👥 - Crew Clue Brew - Spots who’s who in the crew!"""
142
+ action = "👥 - Crew Clue Brew - Spots who’s who in the crew!"
143
+ username = st.session_state.get('username', 'System 🌟')
144
+ await asyncio.to_thread(log_action, username, action)
145
+ users = set()
146
+ for line in chat_content.split('\n'):
147
+ if line.strip() and ': ' in line:
148
+ user = line.split(': ')[1].split(' ')[0]
149
+ users.add(user)
150
+ return sorted(list(users))
151
+
152
+ async def has_joined_before(client_id, chat_content):
153
+ """🚪 - Join Check Trek - Sees who’s back, no wreck!"""
154
+ action = "🚪 - Join Check Trek - Sees who’s back, no wreck!"
155
+ username = st.session_state.get('username', 'System 🌟')
156
+ await asyncio.to_thread(log_action, username, action)
157
+ return any(f"Client-{client_id} has joined" in line for line in chat_content.split('\n'))
158
+
159
+ async def get_message_suggestions(chat_content, prefix):
160
+ """🔍 - Suggest Jest Chest - Finds old quips, the best!"""
161
+ action = "🔍 - Suggest Jest Chest - Finds old quips, the best!"
162
+ username = st.session_state.get('username', 'System 🌟')
163
+ await asyncio.to_thread(log_action, username, action)
164
+ lines = chat_content.split('\n')
165
+ messages = [line.split(': ', 1)[1] for line in lines if ': ' in line and line.strip()]
166
+ return [msg for msg in messages if msg.lower().startswith(prefix.lower())][:5]
167
+
168
+ async def save_vote(file, item, user_hash, username, comment=""):
169
+ """👍 - Vote Note Float - Cheers rise, we gloat!"""
170
+ action = "👍 - Vote Note Float - Cheers rise, we gloat!"
171
+ await asyncio.to_thread(log_action, username, action)
172
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
173
+ entry = f"[{timestamp}] {user_hash} voted for {item}"
174
+ await asyncio.to_thread(lambda: open(file, 'a').write(f"{entry}\n"))
175
+ await asyncio.to_thread(lambda: open(HISTORY_FILE, "a").write(f"- {timestamp} - User {user_hash} voted for {item}\n"))
176
+ chat_message = f"{username} upvoted: \"{item}\""
177
+ if comment:
178
+ chat_message += f" - {comment}"
179
+ await save_chat_entry(username, chat_message)
180
+
181
+ async def load_votes(file):
182
+ """🏆 - Tally Rally Call - Counts the cheers, no stall!"""
183
+ action = "🏆 - Tally Rally Call - Counts the cheers, no stall!"
184
+ username = st.session_state.get('username', 'System 🌟')
185
+ await asyncio.to_thread(log_action, username, action)
186
+ if not os.path.exists(file):
187
+ await asyncio.to_thread(lambda: open(file, 'w').write("# Vote Tally\n\nNo votes yet - get clicking! 🖱️\n"))
188
+ with open(file, 'r') as f:
189
+ lines = await asyncio.to_thread(f.read.strip().split, '\n')[2:]
190
+ votes = {}
191
+ user_votes = set()
192
+ for line in lines:
193
+ if line.strip() and 'voted for' in line:
194
+ user_hash = line.split('] ')[1].split(' voted for ')[0]
195
+ item = line.split('voted for ')[1]
196
+ vote_key = f"{user_hash}-{item}"
197
+ if vote_key not in user_votes:
198
+ votes[item] = votes.get(item, 0) + 1
199
+ user_votes.add(vote_key)
200
+ return votes
201
+
202
+ async def generate_user_hash():
203
+ """🔑 - Hash Dash Bash - Crafts a code, so brash!"""
204
+ action = "🔑 - Hash Dash Bash - Crafts a code, so brash!"
205
+ username = st.session_state.get('username', 'System 🌟')
206
+ await asyncio.to_thread(log_action, username, action)
207
+ if 'user_hash' not in st.session_state:
208
+ st.session_state.user_hash = hashlib.md5(str(random.getrandbits(128)).encode()).hexdigest()[:8]
209
+ return st.session_state.user_hash
210
+
211
+ async def async_edge_tts_generate(text, voice, rate=0, pitch=0, file_format="mp3"):
212
+ """🎶 - Tune Moon Boom - Drops a beat, so groom!"""
213
+ action = "🎶 - Tune Moon Boom - Drops a beat, so groom!"
214
+ username = st.session_state.get('username', 'System 🌟')
215
+ await asyncio.to_thread(log_action, username, action)
216
+ timestamp = format_timestamp_prefix()
217
+ filename = os.path.join(AUDIO_DIR, f"audio_{timestamp}_{random.randint(1000, 9999)}.mp3")
218
+ communicate = edge_tts.Communicate(text, voice, rate=f"{rate:+d}%", pitch=f"{pitch:+d}Hz")
219
+ await communicate.save(filename)
220
+ return filename if os.path.exists(filename) else None
221
+
222
+ def play_and_download_audio(file_path):
223
+ """🔊 - Sound Pound Ground - Plays it loud, all around!"""
224
+ if file_path and os.path.exists(file_path):
225
+ st.audio(file_path)
226
+ with open(file_path, "rb") as f:
227
+ b64 = base64.b64encode(f.read()).decode()
228
+ dl_link = f'<a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file_path)}">🎵 Download {os.path.basename(file_path)}</a>'
229
+ st.markdown(dl_link, unsafe_allow_html=True)
230
+
231
+ async def save_pasted_image(image_data):
232
+ """📸 - Snap Cap Trap - Saves your pic, no flap!"""
233
+ action = "📸 - Snap Cap Trap - Saves your pic, no flap!"
234
+ username = st.session_state.get('username', 'System 🌟')
235
+ await asyncio.to_thread(log_action, username, action)
236
+ timestamp = format_timestamp_prefix()
237
+ filename = f"paste_{timestamp}.png"
238
+ filepath = os.path.join('./', filename)
239
+ if ',' in image_data:
240
+ image_data = image_data.split(',')[1]
241
+ img_bytes = base64.b64decode(image_data)
242
+ img = Image.open(io.BytesIO(img_bytes))
243
+ await asyncio.to_thread(img.save, filepath, "PNG")
244
+ return filename
245
+
246
+ async def get_video_html(video_path, width="100%"):
247
+ """🎥 - Reel Deal Steal - Plays your flick, so real!"""
248
+ action = "🎥 - Reel Deal Steal - Plays your flick, so real!"
249
+ username = st.session_state.get('username', 'System 🌟')
250
+ await asyncio.to_thread(log_action, username, action)
251
+ video_url = f"data:video/mp4;base64,{base64.b64encode(await asyncio.to_thread(open, video_path, 'rb').read()).decode()}"
252
+ return f'<video width="{width}" controls autoplay muted loop><source src="{video_url}" type="video/mp4">Your browser does not support the video tag.</video>'
253
+
254
+ async def get_audio_html(audio_path, width="100%"):
255
+ """🎶 - Tune Moon Boom - Drops a beat, so groom!"""
256
+ action = "🎶 - Tune Moon Boom - Drops a beat, so groom!"
257
+ username = st.session_state.get('username', 'System 🌟')
258
+ await asyncio.to_thread(log_action, username, action)
259
+ audio_url = f"data:audio/mpeg;base64,{base64.b64encode(await asyncio.to_thread(open, audio_path, 'rb').read()).decode()}"
260
+ return f'<audio controls style="width: {width};"><source src="{audio_url}" type="audio/mpeg">Your browser does not support the audio element.</audio>'
261
+
262
+ active_connections = {}
263
+
264
+ async def websocket_handler(websocket, path):
265
+ """🌐 - Web Sock Jock - Links the chat, no block!"""
266
+ action = "🌐 - Web Sock Jock - Links the chat, no block!"
267
+ username = st.session_state.get('username', 'System 🌟')
268
+ await asyncio.to_thread(log_action, username, action)
269
  try:
270
+ client_id = str(uuid.uuid4())
271
+ room_id = "chat"
272
+ active_connections.setdefault(room_id, {})[client_id] = websocket
273
+ chat_content = await load_chat()
274
+ username = st.session_state.get('username', random.choice(list(FUN_USERNAMES.keys())))
275
+ if not await has_joined_before(client_id, chat_content):
276
+ await save_chat_entry(f"Client-{client_id}", f"{username} has joined {START_ROOM}!")
277
+ async for message in websocket:
278
+ parts = message.split('|', 1)
279
+ if len(parts) == 2:
280
+ username, content = parts
281
+ await save_chat_entry(username, content)
282
+ await broadcast_message(f"{username}|{content}", room_id)
283
+ except websockets.ConnectionClosed:
284
  pass
285
+ finally:
286
+ if room_id in active_connections and client_id in active_connections[room_id]:
287
+ del active_connections[room_id][client_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  async def broadcast_message(message, room_id):
290
+ """📢 - Shout Out Bout - Blasts the word, no doubt!"""
291
+ action = "📢 - Shout Out Bout - Blasts the word, no doubt!"
292
+ username = st.session_state.get('username', 'System 🌟')
293
+ await asyncio.to_thread(log_action, username, action)
294
  if room_id in active_connections:
295
+ disconnected = []
296
+ for client_id, ws in active_connections[room_id].items():
 
297
  try:
298
+ await ws.send(message)
299
+ except websockets.ConnectionClosed:
300
+ disconnected.append(client_id)
301
+ for client_id in disconnected:
 
 
302
  del active_connections[room_id][client_id]
303
 
304
+ async def run_websocket_server():
305
+ """🖥️ - Server Ferver Verve - Spins the web, with nerve!"""
306
+ action = "🖥️ - Server Ferver Verve - Spins the web, with nerve!"
307
+ username = st.session_state.get('username', 'System 🌟')
308
+ await asyncio.to_thread(log_action, username, action)
309
+ global server_running, server_task
310
+ if not server_running:
311
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
312
+ server_running = True
313
+ await server.wait_closed()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
+ async def process_voice_input(audio_bytes):
316
+ """🎤 - Voice Choice Rejoice - Speaks your mind, oh boy!"""
317
+ action = "🎤 - Voice Choice Rejoice - Speaks your mind, oh boy!"
318
+ username = st.session_state.get('username', 'System 🌟')
319
+ await asyncio.to_thread(log_action, username, action)
320
+ if audio_bytes:
321
+ # Simple placeholder for speech-to-text (requires additional library like `speech_recognition`)
322
+ # For now, we'll simulate text input from audio
323
+ text = "Voice input simulation" # Replace with actual speech-to-text logic
324
+ await save_chat_entry(username, text)
325
+
326
+ async def create_streamlit_interface():
327
+ """🎨 - UI Brew Crew - Builds the view, so new!"""
328
+ action = "🎨 - UI Brew Crew - Builds the view, so new!"
329
+ username = st.session_state.get('username', 'System 🌟')
330
+ await asyncio.to_thread(log_action, username, action)
331
+
332
+ if 'username' not in st.session_state:
333
+ chat_content = await load_chat()
334
+ available_names = [name for name in FUN_USERNAMES if not any(f"{name} has joined" in line for line in chat_content.split('\n'))]
335
+ st.session_state.username = random.choice(available_names) if available_names else random.choice(list(FUN_USERNAMES.keys()))
336
+
337
+ if 'refresh_rate' not in st.session_state:
338
+ st.session_state.refresh_rate = 5
339
+ if 'timer_start' not in st.session_state:
340
+ st.session_state.timer_start = time.time()
341
+ if 'quote_line' not in st.session_state:
342
+ st.session_state.quote_line = None
343
+ if 'pasted_image_data' not in st.session_state:
344
+ st.session_state.pasted_image_data = None
345
+ if 'message_text' not in st.session_state:
346
+ st.session_state.message_text = ""
347
+ if 'audio_cache' not in st.session_state:
348
+ st.session_state.audio_cache = {}
349
+ if 'chat_history' not in st.session_state:
350
+ st.session_state.chat_history = []
351
+
352
+ st.markdown("""
353
+ <style>
354
+ .chat-box {font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 5px; height: 300px; overflow-y: auto;}
355
+ .timer {font-size: 24px; color: #ffcc00; text-align: center; animation: pulse 1s infinite;}
356
+ @keyframes pulse {0% {transform: scale(1);} 50% {transform: scale(1.1);} 100% {transform: scale(1);}}
357
+ #paste-target {border: 2px dashed #ccc; padding: 20px; text-align: center; cursor: pointer;}
358
+ </style>
359
+ """, unsafe_allow_html=True)
360
+
361
+ st.title(f"🤖🧠MMO {st.session_state.username}📝🔬")
362
+ st.markdown(f"Welcome to {START_ROOM} - chat, vote, upload, paste images, and enjoy quoting! 🎉")
363
+
364
+ # Voice Input
365
+ audio_bytes = audio_recorder()
366
+ if audio_bytes:
367
+ await process_voice_input(audio_bytes)
368
+ st.rerun()
369
+
370
+ # Chat Section
371
+ st.subheader(f"{START_ROOM} Chat 💬")
372
+ chat_content = await load_chat()
373
+ chat_lines = chat_content.split('\n')
374
+ chat_votes = await load_votes(QUOTE_VOTES_FILE)
375
+ for i, line in enumerate(chat_lines):
376
+ if line.strip() and ': ' in line:
377
+ col1, col2, col3 = st.columns([4, 1, 1])
378
+ with col1:
379
+ st.markdown(line)
380
+ username = line.split(': ')[1].split(' ')[0]
381
+ audio_file = None
382
+ cache_key = f"{line}_{FUN_USERNAMES.get(username, 'en-US-AriaNeural')}"
383
+ if cache_key in st.session_state.audio_cache:
384
+ audio_file = st.session_state.audio_cache[cache_key]
385
+ else:
386
+ audio_file = await async_edge_tts_generate(line.split(': ', 1)[1], FUN_USERNAMES.get(username, "en-US-AriaNeural"))
387
+ st.session_state.audio_cache[cache_key] = audio_file
388
+ if audio_file:
389
+ play_and_download_audio(audio_file)
390
+ with col2:
391
+ vote_count = chat_votes.get(line.split('. ')[1] if '. ' in line else line, 0)
392
+ if st.button(f"👍 {vote_count}", key=f"chat_vote_{i}"):
393
+ comment = st.session_state.message_text
394
+ await save_vote(QUOTE_VOTES_FILE, line.split('. ')[1] if '. ' in line else line, await generate_user_hash(), st.session_state.username, comment)
395
+ if st.session_state.pasted_image_data:
396
+ filename = await save_pasted_image(st.session_state.pasted_image_data)
397
+ if filename:
398
+ await save_chat_entry(st.session_state.username, f"Pasted image: {filename}")
399
+ st.session_state.pasted_image_data = None
400
+ st.session_state.message_text = ''
401
+ st.rerun()
402
+ with col3:
403
+ if st.button("📢 Quote", key=f"quote_{i}"):
404
+ st.session_state.quote_line = line
405
+ st.rerun()
406
+
407
+ # Quoting Section
408
+ if 'quote_line' in st.session_state:
409
+ st.markdown(f"### Quoting: {st.session_state.quote_line}")
410
+ quote_response = st.text_area("Add your response", key="quote_response")
411
+ if st.button("Send Quote 🚀", key="send_quote"):
412
+ async def process_quote():
413
+ """📢 - Quote Float Boat - Echoes chat, we gloat!"""
414
+ action = "📢 - Quote Float Boat - Echoes chat, we gloat!"
415
+ await asyncio.to_thread(log_action, st.session_state.username, action)
416
+ markdown_response = f"### Quote Response\n- **Original**: {st.session_state.quote_line}\n- **{st.session_state.username} Replies**: {quote_response}"
417
+ if st.session_state.pasted_image_data:
418
+ filename = await save_pasted_image(st.session_state.pasted_image_data)
419
+ if filename:
420
+ markdown_response += f"\n- **Image**: ![Pasted Image]({filename})"
421
+ st.session_state.pasted_image_data = None
422
+ await save_chat_entry(st.session_state.username, markdown_response)
423
+ await process_quote()
424
+ del st.session_state.quote_line
425
+ st.session_state.message_text = ''
426
+ st.rerun()
427
+
428
+ # Username Change Dropdown
429
+ new_username = st.selectbox("Change Name", [""] + list(FUN_USERNAMES.keys()), index=0)
430
+ if new_username and new_username != st.session_state.username:
431
+ await save_chat_entry("System 🌟", f"{st.session_state.username} changed name to {new_username}")
432
+ st.session_state.username = new_username
433
+ st.rerun()
434
+
435
+ # Message Input
436
+ message = st.text_input(f"Message as {st.session_state.username}", key="message_input", value=st.session_state.message_text, on_change=lambda: st.session_state.update(message_text=st.session_state.message_input))
437
+ if st.button("Send 🚀", key="send_button") and message.strip():
438
+ await save_chat_entry(st.session_state.username, message)
439
+ if st.session_state.pasted_image_data:
440
+ filename = await save_pasted_image(st.session_state.pasted_image_data)
441
+ if filename:
442
+ await save_chat_entry(st.session_state.username, f"Pasted image: {filename}")
443
+ st.session_state.pasted_image_data = None
444
+ st.session_state.message_text = ''
445
+ st.rerun()
446
+
447
+ # Paste Target
448
+ paste_component = components.html(
449
+ """
450
+ <div id="paste-target">Paste an image here (Ctrl+V)</div>
451
+ <script>
452
+ const pasteTarget = document.getElementById('paste-target');
453
+ pasteTarget.addEventListener('paste', (event) => {
454
+ const items = (event.clipboardData || window.clipboardData).items;
455
+ for (let i = 0; i < items.length; i++) {
456
+ if (items[i].type.indexOf('image') !== -1) {
457
+ const blob = items[i].getAsFile();
458
+ const reader = new FileReader();
459
+ reader.onload = (e) => {
460
+ window.parent.postMessage({
461
+ type: 'streamlit:setComponentValue',
462
+ value: e.target.result
463
+ }, '*');
464
+ pasteTarget.innerHTML = '<p>Image pasted! Processing...</p>';
465
+ };
466
+ reader.readAsDataURL(blob);
467
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
+ event.preventDefault();
470
+ });
471
+ </script>
472
+ """,
473
+ height=100
474
+ )
475
+
476
+ # Media Section
477
+ st.subheader("Media Gallery 🎨🎶🎥")
478
+ uploaded_file = st.file_uploader("Upload Media", type=['png', 'jpg', 'mp3', 'mp4'])
479
+ if uploaded_file:
480
+ file_path = os.path.join('./', uploaded_file.name)
481
+ await asyncio.to_thread(lambda: open(file_path, 'wb').write(uploaded_file.getbuffer()))
482
+ st.success(f"Uploaded {uploaded_file.name}")
483
+
484
+ media_files = glob.glob("./*.png") + glob.glob("./*.jpg") + glob.glob("./*.mp3") + glob.glob("./*.mp4")
485
+ if media_files:
486
+ cols = st.columns(3)
487
+ media_votes = await load_votes(MEDIA_VOTES_FILE)
488
+ for idx, media_file in enumerate(media_files):
489
+ vote_count = media_votes.get(media_file, 0)
490
+ if vote_count > 0:
491
+ with cols[idx % 3]:
492
+ if media_file.endswith(('.png', '.jpg')):
493
+ st.image(media_file, use_container_width=True)
494
+ elif media_file.endswith('.mp3'):
495
+ st.markdown(await get_audio_html(media_file), unsafe_allow_html=True)
496
+ elif media_file.endswith('.mp4'):
497
+ st.markdown(await get_video_html(media_file), unsafe_allow_html=True)
498
+ col1, col2 = st.columns(2)
499
+ with col1:
500
+ if st.button(f"👍 {vote_count}", key=f"media_vote_{idx}"):
501
+ comment = st.session_state.message_text
502
+ await save_vote(MEDIA_VOTES_FILE, media_file, await generate_user_hash(), st.session_state.username, comment)
503
+ if st.session_state.pasted_image_data:
504
+ filename = await save_pasted_image(st.session_state.pasted_image_data)
505
+ if filename:
506
+ await save_chat_entry(st.session_state.username, f"Pasted image: {filename}")
507
+ st.session_state.pasted_image_data = None
508
+ st.session_state.message_text = ''
509
+ st.rerun()
510
+ with col2:
511
+ if st.button("🗑️", key=f"media_delete_{idx}"):
512
+ await asyncio.to_thread(os.remove, media_file)
513
+ st.rerun()
514
+
515
+ # Refresh Timer
516
+ st.subheader("Refresh ⏳")
517
+ refresh_rate = st.slider("Refresh Rate", 1, 300, st.session_state.refresh_rate)
518
+ st.session_state.refresh_rate = refresh_rate
519
+ timer_placeholder = st.empty()
520
+ for i in range(st.session_state.refresh_rate, -1, -1):
521
+ font_name, font_func = random.choice(UNICODE_FONTS)
522
+ countdown_str = "".join(UNICODE_DIGITS[int(d)] for d in str(i)) if i < 10 else font_func(str(i))
523
+ timer_placeholder.markdown(f"<p class='timer'>⏳ {font_func('Refresh in:')} {countdown_str}</p>", unsafe_allow_html=True)
524
+ await asyncio.sleep(1)
525
+ st.rerun()
526
+
527
+ # Sidebar History
528
+ st.sidebar.subheader("Chat History 📜")
529
+ with open(HISTORY_FILE, 'r') as f:
530
+ history_content = f.read()
531
+ st.sidebar.markdown(history_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
  async def main():
534
+ """🎮 - Game Fame Claim - Starts the fun, no shame!"""
535
+ action = "🎮 - Game Fame Claim - Starts the fun, no shame!"
536
+ username = st.session_state.get('username', 'System 🌟')
537
+ await asyncio.to_thread(log_action, username, action)
538
+ global NODE_NAME, server_task
539
+ NODE_NAME, port = await get_node_name()
540
+ if server_task is None:
541
+ server_task = asyncio.create_task(run_websocket_server())
542
+ await create_streamlit_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
  if __name__ == "__main__":
545
  asyncio.run(main())