Update app.py
Browse files
app.py
CHANGED
@@ -1,545 +1,791 @@
|
|
1 |
-
import
|
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
|
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 |
-
#
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
#
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
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 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
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=
|
|
|
99 |
args = parser.parse_args()
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
if
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
"""
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
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 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
pass
|
285 |
-
|
286 |
-
|
287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
|
289 |
async def broadcast_message(message, room_id):
|
290 |
-
"""
|
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 |
-
|
296 |
-
|
|
|
297 |
try:
|
298 |
-
await
|
299 |
-
except websockets.ConnectionClosed:
|
300 |
-
|
301 |
-
|
|
|
|
|
302 |
del active_connections[room_id][client_id]
|
303 |
|
304 |
-
async def
|
305 |
-
"""
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
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**: "
|
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 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
467 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
}
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
532 |
|
533 |
async def main():
|
534 |
-
"""
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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())
|