Update app.py
Browse files
app.py
CHANGED
@@ -19,9 +19,24 @@ logger = logging.getLogger("chat-node")
|
|
19 |
|
20 |
# Dictionary to store active connections
|
21 |
active_connections = {}
|
22 |
-
# Dictionary to store message history for each chat room
|
23 |
chat_history = {}
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
# Get node name from URL or command line
|
26 |
def get_node_name():
|
27 |
parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
|
@@ -38,6 +53,82 @@ def get_node_name():
|
|
38 |
|
39 |
return node_name, port
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
async def websocket_handler(websocket, path):
|
42 |
"""Handle WebSocket connections."""
|
43 |
try:
|
@@ -49,11 +140,13 @@ async def websocket_handler(websocket, path):
|
|
49 |
client_id = str(uuid.uuid4())
|
50 |
if room_id not in active_connections:
|
51 |
active_connections[room_id] = {}
|
52 |
-
chat_history[room_id] = []
|
53 |
|
54 |
active_connections[room_id][client_id] = websocket
|
55 |
|
56 |
-
#
|
|
|
|
|
|
|
57 |
welcome_msg = {
|
58 |
"type": "system",
|
59 |
"content": f"Welcome to room '{room_id}'! Connected from node '{NODE_NAME}'",
|
@@ -64,7 +157,7 @@ async def websocket_handler(websocket, path):
|
|
64 |
await websocket.send(json.dumps(welcome_msg))
|
65 |
|
66 |
# Send chat history
|
67 |
-
for msg in
|
68 |
await websocket.send(json.dumps(msg))
|
69 |
|
70 |
# Broadcast join notification
|
@@ -84,6 +177,11 @@ async def websocket_handler(websocket, path):
|
|
84 |
try:
|
85 |
data = json.loads(message)
|
86 |
|
|
|
|
|
|
|
|
|
|
|
87 |
# Add metadata to the message
|
88 |
data["timestamp"] = datetime.now().isoformat()
|
89 |
data["sender_node"] = NODE_NAME
|
@@ -91,8 +189,11 @@ async def websocket_handler(websocket, path):
|
|
91 |
|
92 |
# Store in history
|
93 |
chat_history[room_id].append(data)
|
94 |
-
if len(chat_history[room_id]) >
|
95 |
-
chat_history[room_id] = chat_history[room_id][-
|
|
|
|
|
|
|
96 |
|
97 |
# Broadcast to all clients in the room
|
98 |
await broadcast_message(data, room_id)
|
@@ -124,10 +225,9 @@ async def websocket_handler(websocket, path):
|
|
124 |
}
|
125 |
await broadcast_message(leave_msg, room_id)
|
126 |
|
127 |
-
# Clean up empty rooms
|
128 |
if not active_connections[room_id]:
|
129 |
del active_connections[room_id]
|
130 |
-
# Optionally, you might want to keep the chat history
|
131 |
|
132 |
async def broadcast_message(message, room_id):
|
133 |
"""Broadcast a message to all clients in a room."""
|
@@ -183,32 +283,79 @@ def join_room(room_id, chat_history_output):
|
|
183 |
# Sanitize the room ID
|
184 |
room_id = urllib.parse.quote(room_id.strip())
|
185 |
|
186 |
-
#
|
187 |
-
|
188 |
-
chat_history[room_id] = []
|
189 |
|
190 |
# Format existing messages
|
191 |
formatted_history = []
|
192 |
-
for msg in
|
193 |
if msg.get("type") == "chat":
|
194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
elif msg.get("type") == "system":
|
196 |
formatted_history.append(f"System: {msg.get('content', '')}")
|
197 |
|
198 |
return f"Joined room: {room_id}", formatted_history
|
199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
def create_gradio_interface():
|
201 |
"""Create and return the Gradio interface."""
|
202 |
with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
|
203 |
gr.Markdown(f"# Chat Node: {NODE_NAME}")
|
204 |
gr.Markdown("Join a room by entering a room ID below or create a new one.")
|
205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
with gr.Row():
|
207 |
room_id_input = gr.Textbox(label="Room ID", placeholder="Enter room ID")
|
208 |
join_button = gr.Button("Join Room")
|
209 |
|
210 |
-
|
|
|
211 |
|
|
|
212 |
with gr.Row():
|
213 |
username_input = gr.Textbox(label="Username", placeholder="Enter your username", value="User")
|
214 |
message_input = gr.Textbox(label="Message", placeholder="Type your message here")
|
@@ -218,6 +365,18 @@ def create_gradio_interface():
|
|
218 |
current_room_display = gr.Textbox(label="Current Room", value="Not joined any room yet")
|
219 |
|
220 |
# Event handlers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
join_button.click(
|
222 |
join_room,
|
223 |
inputs=[room_id_input, chat_history_output],
|
@@ -247,6 +406,13 @@ def create_gradio_interface():
|
|
247 |
inputs=[message_input, username_input, current_room_display],
|
248 |
outputs=[message_input, chat_history_output]
|
249 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
|
251 |
return interface
|
252 |
|
|
|
19 |
|
20 |
# Dictionary to store active connections
|
21 |
active_connections = {}
|
22 |
+
# Dictionary to store message history for each chat room (in-memory cache)
|
23 |
chat_history = {}
|
24 |
|
25 |
+
# Directory to store persistent chat history
|
26 |
+
HISTORY_DIR = "chat_history"
|
27 |
+
import os
|
28 |
+
import shutil
|
29 |
+
from pathlib import Path
|
30 |
+
|
31 |
+
# Create history directory if it doesn't exist
|
32 |
+
os.makedirs(HISTORY_DIR, exist_ok=True)
|
33 |
+
|
34 |
+
# README.md file that won't be listed or deleted
|
35 |
+
README_PATH = os.path.join(HISTORY_DIR, "README.md")
|
36 |
+
if not os.path.exists(README_PATH):
|
37 |
+
with open(README_PATH, "w") as f:
|
38 |
+
f.write("# Chat History\n\nThis directory contains persistent chat history files.\n")
|
39 |
+
|
40 |
# Get node name from URL or command line
|
41 |
def get_node_name():
|
42 |
parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
|
|
|
53 |
|
54 |
return node_name, port
|
55 |
|
56 |
+
def get_room_history_file(room_id):
|
57 |
+
"""Get the filename for a room's history."""
|
58 |
+
return os.path.join(HISTORY_DIR, f"{room_id}.md")
|
59 |
+
|
60 |
+
def load_room_history(room_id):
|
61 |
+
"""Load chat history for a room from persistent storage."""
|
62 |
+
if room_id not in chat_history:
|
63 |
+
chat_history[room_id] = []
|
64 |
+
|
65 |
+
# Try to load from file
|
66 |
+
history_file = get_room_history_file(room_id)
|
67 |
+
if os.path.exists(history_file):
|
68 |
+
try:
|
69 |
+
with open(history_file, 'r') as f:
|
70 |
+
history_json = f.read()
|
71 |
+
if history_json.strip():
|
72 |
+
loaded_history = json.loads(history_json)
|
73 |
+
chat_history[room_id] = loaded_history
|
74 |
+
logger.info(f"Loaded {len(loaded_history)} messages from history for room {room_id}")
|
75 |
+
except Exception as e:
|
76 |
+
logger.error(f"Error loading history for room {room_id}: {e}")
|
77 |
+
|
78 |
+
return chat_history[room_id]
|
79 |
+
|
80 |
+
def save_room_history(room_id):
|
81 |
+
"""Save chat history for a room to persistent storage."""
|
82 |
+
if room_id in chat_history and chat_history[room_id]:
|
83 |
+
history_file = get_room_history_file(room_id)
|
84 |
+
try:
|
85 |
+
with open(history_file, 'w') as f:
|
86 |
+
json.dump(chat_history[room_id], f)
|
87 |
+
logger.info(f"Saved {len(chat_history[room_id])} messages to history for room {room_id}")
|
88 |
+
except Exception as e:
|
89 |
+
logger.error(f"Error saving history for room {room_id}: {e}")
|
90 |
+
|
91 |
+
def get_all_history_files():
|
92 |
+
"""Get a list of all chat history files, sorted by modification time (newest first)."""
|
93 |
+
history_files = []
|
94 |
+
for file in os.listdir(HISTORY_DIR):
|
95 |
+
if file.endswith(".md") and file != "README.md":
|
96 |
+
file_path = os.path.join(HISTORY_DIR, file)
|
97 |
+
mod_time = os.path.getmtime(file_path)
|
98 |
+
room_id = file[:-3] # Remove .md extension
|
99 |
+
history_files.append((room_id, file_path, mod_time))
|
100 |
+
|
101 |
+
# Sort by modification time (newest first)
|
102 |
+
history_files.sort(key=lambda x: x[2], reverse=True)
|
103 |
+
return history_files
|
104 |
+
|
105 |
+
async def clear_all_history():
|
106 |
+
"""Clear all chat history for all rooms."""
|
107 |
+
global chat_history
|
108 |
+
|
109 |
+
# Clear in-memory history
|
110 |
+
chat_history = {}
|
111 |
+
|
112 |
+
# Delete all history files except README.md
|
113 |
+
for file in os.listdir(HISTORY_DIR):
|
114 |
+
if file.endswith(".md") and file != "README.md":
|
115 |
+
os.remove(os.path.join(HISTORY_DIR, file))
|
116 |
+
|
117 |
+
# Broadcast clear message to all rooms
|
118 |
+
clear_msg = {
|
119 |
+
"type": "system",
|
120 |
+
"content": "🧹 All chat history has been cleared by a user",
|
121 |
+
"timestamp": datetime.now().isoformat(),
|
122 |
+
"sender": "system"
|
123 |
+
}
|
124 |
+
|
125 |
+
for room_id in list(active_connections.keys()):
|
126 |
+
clear_msg["room_id"] = room_id
|
127 |
+
await broadcast_message(clear_msg, room_id)
|
128 |
+
|
129 |
+
logger.info("All chat history cleared")
|
130 |
+
return "All chat history cleared"
|
131 |
+
|
132 |
async def websocket_handler(websocket, path):
|
133 |
"""Handle WebSocket connections."""
|
134 |
try:
|
|
|
140 |
client_id = str(uuid.uuid4())
|
141 |
if room_id not in active_connections:
|
142 |
active_connections[room_id] = {}
|
|
|
143 |
|
144 |
active_connections[room_id][client_id] = websocket
|
145 |
|
146 |
+
# Load or initialize chat history
|
147 |
+
room_history = load_room_history(room_id)
|
148 |
+
|
149 |
+
# Send welcome message
|
150 |
welcome_msg = {
|
151 |
"type": "system",
|
152 |
"content": f"Welcome to room '{room_id}'! Connected from node '{NODE_NAME}'",
|
|
|
157 |
await websocket.send(json.dumps(welcome_msg))
|
158 |
|
159 |
# Send chat history
|
160 |
+
for msg in room_history:
|
161 |
await websocket.send(json.dumps(msg))
|
162 |
|
163 |
# Broadcast join notification
|
|
|
177 |
try:
|
178 |
data = json.loads(message)
|
179 |
|
180 |
+
# Check for clear command
|
181 |
+
if data.get("type") == "command" and data.get("command") == "clear_history":
|
182 |
+
result = await clear_all_history()
|
183 |
+
continue
|
184 |
+
|
185 |
# Add metadata to the message
|
186 |
data["timestamp"] = datetime.now().isoformat()
|
187 |
data["sender_node"] = NODE_NAME
|
|
|
189 |
|
190 |
# Store in history
|
191 |
chat_history[room_id].append(data)
|
192 |
+
if len(chat_history[room_id]) > 500: # Increased limit to 500 messages
|
193 |
+
chat_history[room_id] = chat_history[room_id][-500:]
|
194 |
+
|
195 |
+
# Save to persistent storage
|
196 |
+
save_room_history(room_id)
|
197 |
|
198 |
# Broadcast to all clients in the room
|
199 |
await broadcast_message(data, room_id)
|
|
|
225 |
}
|
226 |
await broadcast_message(leave_msg, room_id)
|
227 |
|
228 |
+
# Clean up empty rooms (but keep history)
|
229 |
if not active_connections[room_id]:
|
230 |
del active_connections[room_id]
|
|
|
231 |
|
232 |
async def broadcast_message(message, room_id):
|
233 |
"""Broadcast a message to all clients in a room."""
|
|
|
283 |
# Sanitize the room ID
|
284 |
room_id = urllib.parse.quote(room_id.strip())
|
285 |
|
286 |
+
# Load room history from persistent storage
|
287 |
+
history = load_room_history(room_id)
|
|
|
288 |
|
289 |
# Format existing messages
|
290 |
formatted_history = []
|
291 |
+
for msg in history:
|
292 |
if msg.get("type") == "chat":
|
293 |
+
sender_node = f" [{msg.get('sender_node', 'unknown')}]" if "sender_node" in msg else ""
|
294 |
+
time_str = ""
|
295 |
+
if "timestamp" in msg:
|
296 |
+
try:
|
297 |
+
dt = datetime.fromisoformat(msg["timestamp"])
|
298 |
+
time_str = f"[{dt.strftime('%H:%M:%S')}] "
|
299 |
+
except:
|
300 |
+
pass
|
301 |
+
formatted_history.append(f"{time_str}{msg.get('username', 'Anonymous')}{sender_node}: {msg.get('content', '')}")
|
302 |
elif msg.get("type") == "system":
|
303 |
formatted_history.append(f"System: {msg.get('content', '')}")
|
304 |
|
305 |
return f"Joined room: {room_id}", formatted_history
|
306 |
|
307 |
+
def send_clear_command():
|
308 |
+
"""Send a command to clear all chat history."""
|
309 |
+
global message_queue
|
310 |
+
|
311 |
+
msg_data = {
|
312 |
+
"type": "command",
|
313 |
+
"command": "clear_history",
|
314 |
+
"username": "System"
|
315 |
+
}
|
316 |
+
|
317 |
+
# Add to queue for processing by the main loop
|
318 |
+
message_queue.append(msg_data)
|
319 |
+
|
320 |
+
return "🧹 Clearing all chat history..."
|
321 |
+
|
322 |
+
def list_available_rooms():
|
323 |
+
"""List all available chat rooms with their last activity time."""
|
324 |
+
history_files = get_all_history_files()
|
325 |
+
|
326 |
+
if not history_files:
|
327 |
+
return "No chat rooms available yet. Create one by joining a room!"
|
328 |
+
|
329 |
+
room_list = "### Available Chat Rooms\n\n"
|
330 |
+
for room_id, file_path, mod_time in history_files:
|
331 |
+
last_activity = datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
|
332 |
+
room_list += f"- **{room_id}**: Last activity {last_activity}\n"
|
333 |
+
|
334 |
+
return room_list
|
335 |
+
|
336 |
def create_gradio_interface():
|
337 |
"""Create and return the Gradio interface."""
|
338 |
with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
|
339 |
gr.Markdown(f"# Chat Node: {NODE_NAME}")
|
340 |
gr.Markdown("Join a room by entering a room ID below or create a new one.")
|
341 |
|
342 |
+
# Room list and management
|
343 |
+
with gr.Row():
|
344 |
+
with gr.Column(scale=3):
|
345 |
+
room_list = gr.Markdown(value="Loading available rooms...")
|
346 |
+
refresh_button = gr.Button("🔄 Refresh Room List")
|
347 |
+
with gr.Column(scale=1):
|
348 |
+
clear_button = gr.Button("🧹 Clear All Chat History", variant="stop")
|
349 |
+
|
350 |
+
# Join room controls
|
351 |
with gr.Row():
|
352 |
room_id_input = gr.Textbox(label="Room ID", placeholder="Enter room ID")
|
353 |
join_button = gr.Button("Join Room")
|
354 |
|
355 |
+
# Chat area
|
356 |
+
chat_history_output = gr.Textbox(label="Chat History", lines=20, max_lines=20)
|
357 |
|
358 |
+
# Message controls
|
359 |
with gr.Row():
|
360 |
username_input = gr.Textbox(label="Username", placeholder="Enter your username", value="User")
|
361 |
message_input = gr.Textbox(label="Message", placeholder="Type your message here")
|
|
|
365 |
current_room_display = gr.Textbox(label="Current Room", value="Not joined any room yet")
|
366 |
|
367 |
# Event handlers
|
368 |
+
refresh_button.click(
|
369 |
+
list_available_rooms,
|
370 |
+
inputs=[],
|
371 |
+
outputs=[room_list]
|
372 |
+
)
|
373 |
+
|
374 |
+
clear_button.click(
|
375 |
+
send_clear_command,
|
376 |
+
inputs=[],
|
377 |
+
outputs=[room_list]
|
378 |
+
)
|
379 |
+
|
380 |
join_button.click(
|
381 |
join_room,
|
382 |
inputs=[room_id_input, chat_history_output],
|
|
|
406 |
inputs=[message_input, username_input, current_room_display],
|
407 |
outputs=[message_input, chat_history_output]
|
408 |
)
|
409 |
+
|
410 |
+
# On load, populate room list
|
411 |
+
interface.load(
|
412 |
+
list_available_rooms,
|
413 |
+
inputs=[],
|
414 |
+
outputs=[room_list]
|
415 |
+
)
|
416 |
|
417 |
return interface
|
418 |
|