Update app.py
Browse files
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 |
-
|
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 |
-
|
|
|
|
|
77 |
|
78 |
-
def
|
79 |
-
|
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 |
-
|
132 |
-
|
|
|
|
|
133 |
except Exception as e:
|
134 |
logger.error(f"Error saving log to {log_file}: {e}")
|
|
|
135 |
|
136 |
-
def
|
|
|
|
|
|
|
|
|
|
|
137 |
try:
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
return
|
144 |
-
|
145 |
-
|
146 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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"
|
211 |
except websockets.ConnectionClosed:
|
212 |
-
logger.info(f"Client {client_id} disconnected from
|
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 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
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
|
269 |
-
|
270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
271 |
interface.css = """
|
272 |
-
#
|
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 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
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"#
|
337 |
-
gr.Markdown("
|
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 |
-
#
|
365 |
-
|
366 |
-
|
367 |
-
|
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
|
430 |
NODE_NAME, port = get_node_name()
|
431 |
-
main_event_loop = asyncio.get_running_loop()
|
432 |
await start_websocket_server()
|
433 |
-
|
434 |
-
|
435 |
-
|
|
|
|
|
|
|
|
|
436 |
|
437 |
-
from starlette.
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
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.
|
|
|
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 |
|