awacke1 commited on
Commit
6862189
Β·
verified Β·
1 Parent(s): ca4200f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -353
app.py CHANGED
@@ -4,14 +4,10 @@ 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 shutil
13
- from pathlib import Path
14
- import time
15
  import random
16
 
17
  # Configure logging
@@ -22,33 +18,6 @@ logging.basicConfig(
22
  )
23
  logger = logging.getLogger("chat-node")
24
 
25
- # Dictionaries for state
26
- active_connections = {}
27
- chat_history = {}
28
- log_history = []
29
- file_modification_times = {}
30
- sector_users = {}
31
-
32
- # Grid dimensions
33
- GRID_WIDTH = 10
34
- GRID_HEIGHT = 10
35
-
36
- # Directories
37
- HISTORY_DIR = "chat_history"
38
- LOG_DIR = "server_logs"
39
- os.makedirs(HISTORY_DIR, exist_ok=True)
40
- os.makedirs(LOG_DIR, exist_ok=True)
41
-
42
- # README files
43
- for dir_path, content in [
44
- (HISTORY_DIR, "# Chat History\n\nThis directory contains persistent chat history files.\n"),
45
- (LOG_DIR, "# Server Logs\n\nThis directory contains server log files.\n")
46
- ]:
47
- readme_path = os.path.join(dir_path, "README.md")
48
- if not os.path.exists(readme_path):
49
- with open(readme_path, "w") as f:
50
- f.write(content)
51
-
52
  # Fun usernames with emojis
53
  FUN_USERNAMES = [
54
  "CosmicJester 🌌", "PixelPanda 🐼", "QuantumQuack πŸ¦†", "StellarSquirrel 🐿️",
@@ -58,166 +27,74 @@ FUN_USERNAMES = [
58
  "MysticMoose 🦌", "GlitchGnome 🧚", "VortexViper 🐍", "ChronoChimp πŸ’"
59
  ]
60
 
 
 
 
 
61
  # Node name
62
  def get_node_name():
63
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
64
  parser.add_argument('--node-name', type=str, default=None, help='Name for this chat node')
65
  parser.add_argument('--port', type=int, default=7860, help='Port to run the Gradio interface on')
66
  args = parser.parse_args()
67
- node_name = args.node_name or f"node-{uuid.uuid4().hex[:8]}"
68
- return node_name, args.port
69
-
70
- def get_room_history_file(room_id):
71
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
72
- return os.path.join(HISTORY_DIR, f"{room_id}_{timestamp}.jsonl")
73
 
74
- def get_log_file():
75
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
76
- return os.path.join(LOG_DIR, f"log_{timestamp}.jsonl")
 
 
77
 
78
- def get_all_room_history_files(room_id):
79
- files = [os.path.join(HISTORY_DIR, f) for f in os.listdir(HISTORY_DIR) if f.startswith(f"{room_id}_") and f.endswith(".jsonl")]
80
- files.sort(key=os.path.getmtime, reverse=True)
81
- return files
82
-
83
- def list_all_history_files():
84
- files = []
85
- for file in os.listdir(HISTORY_DIR):
86
- if file.endswith(".jsonl") and file != "README.md":
87
- room_id = file.split('_', 1)[0]
88
- file_path = os.path.join(HISTORY_DIR, file)
89
- mod_time = os.path.getmtime(file_path)
90
- files.append((room_id, file_path, mod_time))
91
- files.sort(key=lambda x: x[2], reverse=True)
92
- return files
93
-
94
- def load_room_history(room_id):
95
- if room_id not in chat_history:
96
- chat_history[room_id] = []
97
- history_files = get_all_room_history_files(room_id)
98
- for file in history_files:
99
- file_modification_times[file] = file_modification_times.get(file, os.path.getmtime(file))
100
- try:
101
- with open(file, 'r') as f:
102
- for line in f:
103
- if line.strip():
104
- try:
105
- chat_history[room_id].append(json.loads(line))
106
- except json.JSONDecodeError:
107
- logger.error(f"Error parsing JSON line in {file}")
108
- except Exception as e:
109
- logger.error(f"Error loading history from {file}: {e}")
110
- chat_history[room_id].sort(key=lambda x: x.get("timestamp", ""))
111
- logger.info(f"Loaded {len(chat_history[room_id])} messages from {len(history_files)} files for room {room_id}")
112
- sector_users[room_id] = sector_users.get(room_id, set())
113
- return chat_history[room_id]
114
-
115
- def save_message_to_history(room_id, message):
116
- history_files = get_all_room_history_files(room_id)
117
- history_file = get_room_history_file(room_id) if not history_files or os.path.getsize(history_files[0]) > 1024 * 1024 else history_files[0]
118
- try:
119
- with open(history_file, 'a') as f:
120
- f.write(json.dumps(message) + '\n')
121
- file_modification_times[history_file] = os.path.getmtime(history_file)
122
- except Exception as e:
123
- logger.error(f"Error saving message to {history_file}: {e}")
124
-
125
- def save_log_entry(entry):
126
- log_files = [os.path.join(LOG_DIR, f) for f in os.listdir(LOG_DIR) if f.startswith("log_") and f.endswith(".jsonl")]
127
- log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
128
- log_file = get_log_file() if not log_files or os.path.getsize(log_files[0]) > 1024 * 1024 else log_files[0]
129
  try:
130
  with open(log_file, 'a') as f:
131
- f.write(json.dumps(entry) + '\n')
132
- file_modification_times[log_file] = os.path.getmtime(log_file)
 
 
133
  except Exception as e:
134
  logger.error(f"Error saving log to {log_file}: {e}")
 
135
 
136
- def get_sector_coordinates(room_id):
 
 
 
 
 
137
  try:
138
- if ',' in room_id:
139
- x, y = map(int, room_id.split(','))
140
- return max(0, min(x, GRID_WIDTH-1)), max(0, min(y, GRID_HEIGHT-1))
141
- except:
142
- hash_val = hash(room_id)
143
- return abs(hash_val) % GRID_WIDTH, abs(hash_val >> 8) % GRID_HEIGHT
144
-
145
- def generate_sector_map():
146
- grid = [[' ' for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
147
- for room_id, users in sector_users.items():
148
- if users:
149
- x, y = get_sector_coordinates(room_id)
150
- user_count = len(users)
151
- grid[y][x] = str(min(user_count, 9)) if user_count < 10 else '+'
152
- header = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
153
- map_str = header + '\n' + '\n'.join(f"{y % 10}|{''.join(grid[y])}|" for y in range(GRID_HEIGHT)) + '\n' + header
154
- return f"```\n{map_str}\n```\n\nLegend: Number indicates users in sector. '+' means 10+ users."
155
 
156
- async def clear_all_history():
157
- global chat_history
158
- chat_history = {}
159
- for file in os.listdir(HISTORY_DIR):
160
- if file.endswith(".jsonl"):
161
- os.remove(os.path.join(HISTORY_DIR, file))
162
- clear_msg = {"type": "system", "content": "🧹 All chat history cleared", "timestamp": datetime.now().isoformat(), "sender": "system"}
163
- for room_id in list(active_connections.keys()):
164
- clear_msg["room_id"] = room_id
165
- await broadcast_message(clear_msg, room_id)
166
- logger.info("All chat history cleared")
167
- return "All chat history cleared"
168
 
169
  async def websocket_handler(websocket, path):
170
  try:
171
- room_id = path.strip('/').split('/')[0] or "default"
172
  client_id = str(uuid.uuid4())
 
173
  active_connections.setdefault(room_id, {})[client_id] = websocket
174
- sector_users.setdefault(room_id, set()).add(client_id)
175
- x, y = get_sector_coordinates(room_id)
176
- room_history = load_room_history(room_id)
177
-
178
- for msg in [
179
- {"type": "system", "content": f"Welcome to room '{room_id}' (Sector {x},{y})! Node: '{NODE_NAME}'", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id},
180
- {"type": "system", "content": f"Sector Map:\n{generate_sector_map()}", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}
181
- ] + room_history:
182
- await websocket.send(json.dumps(msg))
183
-
184
- join_msg = {"type": "system", "content": f"User joined (Sector {x},{y}) - {len(sector_users[room_id])} users", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}
185
- await broadcast_message(join_msg, room_id)
186
- save_message_to_history(room_id, join_msg)
187
- logger.info(f"Client {client_id} connected to room {room_id} (Sector {x},{y})")
188
-
189
- if room_id == "logs":
190
- for entry in log_history[-10:]:
191
- await websocket.send(json.dumps({"type": "log", "content": entry["message"], "timestamp": entry["timestamp"], "sender": "system", "room_id": "logs"}))
192
 
193
  async for message in websocket:
194
  try:
195
  data = json.loads(message)
196
- if data.get("type") == "command":
197
- if data.get("command") == "clear_history":
198
- await clear_all_history()
199
- continue
200
- if data.get("command") == "show_map":
201
- await websocket.send(json.dumps({"type": "system", "content": f"Sector Map:\n{generate_sector_map()}", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}))
202
- continue
203
- data.update({"timestamp": datetime.now().isoformat(), "sender_node": NODE_NAME, "room_id": room_id})
204
- chat_history[room_id].append(data)
205
- if len(chat_history[room_id]) > 500:
206
- chat_history[room_id] = chat_history[room_id][-500:]
207
- save_message_to_history(room_id, data)
208
  await broadcast_message(data, room_id)
209
  except json.JSONDecodeError:
210
- await websocket.send(json.dumps({"type": "error", "content": "Invalid JSON", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}))
211
  except websockets.ConnectionClosed:
212
- logger.info(f"Client {client_id} disconnected from room {room_id}")
213
  finally:
214
  if room_id in active_connections and client_id in active_connections[room_id]:
215
  del active_connections[room_id][client_id]
216
- sector_users[room_id].discard(client_id)
217
- x, y = get_sector_coordinates(room_id)
218
- leave_msg = {"type": "system", "content": f"User left (Sector {x},{y}) - {len(sector_users[room_id])} users", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}
219
- await broadcast_message(leave_msg, room_id)
220
- save_message_to_history(room_id, leave_msg)
221
  if not active_connections[room_id]:
222
  del active_connections[room_id]
223
 
@@ -237,214 +114,77 @@ async def start_websocket_server(host='0.0.0.0', port=8765):
237
  logger.info(f"WebSocket server started on ws://{host}:{port}")
238
  return server
239
 
240
- main_event_loop = None
241
- message_queue = []
242
-
243
- def send_message(message, username, room_id):
244
- if not message.strip():
245
- return None
246
- msg_data = {"type": "chat", "content": message, "username": username, "room_id": room_id}
247
- message_queue.append(msg_data)
248
- return f"{username}: {message}"
249
-
250
- def join_room(room_id, chat_history_output):
251
- if not room_id.strip():
252
- return "Please enter a valid room ID", chat_history_output
253
- room_id = urllib.parse.quote(room_id.strip())
254
- history = load_room_history(room_id)
255
- formatted_history = [
256
- f"[{datetime.fromisoformat(msg['timestamp']).strftime('%H:%M:%S')}] {msg.get('username', 'Anonymous')} [{'sender_node' in msg and msg['sender_node'] or 'unknown'}]: {msg['content']}"
257
- if msg.get("type") == "chat" else f"System: {msg['content']}"
258
- for msg in history
259
- ]
260
- return f"Joined room: {room_id}", "\n".join(formatted_history)
261
-
262
- def list_available_rooms():
263
- history_files = list_all_history_files()
264
- return "No chat rooms available yet. Join one to create it!" if not history_files else "### Available Chat Rooms\n\n" + "\n".join(
265
- f"- **{room_id}**: Last activity {datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S')}" for room_id, _, mod_time in history_files
266
- )
267
 
268
- def create_gradio_interface():
269
- with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
270
- # Custom CSS and JS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  interface.css = """
272
- #log-overlay {
273
- position: fixed; top: 10px; right: 10px; width: 300px; height: 200px;
274
- background: rgba(0, 0, 0, 0.8); color: white; padding: 10px; overflow-y: auto;
275
- border-radius: 5px; z-index: 1000; display: none;
276
- }
277
- #threejs-container { width: 100%; height: 400px; }
278
- .chat-container { margin-top: 20px; }
279
  """
280
- interface.js = """
281
- async () => {
282
- // Log Overlay
283
- let logOverlay = document.createElement('div');
284
- logOverlay.id = 'log-overlay';
285
- document.body.appendChild(logOverlay);
286
- let wsLogs = new WebSocket('ws://' + window.location.hostname + ':8765/logs');
287
- wsLogs.onmessage = (event) => {
288
- let data = JSON.parse(event.data);
289
- if (data.type === 'log') {
290
- logOverlay.style.display = 'block';
291
- logOverlay.innerHTML += `<p>${data.timestamp} - ${data.content}</p>`;
292
- logOverlay.scrollTop = logOverlay.scrollHeight;
293
- }
294
- };
295
- wsLogs.onerror = () => console.error('Log WebSocket error');
296
- wsLogs.onclose = () => console.log('Log WebSocket closed');
297
-
298
- // Three.js Sector Map
299
- let container = document.getElementById('threejs-container');
300
- if (!container) return;
301
- const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
302
- const scene = new THREE.Scene();
303
- const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
304
- const renderer = new THREE.WebGLRenderer();
305
- renderer.setSize(container.clientWidth, container.clientHeight);
306
- container.appendChild(renderer.domElement);
307
-
308
- const gridSize = 10;
309
- const geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
310
- const edges = new THREE.EdgesGeometry(geometry);
311
- for (let x = 0; x < gridSize; x++) {
312
- for (let y = 0; y < gridSize; y++) {
313
- const hasUsers = document.querySelector(`[data-room="${x},${y}"]`)?.dataset.users > 0;
314
- const material = new THREE.MeshBasicMaterial({ color: hasUsers ? 0x00ff00 : 0x333333 });
315
- const cube = new THREE.Mesh(geometry, material);
316
- cube.position.set(x - gridSize / 2, 0, y - gridSize / 2);
317
- scene.add(cube);
318
- if (hasUsers) {
319
- const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff }));
320
- line.position.set(x - gridSize / 2, 0, y - gridSize / 2);
321
- scene.add(line);
322
- }
323
- }
324
- }
325
- camera.position.set(0, 10, 10);
326
- camera.lookAt(0, 0, 0);
327
-
328
- function animate() {
329
- requestAnimationFrame(animate);
330
- renderer.render(scene, camera);
331
- }
332
- animate();
333
- }
334
  """
335
 
336
- gr.Markdown(f"# Chat Node: {NODE_NAME}")
337
- gr.Markdown("Welcome! You've auto-joined a room with a fun username.")
338
-
339
- with gr.Row():
340
- with gr.Column(scale=3):
341
- room_list = gr.Markdown(value=list_available_rooms())
342
- refresh_button = gr.Button("πŸ”„ Refresh Room List")
343
- with gr.Column(scale=1):
344
- clear_button = gr.Button("🧹 Clear All Chat History", variant="stop")
345
-
346
- gr.HTML('<div id="threejs-container"></div>') # Three.js container
347
-
348
- with gr.Row():
349
- room_id_input = gr.Textbox(label="Room ID", placeholder="Change room (e.g., '0,0')")
350
- join_button = gr.Button("Join Room")
351
-
352
- chat_history_output = gr.Textbox(label="Chat History", lines=20, max_lines=20, elem_classes=["chat-container"])
353
-
354
- with gr.Row():
355
- username_input = gr.Textbox(label="Username", value=random.choice(FUN_USERNAMES))
356
- with gr.Column(scale=3):
357
- message_input = gr.Textbox(label="Message", placeholder="Type here...", lines=3)
358
- with gr.Column(scale=1):
359
- send_button = gr.Button("Send")
360
- map_button = gr.Button("πŸ—ΊοΈ Show Map")
361
-
362
- current_room_display = gr.Textbox(label="Current Room", value="Auto-joining...")
363
 
364
- # Event handlers
365
- refresh_button.click(list_available_rooms, [], [room_list])
366
- clear_button.click(lambda: (message_queue.append({"type": "command", "command": "clear_history", "username": "System"}), "🧹 Clearing..."), [], [room_list])
367
- join_button.click(join_room, [room_id_input, chat_history_output], [current_room_display, chat_history_output])
368
-
369
- def send_and_clear(message, username, room_id):
370
- if not room_id.startswith("Joined room:"):
371
- return "", "Please join a room first"
372
- actual_room_id = room_id.replace("Joined room: ", "").strip()
373
- formatted_msg = send_message(message, username, actual_room_id)
374
- return "", formatted_msg or ""
375
-
376
- send_button.click(send_and_clear, [message_input, username_input, current_room_display], [message_input, chat_history_output])
377
- message_input.submit(send_and_clear, [message_input, username_input, current_room_display], [message_input, chat_history_output])
378
- map_button.click(lambda room_id: generate_sector_map() if room_id.startswith("Joined room:") else "Join a room first", [current_room_display], [chat_history_output])
379
-
380
- # Auto-join on load
381
- def auto_join():
382
- random_room = f"{random.randint(0, GRID_WIDTH-1)},{random.randint(0, GRID_HEIGHT-1)}"
383
- return join_room(random_room, "")
384
- interface.load(auto_join, [], [current_room_display, chat_history_output])
385
 
386
  return interface
387
 
388
- async def process_message_queue():
389
- while True:
390
- if message_queue:
391
- msg_data = message_queue.pop(0)
392
- await broadcast_message(msg_data, msg_data["room_id"])
393
- await asyncio.sleep(0.1)
394
-
395
- async def process_logs():
396
- while True:
397
- global log_history
398
- updated = False
399
- for file in os.listdir(LOG_DIR):
400
- if file.endswith(".jsonl"):
401
- file_path = os.path.join(LOG_DIR, file)
402
- current_mtime = os.path.getmtime(file_path)
403
- if file_path not in file_modification_times or current_mtime > file_modification_times[file_path]:
404
- updated = True
405
- file_modification_times[file_path] = current_mtime
406
- if updated:
407
- log_history = []
408
- for file in sorted([f for f in os.listdir(LOG_DIR) if f.endswith(".jsonl")], key=lambda x: os.path.getmtime(os.path.join(LOG_DIR, x))):
409
- try:
410
- with open(os.path.join(LOG_DIR, file), 'r') as f:
411
- for line in f:
412
- if line.strip():
413
- log_history.append(json.loads(line))
414
- except Exception as e:
415
- logger.error(f"Error loading logs from {file}: {e}")
416
- log_msg = {"type": "log", "content": "\n".join(entry["message"] for entry in log_history[-10:]), "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": "logs"}
417
- await broadcast_message(log_msg, "logs")
418
- await asyncio.sleep(1)
419
-
420
- class LogBroadcastHandler(logging.Handler):
421
- def emit(self, record):
422
- entry = {"timestamp": datetime.now().isoformat(), "level": record.levelname, "message": self.format(record), "name": record.name}
423
- save_log_entry(entry)
424
- log_history.append(entry)
425
-
426
- logger.addHandler(LogBroadcastHandler())
427
-
428
  async def main():
429
- global NODE_NAME, main_event_loop
430
  NODE_NAME, port = get_node_name()
431
- main_event_loop = asyncio.get_running_loop()
432
  await start_websocket_server()
433
- asyncio.create_task(process_message_queue())
434
- asyncio.create_task(process_logs())
435
- interface = create_gradio_interface()
 
 
 
 
436
 
437
- from starlette.middleware.base import BaseHTTPMiddleware
438
- class NodeNameMiddleware(BaseHTTPMiddleware):
439
- async def dispatch(self, request, call_next):
440
- global NODE_NAME
441
- if "node_name" in dict(request.query_params):
442
- NODE_NAME = dict(request.query_params)["node_name"]
443
- logger.info(f"Node name set to {NODE_NAME} from URL")
444
- return await call_next(request)
445
 
446
  app = gr.routes.App.create_app(interface)
447
- app.add_middleware(NodeNameMiddleware)
 
448
  import uvicorn
449
  await uvicorn.Server(uvicorn.Config(app, host="0.0.0.0", port=port)).serve()
450
 
 
4
  import json
5
  import uuid
6
  import argparse
 
7
  from datetime import datetime
8
  import logging
9
  import sys
10
  import os
 
 
 
11
  import random
12
 
13
  # Configure logging
 
18
  )
19
  logger = logging.getLogger("chat-node")
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Fun usernames with emojis
22
  FUN_USERNAMES = [
23
  "CosmicJester 🌌", "PixelPanda 🐼", "QuantumQuack πŸ¦†", "StellarSquirrel 🐿️",
 
27
  "MysticMoose 🦌", "GlitchGnome 🧚", "VortexViper 🐍", "ChronoChimp πŸ’"
28
  ]
29
 
30
+ # Directories
31
+ LOG_DIR = "user_logs"
32
+ os.makedirs(LOG_DIR, exist_ok=True)
33
+
34
  # Node name
35
  def get_node_name():
36
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
37
  parser.add_argument('--node-name', type=str, default=None, help='Name for this chat node')
38
  parser.add_argument('--port', type=int, default=7860, help='Port to run the Gradio interface on')
39
  args = parser.parse_args()
40
+ return args.node_name or f"node-{uuid.uuid4().hex[:8]}", args.port
 
 
 
 
 
41
 
42
+ def get_log_file(username):
43
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
44
+ # Replace spaces and emojis with underscores for valid filenames
45
+ safe_username = username.replace(" ", "_").encode('ascii', 'ignore').decode('ascii')
46
+ return os.path.join(LOG_DIR, f"{safe_username}_{timestamp}.md")
47
 
48
+ def save_log_entry(username, entry):
49
+ log_file = get_log_file(username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  try:
51
  with open(log_file, 'a') as f:
52
+ # Format as Markdown with timestamp
53
+ f.write(f"```log\n[{entry['timestamp']}] {entry['level']}: {entry['message']}\n```\n")
54
+ logger.info(f"Saved log entry to {log_file}")
55
+ return log_file
56
  except Exception as e:
57
  logger.error(f"Error saving log to {log_file}: {e}")
58
+ return None
59
 
60
+ def load_latest_log(username):
61
+ safe_username = username.replace(" ", "_").encode('ascii', 'ignore').decode('ascii')
62
+ log_files = [f for f in os.listdir(LOG_DIR) if f.startswith(safe_username) and f.endswith(".md")]
63
+ if not log_files:
64
+ return "# No logs yet\nStart interacting to generate logs!"
65
+ latest_file = max(log_files, key=lambda f: os.path.getmtime(os.path.join(LOG_DIR, f)))
66
  try:
67
+ with open(os.path.join(LOG_DIR, latest_file), 'r') as f:
68
+ content = f.read()
69
+ # Add line numbers to the Markdown content
70
+ lines = content.strip().split('\n')
71
+ numbered_content = "\n".join(f"{i+1}. {line}" for i, line in enumerate(lines))
72
+ return f"# Log for {username} ({latest_file})\n\n{numbered_content}"
73
+ except Exception as e:
74
+ logger.error(f"Error loading log {latest_file}: {e}")
75
+ return "# Error loading log\nCheck server logs for details."
 
 
 
 
 
 
 
 
76
 
77
+ active_connections = {}
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  async def websocket_handler(websocket, path):
80
  try:
 
81
  client_id = str(uuid.uuid4())
82
+ room_id = "logs" # Simplified to a single logs room
83
  active_connections.setdefault(room_id, {})[client_id] = websocket
84
+ logger.info(f"Client {client_id} connected to logs")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  async for message in websocket:
87
  try:
88
  data = json.loads(message)
89
+ data["timestamp"] = datetime.now().isoformat()
 
 
 
 
 
 
 
 
 
 
 
90
  await broadcast_message(data, room_id)
91
  except json.JSONDecodeError:
92
+ await websocket.send(json.dumps({"type": "error", "content": "Invalid JSON", "timestamp": datetime.now().isoformat(), "sender": "system"}))
93
  except websockets.ConnectionClosed:
94
+ logger.info(f"Client {client_id} disconnected from logs")
95
  finally:
96
  if room_id in active_connections and client_id in active_connections[room_id]:
97
  del active_connections[room_id][client_id]
 
 
 
 
 
98
  if not active_connections[room_id]:
99
  del active_connections[room_id]
100
 
 
114
  logger.info(f"WebSocket server started on ws://{host}:{port}")
115
  return server
116
 
117
+ class LogBroadcastHandler(logging.Handler):
118
+ def __init__(self, username):
119
+ super().__init__()
120
+ self.username = username
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ def emit(self, record):
123
+ entry = {
124
+ "timestamp": datetime.now().isoformat(),
125
+ "level": record.levelname,
126
+ "message": self.format(record),
127
+ "name": record.name
128
+ }
129
+ log_file = save_log_entry(self.username, entry)
130
+ if log_file:
131
+ # Notify WebSocket clients of new log file
132
+ asyncio.create_task(broadcast_message({
133
+ "type": "log",
134
+ "content": f"New log entry saved to {log_file}",
135
+ "timestamp": entry["timestamp"],
136
+ "sender": "system",
137
+ "username": self.username
138
+ }, "logs"))
139
+
140
+ def create_gradio_interface(username):
141
+ logger.handlers = [LogBroadcastHandler(username)] # Replace handlers with user-specific one
142
+ with gr.Blocks(title=f"Log Viewer: {NODE_NAME}") as interface:
143
  interface.css = """
144
+ .code-container { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 5px; }
 
 
 
 
 
 
145
  """
146
+ interface.js = f"""
147
+ () => {{
148
+ setInterval(async () => {{
149
+ const response = await fetch('/get_log?username={username}');
150
+ const logContent = await response.text();
151
+ document.getElementById('log-display').innerText = logContent;
152
+ }}, 900); // Refresh every 0.9 seconds
153
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  """
155
 
156
+ gr.Markdown(f"# Log Viewer for {username} on Node: {NODE_NAME}")
157
+ gr.Markdown("Your logs are displayed below, auto-refreshing every 0.9 seconds.")
158
+ log_display = gr.Code(label="Your Latest Log", language="markdown", lines=20, elem_classes=["code-container"], elem_id="log-display")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # Custom endpoint to fetch log
161
+ def get_log():
162
+ return load_latest_log(username)
163
+ interface.load(get_log, [], [log_display])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  return interface
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  async def main():
168
+ global NODE_NAME
169
  NODE_NAME, port = get_node_name()
 
170
  await start_websocket_server()
171
+
172
+ # Assign a random username on startup
173
+ username = random.choice(FUN_USERNAMES)
174
+ logger.info(f"Assigned username: {username}")
175
+
176
+ # Create and run Gradio interface
177
+ interface = create_gradio_interface(username)
178
 
179
+ from starlette.routing import Route
180
+ from starlette.responses import PlainTextResponse
181
+
182
+ async def get_log_endpoint(request):
183
+ return PlainTextResponse(load_latest_log(username))
 
 
 
184
 
185
  app = gr.routes.App.create_app(interface)
186
+ app.routes.append(Route("/get_log", get_log_endpoint))
187
+
188
  import uvicorn
189
  await uvicorn.Server(uvicorn.Config(app, host="0.0.0.0", port=port)).serve()
190