awacke1 commited on
Commit
eb41de1
·
verified ·
1 Parent(s): 69a7866

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +759 -513
app.py CHANGED
@@ -1,545 +1,791 @@
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 = get_node_name() # Synchronous call, no await needed
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())
 
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
+
57
  args = parser.parse_args()
58
+ node_name = args.node_name
59
+ port = args.port
60
+
61
+ # If no node name specified, generate a random one
62
+ if not node_name:
63
+ node_name = f"node-{uuid.uuid4().hex[:8]}"
64
+
65
+ return node_name, port
66
+
67
+ def get_room_history_file(room_id):
68
+ """Get the filename for a room's history."""
69
+ # Create timestamp-based log files
70
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
71
+ return os.path.join(HISTORY_DIR, f"{room_id}_{timestamp}.jsonl")
72
+
73
+ def get_all_room_history_files(room_id):
74
+ """Get all history files for a specific room."""
75
+ files = []
76
+ for file in os.listdir(HISTORY_DIR):
77
+ if file.startswith(f"{room_id}_") and file.endswith(".jsonl"):
78
+ files.append(os.path.join(HISTORY_DIR, file))
79
+ # Sort by modification time (newest first)
80
+ files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
81
+ return files
82
+
83
+ def get_all_history_files():
84
+ """Get a list of all unique room IDs from history files, sorted by modification time (newest first)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  try:
86
+ rooms = {} # room_id -> (newest_file_path, mod_time)
87
+
88
+ for file in os.listdir(HISTORY_DIR):
89
+ if file.endswith(".jsonl"):
90
+ file_path = os.path.join(HISTORY_DIR, file)
91
+ mod_time = os.path.getmtime(file_path)
92
+
93
+ # Extract room_id from filename (format: roomid_timestamp.jsonl)
94
+ parts = file.split('_', 1)
95
+ if len(parts) > 0:
96
+ room_id = parts[0]
97
+
98
+ # Keep track of newest file for each room
99
+ if room_id not in rooms or mod_time > rooms[room_id][1]:
100
+ rooms[room_id] = (file_path, mod_time)
101
+
102
+ # Convert to list and sort by modification time
103
+ history_files = [(room_id, file_path, mod_time) for room_id, (file_path, mod_time) in rooms.items()]
104
+ history_files.sort(key=lambda x: x[2], reverse=True)
105
+
106
+ return history_files
107
+ except Exception as e:
108
+ logger.error(f"Error in get_all_history_files: {e}")
109
+ return [] # Return empty list on error
110
+
111
+ def load_room_history(room_id):
112
+ """Load chat history for a room from all persistent storage files."""
113
+ if room_id not in chat_history:
114
+ chat_history[room_id] = []
115
+
116
+ # Get all history files for this room
117
+ history_files = get_all_room_history_files(room_id)
118
+
119
+ # Track file modification times
120
+ for file in history_files:
121
+ if file not in file_modification_times:
122
+ file_modification_times[file] = os.path.getmtime(file)
123
+
124
+ # Load messages from all files
125
+ messages = []
126
+ for history_file in history_files:
127
+ try:
128
+ with open(history_file, 'r') as f:
129
+ for line in f:
130
+ line = line.strip()
131
+ if line: # Skip empty lines
132
+ try:
133
+ data = json.loads(line)
134
+ messages.append(data)
135
+ except json.JSONDecodeError:
136
+ logger.error(f"Error parsing JSON line in {history_file}")
137
+ except Exception as e:
138
+ logger.error(f"Error loading history from {history_file}: {e}")
139
+
140
+ # Sort by timestamp
141
+ messages.sort(key=lambda x: x.get("timestamp", ""), reverse=False)
142
+ chat_history[room_id] = messages
143
+
144
+ logger.info(f"Loaded {len(messages)} messages from {len(history_files)} files for room {room_id}")
145
+
146
+ # Track users in this sector
147
+ if room_id not in sector_users:
148
+ sector_users[room_id] = set()
149
+
150
+ return chat_history[room_id]
151
+
152
+ def save_message_to_history(room_id, message):
153
+ """Save a single message to the newest history file for a room."""
154
+ # Get the newest history file or create a new one
155
+ history_files = get_all_room_history_files(room_id)
156
+
157
+ if not history_files:
158
+ # Create a new file
159
+ history_file = get_room_history_file(room_id)
160
+ else:
161
+ # Use the newest file if it's less than 1 MB, otherwise create a new one
162
+ newest_file = history_files[0]
163
+ if os.path.getsize(newest_file) > 1024 * 1024: # 1 MB
164
+ history_file = get_room_history_file(room_id)
165
+ else:
166
+ history_file = newest_file
167
+
168
+ try:
169
+ # Append the message as a single line of JSON
170
+ with open(history_file, 'a') as f:
171
+ f.write(json.dumps(message) + '\n')
172
+
173
+ # Update modification time
174
+ file_modification_times[history_file] = os.path.getmtime(history_file)
175
+
176
+ logger.debug(f"Saved message to {history_file}")
177
+ except Exception as e:
178
+ logger.error(f"Error saving message to {history_file}: {e}")
179
+
180
+ def check_for_new_messages():
181
+ """Check for new messages in all history files."""
182
+ updated_rooms = set()
183
+
184
+ # Check all files in the history directory
185
+ for file in os.listdir(HISTORY_DIR):
186
+ if file.endswith(".jsonl"):
187
+ file_path = os.path.join(HISTORY_DIR, file)
188
+ current_mtime = os.path.getmtime(file_path)
189
+
190
+ # Check if this file is new or has been modified
191
+ if file_path not in file_modification_times or current_mtime > file_modification_times[file_path]:
192
+ # Extract room_id from filename
193
+ parts = file.split('_', 1)
194
+ if len(parts) > 0:
195
+ room_id = parts[0]
196
+ updated_rooms.add(room_id)
197
+
198
+ # Update tracked modification time
199
+ file_modification_times[file_path] = current_mtime
200
+
201
+ # Reload history for updated rooms
202
+ for room_id in updated_rooms:
203
+ if room_id in chat_history:
204
+ # Remember we had this room loaded
205
+ old_history_len = len(chat_history[room_id])
206
+ # Clear and reload
207
+ chat_history[room_id] = []
208
+ load_room_history(room_id)
209
+ new_history_len = len(chat_history[room_id])
210
+
211
+ if new_history_len > old_history_len:
212
+ logger.info(f"Found {new_history_len - old_history_len} new messages for room {room_id}")
213
+
214
+ return updated_rooms
215
+
216
+ def get_sector_coordinates(room_id):
217
+ """Convert a room ID to grid coordinates, or assign new ones."""
218
+ try:
219
+ # Try to parse room ID as "x,y"
220
+ if ',' in room_id:
221
+ x, y = map(int, room_id.split(','))
222
+ return max(0, min(x, GRID_WIDTH-1)), max(0, min(y, GRID_HEIGHT-1))
223
+ except:
224
  pass
225
+
226
+ # Hash the room_id string to get stable coordinates
227
+ hash_val = hash(room_id)
228
+ x = abs(hash_val) % GRID_WIDTH
229
+ y = abs(hash_val >> 8) % GRID_HEIGHT
230
+
231
+ return x, y
232
+
233
+ def generate_sector_map():
234
+ """Generate an ASCII representation of the sector map."""
235
+ # Initialize empty grid
236
+ grid = [[' ' for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
237
+
238
+ # Place active rooms with user counts
239
+ for room_id, users in sector_users.items():
240
+ if users: # Only show rooms with users
241
+ x, y = get_sector_coordinates(room_id)
242
+ user_count = len(users)
243
+ grid[y][x] = str(min(user_count, 9)) if user_count < 10 else '+'
244
+
245
+ # Create ASCII representation
246
+ header = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
247
+ map_str = header + '\n'
248
+
249
+ for y in range(GRID_HEIGHT):
250
+ row = f"{y % 10}|"
251
+ for x in range(GRID_WIDTH):
252
+ row += grid[y][x]
253
+ row += '|'
254
+ map_str += row + '\n'
255
+
256
+ footer = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
257
+ map_str += footer
258
+
259
+ return f"```\n{map_str}\n```\n\nLegend: Number indicates users in sector. '+' means 10+ users."
260
+
261
+ def list_available_rooms():
262
+ """List all available chat rooms with their last activity time and user count."""
263
+ try:
264
+ history_files = get_all_history_files()
265
+
266
+ if not history_files:
267
+ return "No chat rooms available yet. Create one by joining a room!"
268
+
269
+ room_list = "### Available Chat Rooms (Sectors)\n\n"
270
+ room_list += "| Room ID | Sector | Users | Last Activity |\n"
271
+ room_list += "|---------|--------|-------|---------------|\n"
272
+
273
+ for room_id, file_path, mod_time in history_files:
274
+ x, y = get_sector_coordinates(room_id)
275
+ user_count = len(sector_users.get(room_id, set()))
276
+ last_activity = datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
277
+ room_list += f"| {room_id} | ({x},{y}) | {user_count} | {last_activity} |\n"
278
+
279
+ room_list += "\n\n### Sector Map\n\n" + generate_sector_map()
280
+
281
+ return room_list
282
+ except Exception as e:
283
+ logger.error(f"Error in list_available_rooms: {e}")
284
+ return f"Error listing rooms: {str(e)}"
285
 
286
  async def broadcast_message(message, room_id):
287
+ """Broadcast a message to all clients in a room."""
 
 
 
288
  if room_id in active_connections:
289
+ disconnected_clients = []
290
+
291
+ for client_id, websocket in active_connections[room_id].items():
292
  try:
293
+ await websocket.send(json.dumps(message))
294
+ except websockets.exceptions.ConnectionClosed:
295
+ disconnected_clients.append(client_id)
296
+
297
+ # Clean up disconnected clients
298
+ for client_id in disconnected_clients:
299
  del active_connections[room_id][client_id]
300
 
301
+ async def start_websocket_server(host='0.0.0.0', port=8765):
302
+ """Start the WebSocket server."""
303
+ server = await websockets.serve(websocket_handler, host, port)
304
+ logger.info(f"WebSocket server started on ws://{host}:{port}")
305
+ return server
306
+
307
+ def send_message(message, username, room_id):
308
+ """Function to send a message from the Gradio interface."""
309
+ if not message.strip():
310
+ return None
311
+
312
+ global message_queue
313
+
314
+ msg_data = {
315
+ "type": "chat",
316
+ "content": message,
317
+ "username": username,
318
+ "room_id": room_id
319
+ }
320
+
321
+ # Add to queue for processing by the main loop
322
+ message_queue.append(msg_data)
323
+
324
+ # Format the message for display in the UI
325
+ formatted_msg = f"{username}: {message}"
326
+ return formatted_msg
327
+
328
+ def send_clear_command():
329
+ """Send a command to clear all chat history."""
330
+ global message_queue
331
+
332
+ msg_data = {
333
+ "type": "command",
334
+ "command": "clear_history",
335
+ "username": "System"
336
+ }
337
+
338
+ # Add to queue for processing by the main loop
339
+ message_queue.append(msg_data)
340
+
341
+ return "🧹 Clearing all chat history..."
342
+
343
+ async def clear_all_history():
344
+ """Clear all chat history for all rooms."""
345
+ global chat_history, sector_users
346
+
347
+ # Clear in-memory history
348
+ chat_history = {}
349
+ sector_users = {}
350
+
351
+ # Delete all history files except README.md
352
+ for file in os.listdir(HISTORY_DIR):
353
+ if file.endswith(".jsonl"):
354
+ try:
355
+ os.remove(os.path.join(HISTORY_DIR, file))
356
+ except Exception as e:
357
+ logger.error(f"Error removing file {file}: {e}")
358
+
359
+ # Broadcast clear message to all rooms
360
+ clear_msg = {
361
+ "type": "system",
362
+ "content": "🧹 All chat history has been cleared by a user",
363
+ "timestamp": datetime.now().isoformat(),
364
+ "sender": "system"
365
+ }
366
+
367
+ for room_id in list(active_connections.keys()):
368
+ clear_msg["room_id"] = room_id
369
+ await broadcast_message(clear_msg, room_id)
370
+
371
+ logger.info("All chat history cleared")
372
+ return "All chat history cleared"
373
+
374
+ def join_room(room_id, chat_history_output):
375
+ """Join a specific chat room."""
376
+ if not room_id.strip():
377
+ return "Please enter a valid room ID", chat_history_output
378
+
379
+ # Sanitize the room ID
380
+ room_id = urllib.parse.quote(room_id.strip())
381
+
382
+ # Load room history from persistent storage
383
+ history = load_room_history(room_id)
384
+
385
+ # Get sector coordinates
386
+ x, y = get_sector_coordinates(room_id)
387
+
388
+ # Format existing messages
389
+ formatted_history = [f"You are now in Sector ({x},{y}) - Room ID: {room_id}"]
390
+ formatted_history.append(f"Sector Map:\n{generate_sector_map()}")
391
+
392
+ for msg in history:
393
+ if msg.get("type") == "chat":
394
+ sender_node = f" [{msg.get('sender_node', 'unknown')}]" if "sender_node" in msg else ""
395
+ time_str = ""
396
+ if "timestamp" in msg:
397
+ try:
398
+ dt = datetime.fromisoformat(msg["timestamp"])
399
+ time_str = f"[{dt.strftime('%H:%M:%S')}] "
400
+ except:
401
+ pass
402
+ formatted_history.append(f"{time_str}{msg.get('username', 'Anonymous')}{sender_node}: {msg.get('content', '')}")
403
+ elif msg.get("type") == "system":
404
+ formatted_history.append(f"System: {msg.get('content', '')}")
405
+
406
+ return f"Joined room: {room_id}", formatted_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
+ async def websocket_handler(websocket, path):
409
+ """Handle WebSocket connections."""
410
+ client_id = str(uuid.uuid4())
411
+ room_id = "default" # Default initialization to avoid reference errors
412
+
413
+ try:
414
+ # Extract room_id from path if present
415
+ path_parts = path.strip('/').split('/')
416
+ room_id = path_parts[0] if path_parts else "default"
417
+
418
+ # Register the new client
419
+ if room_id not in active_connections:
420
+ active_connections[room_id] = {}
421
+
422
+ active_connections[room_id][client_id] = websocket
423
+
424
+ # Add user to sector map
425
+ if room_id not in sector_users:
426
+ sector_users[room_id] = set()
427
+ sector_users[room_id].add(client_id)
428
+
429
+ # Get sector coordinates
430
+ x, y = get_sector_coordinates(room_id)
431
+
432
+ # Load or initialize chat history
433
+ room_history = load_room_history(room_id)
434
+
435
+ # Send welcome message
436
+ welcome_msg = {
437
+ "type": "system",
438
+ "content": f"Welcome to room '{room_id}' (Sector {x},{y})! Connected from node '{NODE_NAME}'",
439
+ "timestamp": datetime.now().isoformat(),
440
+ "sender": "system",
441
+ "room_id": room_id
442
+ }
443
+ await websocket.send(json.dumps(welcome_msg))
444
+
445
+ # Send sector map
446
+ map_msg = {
447
+ "type": "system",
448
+ "content": f"Sector Map:\n{generate_sector_map()}",
449
+ "timestamp": datetime.now().isoformat(),
450
+ "sender": "system",
451
+ "room_id": room_id
452
+ }
453
+ await websocket.send(json.dumps(map_msg))
454
+
455
+ # Send chat history
456
+ for msg in room_history:
457
+ await websocket.send(json.dumps(msg))
458
+
459
+ # Broadcast join notification
460
+ join_msg = {
461
+ "type": "system",
462
+ "content": f"User joined the room (Sector {x},{y}) - {len(sector_users[room_id])} users now present",
463
+ "timestamp": datetime.now().isoformat(),
464
+ "sender": "system",
465
+ "room_id": room_id
466
+ }
467
+ await broadcast_message(join_msg, room_id)
468
+ save_message_to_history(room_id, join_msg)
469
+
470
+ logger.info(f"New client {client_id} connected to room {room_id} (Sector {x},{y})")
471
+
472
+ # Handle messages from this client
473
+ async for message in websocket:
474
+ try:
475
+ data = json.loads(message)
476
+
477
+ # Check for clear command
478
+ if data.get("type") == "command" and data.get("command") == "clear_history":
479
+ result = await clear_all_history()
480
+ continue
481
+
482
+ # Check for map request
483
+ if data.get("type") == "command" and data.get("command") == "show_map":
484
+ map_msg = {
485
+ "type": "system",
486
+ "content": f"Sector Map:\n{generate_sector_map()}",
487
+ "timestamp": datetime.now().isoformat(),
488
+ "sender": "system",
489
+ "room_id": room_id
490
+ }
491
+ await websocket.send(json.dumps(map_msg))
492
+ continue
493
+
494
+ # Add metadata to the message
495
+ data["timestamp"] = datetime.now().isoformat()
496
+ data["sender_node"] = NODE_NAME
497
+ data["room_id"] = room_id
498
+
499
+ # Store in history
500
+ chat_history[room_id].append(data)
501
+ if len(chat_history[room_id]) > 500: # Increased limit to 500 messages
502
+ chat_history[room_id] = chat_history[room_id][-500:]
503
+
504
+ # Save to persistent storage
505
+ save_message_to_history(room_id, data)
506
+
507
+ # Broadcast to all clients in the room
508
+ await broadcast_message(data, room_id)
509
+
510
+ except json.JSONDecodeError:
511
+ error_msg = {
512
+ "type": "error",
513
+ "content": "Invalid JSON format",
514
+ "timestamp": datetime.now().isoformat(),
515
+ "sender": "system",
516
+ "room_id": room_id
517
  }
518
+ await websocket.send(json.dumps(error_msg))
519
+
520
+ except websockets.exceptions.ConnectionClosed:
521
+ logger.info(f"Client {client_id} disconnected from room {room_id}")
522
+ finally:
523
+ # Remove the client when disconnected
524
+ if room_id in active_connections and client_id in active_connections[room_id]:
525
+ del active_connections[room_id][client_id]
526
+
527
+ # Remove user from sector map
528
+ if room_id in sector_users and client_id in sector_users[room_id]:
529
+ sector_users[room_id].remove(client_id)
530
+
531
+ # Get sector coordinates
532
+ x, y = get_sector_coordinates(room_id)
533
+
534
+ # Broadcast leave notification
535
+ leave_msg = {
536
+ "type": "system",
537
+ "content": f"User left the room (Sector {x},{y}) - {len(sector_users.get(room_id, set()))} users remaining",
538
+ "timestamp": datetime.now().isoformat(),
539
+ "sender": "system",
540
+ "room_id": room_id
541
  }
542
+ await broadcast_message(leave_msg, room_id)
543
+ save_message_to_history(room_id, leave_msg)
544
+
545
+ # Clean up empty rooms (but keep history)
546
+ if not active_connections[room_id]:
547
+ del active_connections[room_id]
548
+
549
+ async def process_message_queue():
550
+ """Process messages in the queue and broadcast them."""
551
+ global message_queue
552
+
553
+ while True:
554
+ # Check if there are messages to process
555
+ if message_queue:
556
+ # Get the oldest message
557
+ msg_data = message_queue.pop(0)
558
+
559
+ # Check for clear command
560
+ if msg_data.get("type") == "command" and msg_data.get("command") == "clear_history":
561
+ await clear_all_history()
562
+ elif "room_id" in msg_data:
563
+ # Regular message - add to history and broadcast
564
+ room_id = msg_data["room_id"]
565
+
566
+ # Add timestamp if not present
567
+ if "timestamp" not in msg_data:
568
+ msg_data["timestamp"] = datetime.now().isoformat()
569
+
570
+ # Add sender node if not present
571
+ if "sender_node" not in msg_data:
572
+ msg_data["sender_node"] = NODE_NAME
573
+
574
+ # Store in memory history
575
+ if room_id not in chat_history:
576
+ chat_history[room_id] = []
577
+ chat_history[room_id].append(msg_data)
578
+
579
+ # Save to file
580
+ save_message_to_history(room_id, msg_data)
581
+
582
+ # Broadcast to all clients in the room
583
+ await broadcast_message(msg_data, room_id)
584
+
585
+ # Check for file changes every second
586
+ updated_rooms = check_for_new_messages()
587
+
588
+ # If any rooms were updated, notify clients
589
+ for room_id in updated_rooms:
590
+ if room_id in active_connections:
591
+ # Find the newest messages that clients might not have seen
592
+ # This is a simplification - in a real app, you'd track which messages each client has seen
593
+ if room_id in chat_history and chat_history[room_id]:
594
+ # Get the latest 5 messages as an example
595
+ latest_messages = chat_history[room_id][-5:]
596
+ for msg in latest_messages:
597
+ # Only broadcast messages from other nodes (to avoid duplicates)
598
+ if msg.get("sender_node") != NODE_NAME:
599
+ await broadcast_message(msg, room_id)
600
+
601
+ # Sleep to avoid busy-waiting
602
+ await asyncio.sleep(1.0) # Check every second
603
+
604
+ def create_gradio_interface():
605
+ """Create and return the Gradio interface."""
606
+ with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
607
+ gr.Markdown(f"# Chat Node: {NODE_NAME}")
608
+ gr.Markdown("Join a room by entering a room ID below or create a new one.")
609
+
610
+ # Room list and management
611
+ with gr.Row():
612
+ with gr.Column(scale=3):
613
+ room_list = gr.Markdown(value="Loading available rooms...")
614
+ refresh_button = gr.Button("🔄 Refresh Room List")
615
+ with gr.Column(scale=1):
616
+ clear_button = gr.Button("🧹 Clear All Chat History", variant="stop")
617
+
618
+ # Join room controls with 2D grid input
619
+ with gr.Row():
620
+ with gr.Column(scale=2):
621
+ room_id_input = gr.Textbox(label="Room ID", placeholder="Enter room ID or use x,y coordinates")
622
+ join_button = gr.Button("Join Room")
623
+ with gr.Column(scale=1):
624
+ with gr.Row():
625
+ x_coord = gr.Number(label="X", value=0, minimum=0, maximum=GRID_WIDTH-1, step=1)
626
+ y_coord = gr.Number(label="Y", value=0, minimum=0, maximum=GRID_HEIGHT-1, step=1)
627
+ grid_join_button = gr.Button("Join by Coordinates")
628
+
629
+ # Chat area with multiline support
630
+ chat_history_output = gr.Textbox(label="Chat History", lines=20, max_lines=20)
631
+
632
+ # Message controls with multiline support
633
+ with gr.Row():
634
+ username_input = gr.Textbox(label="Username", placeholder="Enter your username", value="User")
635
+ with gr.Column(scale=3):
636
+ message_input = gr.Textbox(
637
+ label="Message",
638
+ placeholder="Type your message here. Press Shift+Enter for new line, Enter to send.",
639
+ lines=3
640
+ )
641
+ with gr.Column(scale=1):
642
+ send_button = gr.Button("Send")
643
+ map_button = gr.Button("🗺️ Show Map")
644
+
645
+ # Current room display
646
+ current_room_display = gr.Textbox(label="Current Room", value="Not joined any room yet")
647
+
648
+ # Event handlers
649
+ refresh_button.click(
650
+ list_available_rooms,
651
+ inputs=[],
652
+ outputs=[room_list]
653
+ )
654
+
655
+ clear_button.click(
656
+ send_clear_command,
657
+ inputs=[],
658
+ outputs=[room_list]
659
+ )
660
+
661
+ def join_by_coordinates(x, y):
662
+ """Join a room using grid coordinates."""
663
+ room_id = f"{int(x)},{int(y)}"
664
+ return room_id
665
+
666
+ # Link grid coordinates to room ID
667
+ grid_join_button.click(
668
+ join_by_coordinates,
669
+ inputs=[x_coord, y_coord],
670
+ outputs=[room_id_input]
671
+ ).then(
672
+ join_room,
673
+ inputs=[room_id_input, chat_history_output],
674
+ outputs=[current_room_display, chat_history_output]
675
+ )
676
+
677
+ join_button.click(
678
+ join_room,
679
+ inputs=[room_id_input, chat_history_output],
680
+ outputs=[current_room_display, chat_history_output]
681
+ )
682
+
683
+ def send_and_clear(message, username, room_id):
684
+ if not room_id.startswith("Joined room:"):
685
+ return "Please join a room first", message
686
+
687
+ actual_room_id = room_id.replace("Joined room: ", "").strip()
688
+
689
+ # Support for multi-line messages
690
+ message_lines = message.strip().split("\n")
691
+ formatted_msg = ""
692
+
693
+ for line in message_lines:
694
+ if line.strip(): # Skip empty lines
695
+ sent_msg = send_message(line.strip(), username, actual_room_id)
696
+ if sent_msg:
697
+ formatted_msg += sent_msg + "\n"
698
+
699
+ if formatted_msg:
700
+ return "", formatted_msg
701
+ return message, None
702
+
703
+ send_button.click(
704
+ send_and_clear,
705
+ inputs=[message_input, username_input, current_room_display],
706
+ outputs=[message_input, chat_history_output]
707
+ )
708
+
709
+ def show_sector_map(room_id):
710
+ if not room_id.startswith("Joined room:"):
711
+ return "Please join a room first to view the map"
712
+
713
+ return generate_sector_map()
714
+
715
+ map_button.click(
716
+ show_sector_map,
717
+ inputs=[current_room_display],
718
+ outputs=[chat_history_output]
719
+ )
720
+
721
+ # Handle Enter key for sending, Shift+Enter for new line
722
+ def on_message_submit(message, username, room_id):
723
+ # Simply call send_and_clear
724
+ return send_and_clear(message, username, room_id)
725
+
726
+ message_input.submit(
727
+ on_message_submit,
728
+ inputs=[message_input, username_input, current_room_display],
729
+ outputs=[message_input, chat_history_output]
730
+ )
731
+
732
+ # On load, populate room list
733
+ interface.load(
734
+ list_available_rooms,
735
+ inputs=[],
736
+ outputs=[room_list]
737
+ )
738
+
739
+ return interface
740
 
741
  async def main():
742
+ """Main function to start the application."""
743
+ global NODE_NAME, main_event_loop
744
+ NODE_NAME, port = get_node_name()
745
+
746
+ # Store the main event loop for later use
747
+ main_event_loop = asyncio.get_running_loop()
748
+
749
+ # Start WebSocket server
750
+ server = await start_websocket_server()
751
+
752
+ # Start message queue processor
753
+ asyncio.create_task(process_message_queue())
754
+
755
+ # Create and launch Gradio interface
756
+ interface = create_gradio_interface()
757
+
758
+ # Custom middleware to extract node name from URL query parameters
759
+ from starlette.middleware.base import BaseHTTPMiddleware
760
+
761
+ class NodeNameMiddleware(BaseHTTPMiddleware):
762
+ async def dispatch(self, request, call_next):
763
+ global NODE_NAME
764
+ query_params = dict(request.query_params)
765
+ if "node_name" in query_params:
766
+ NODE_NAME = query_params["node_name"]
767
+ logger.info(f"Node name set to {NODE_NAME} from URL parameter")
768
+
769
+ response = await call_next(request)
770
+ return response
771
+
772
+ # Apply middleware
773
+ app = gr.routes.App.create_app(interface)
774
+ app.add_middleware(NodeNameMiddleware)
775
+
776
+ # Launch with the modified app
777
+ gr.routes.mount_gradio_app(app, interface, path="/")
778
+
779
+ # Run the FastAPI app with uvicorn
780
+ import uvicorn
781
+ config = uvicorn.Config(app, host="0.0.0.0", port=port)
782
+ server = uvicorn.Server(config)
783
+
784
+ logger.info(f"Starting Gradio interface on http://0.0.0.0:{port} with node name '{NODE_NAME}'")
785
+ logger.info("Starting message queue processor")
786
+
787
+ # Start the server
788
+ await server.serve()
789
 
790
  if __name__ == "__main__":
791
  asyncio.run(main())