Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,7 @@ import gradio as gr
|
|
| 2 |
import time
|
| 3 |
from datetime import datetime
|
| 4 |
from collections import deque
|
|
|
|
| 5 |
|
| 6 |
# --- GLOBAL SERVER STATE (In-Memory Only) ---
|
| 7 |
class ChatServer:
|
|
@@ -21,16 +22,24 @@ class ChatServer:
|
|
| 21 |
if u in self.user_rooms:
|
| 22 |
del self.user_rooms[u]
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def _add_message_to_queue(self, queue, user, message, profile_pic):
|
| 25 |
-
|
|
|
|
| 26 |
# Stacking Logic: Check if last message was same user and same text
|
| 27 |
-
if queue and queue[-1]['user'] == user and queue[-1]['text'] ==
|
| 28 |
queue[-1]['count'] = queue[-1].get('count', 1) + 1
|
| 29 |
-
queue[-1]['time'] = datetime.now().strftime("%H:%M")
|
| 30 |
else:
|
| 31 |
msg_data = {
|
| 32 |
"user": user,
|
| 33 |
-
"text":
|
| 34 |
"pic": profile_pic,
|
| 35 |
"time": datetime.now().strftime("%H:%M"),
|
| 36 |
"count": 1
|
|
@@ -56,14 +65,14 @@ def get_messages_html(messages):
|
|
| 56 |
if not messages:
|
| 57 |
return "<div style='color: gray; text-align: center; margin-top: 20px;'>No messages yet.</div>"
|
| 58 |
|
| 59 |
-
|
| 60 |
for m in messages:
|
| 61 |
pic_url = m.get('pic') or "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 62 |
display_text = m['text']
|
| 63 |
if m.get('count', 1) > 1:
|
| 64 |
display_text += f" <span style='color: #888; font-size: 0.8em; font-weight: normal;'>(x{m['count']})</span>"
|
| 65 |
|
| 66 |
-
|
| 67 |
<div class="msg-row" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 4px;">
|
| 68 |
<img src="{pic_url}" style="width: 36px; height: 36px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.1); flex-shrink: 0;">
|
| 69 |
<div style="background: rgba(255, 255, 255, 0.07); padding: 10px 14px; border-radius: 0 16px 16px 16px; max-width: 85%;">
|
|
@@ -76,8 +85,9 @@ def get_messages_html(messages):
|
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
"""
|
| 79 |
-
|
| 80 |
-
|
|
|
|
| 81 |
</div>
|
| 82 |
<script>
|
| 83 |
(function() {
|
|
@@ -89,33 +99,32 @@ def get_messages_html(messages):
|
|
| 89 |
const observer = new MutationObserver(scrollDown);
|
| 90 |
observer.observe(container, { childList: true, subtree: true });
|
| 91 |
container.dataset.observed = "true";
|
| 92 |
-
scrollDown();
|
| 93 |
} else if (container) {
|
| 94 |
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
| 95 |
}
|
| 96 |
})();
|
| 97 |
</script>
|
| 98 |
"""
|
| 99 |
-
return
|
| 100 |
|
| 101 |
def get_active_users_html(current_user):
|
| 102 |
server.prune_server()
|
| 103 |
-
|
| 104 |
for user, data in server.active_users.items():
|
| 105 |
pic = data.get('pic') or "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 106 |
label = f"{user}" + (" (You)" if user == current_user else "")
|
| 107 |
-
|
| 108 |
<div style="display: flex; align-items: center; gap: 10px; font-size: 0.85em; padding: 4px;">
|
| 109 |
<img src="{pic}" style="width: 22px; height: 22px; border-radius: 50%;">
|
| 110 |
<span style="color: #ccc;">{label}</span>
|
| 111 |
<div style="width: 8px; height: 8px; background: #4ade80; border-radius: 50%; margin-left: auto;"></div>
|
| 112 |
</div>
|
| 113 |
"""
|
| 114 |
-
return
|
| 115 |
|
| 116 |
# --- CSS ---
|
| 117 |
custom_css = """
|
| 118 |
-
/* Modern Font Stack */
|
| 119 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
| 120 |
|
| 121 |
body, .gradio-container, .main-title, #chat-container, #side-panel, textarea, button {
|
|
@@ -158,8 +167,7 @@ body, .gradio-container, .main-title, #chat-container, #side-panel, textarea, bu
|
|
| 158 |
}
|
| 159 |
"""
|
| 160 |
|
| 161 |
-
|
| 162 |
-
with gr.Blocks() as demo:
|
| 163 |
user_session = gr.State(None)
|
| 164 |
current_room = gr.State("Main")
|
| 165 |
dm_target = gr.State(None)
|
|
@@ -190,7 +198,7 @@ with gr.Blocks() as demo:
|
|
| 190 |
|
| 191 |
with gr.Row():
|
| 192 |
msg_input = gr.Textbox(
|
| 193 |
-
placeholder="Type a message...",
|
| 194 |
show_label=False,
|
| 195 |
scale=10,
|
| 196 |
elem_id="msg-input"
|
|
@@ -226,6 +234,7 @@ with gr.Blocks() as demo:
|
|
| 226 |
|
| 227 |
def send_msg(text, session, room, dm_user):
|
| 228 |
if not text or not session: return ""
|
|
|
|
| 229 |
if dm_user:
|
| 230 |
server.send_dm(session['name'], dm_user, text, session['pic'])
|
| 231 |
else:
|
|
@@ -246,8 +255,4 @@ with gr.Blocks() as demo:
|
|
| 246 |
room_list.change(lambda name: (name, None), room_list, [current_room, dm_target])
|
| 247 |
start_dm_btn.click(lambda target: ("Main", target), dm_user_input, [current_room, dm_target])
|
| 248 |
|
| 249 |
-
|
| 250 |
-
demo.launch(
|
| 251 |
-
theme=gr.themes.Soft(primary_hue="orange", neutral_hue="slate"),
|
| 252 |
-
css=custom_css
|
| 253 |
-
)
|
|
|
|
| 2 |
import time
|
| 3 |
from datetime import datetime
|
| 4 |
from collections import deque
|
| 5 |
+
import html
|
| 6 |
|
| 7 |
# --- GLOBAL SERVER STATE (In-Memory Only) ---
|
| 8 |
class ChatServer:
|
|
|
|
| 22 |
if u in self.user_rooms:
|
| 23 |
del self.user_rooms[u]
|
| 24 |
|
| 25 |
+
def _sanitize(self, text):
|
| 26 |
+
"""Prevents HTML injection by escaping user input."""
|
| 27 |
+
# Escape all HTML characters (&, <, >, ", ')
|
| 28 |
+
escaped = html.escape(text)
|
| 29 |
+
# Re-allow line breaks only
|
| 30 |
+
return escaped.replace("\n", "<br>")
|
| 31 |
+
|
| 32 |
def _add_message_to_queue(self, queue, user, message, profile_pic):
|
| 33 |
+
safe_text = self._sanitize(message)
|
| 34 |
+
|
| 35 |
# Stacking Logic: Check if last message was same user and same text
|
| 36 |
+
if queue and queue[-1]['user'] == user and queue[-1]['text'] == safe_text:
|
| 37 |
queue[-1]['count'] = queue[-1].get('count', 1) + 1
|
| 38 |
+
queue[-1]['time'] = datetime.now().strftime("%H:%M")
|
| 39 |
else:
|
| 40 |
msg_data = {
|
| 41 |
"user": user,
|
| 42 |
+
"text": safe_text,
|
| 43 |
"pic": profile_pic,
|
| 44 |
"time": datetime.now().strftime("%H:%M"),
|
| 45 |
"count": 1
|
|
|
|
| 65 |
if not messages:
|
| 66 |
return "<div style='color: gray; text-align: center; margin-top: 20px;'>No messages yet.</div>"
|
| 67 |
|
| 68 |
+
html_output = "<div id='chat-flow' style='display: flex; flex-direction: column; gap: 12px; font-family: sans-serif;'>"
|
| 69 |
for m in messages:
|
| 70 |
pic_url = m.get('pic') or "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 71 |
display_text = m['text']
|
| 72 |
if m.get('count', 1) > 1:
|
| 73 |
display_text += f" <span style='color: #888; font-size: 0.8em; font-weight: normal;'>(x{m['count']})</span>"
|
| 74 |
|
| 75 |
+
html_output += f"""
|
| 76 |
<div class="msg-row" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 4px;">
|
| 77 |
<img src="{pic_url}" style="width: 36px; height: 36px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.1); flex-shrink: 0;">
|
| 78 |
<div style="background: rgba(255, 255, 255, 0.07); padding: 10px 14px; border-radius: 0 16px 16px 16px; max-width: 85%;">
|
|
|
|
| 85 |
</div>
|
| 86 |
</div>
|
| 87 |
"""
|
| 88 |
+
|
| 89 |
+
# SYSTEM SCRIPT (Safe since it's hardcoded by us, not user input)
|
| 90 |
+
html_output += """
|
| 91 |
</div>
|
| 92 |
<script>
|
| 93 |
(function() {
|
|
|
|
| 99 |
const observer = new MutationObserver(scrollDown);
|
| 100 |
observer.observe(container, { childList: true, subtree: true });
|
| 101 |
container.dataset.observed = "true";
|
| 102 |
+
scrollDown();
|
| 103 |
} else if (container) {
|
| 104 |
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
| 105 |
}
|
| 106 |
})();
|
| 107 |
</script>
|
| 108 |
"""
|
| 109 |
+
return html_output
|
| 110 |
|
| 111 |
def get_active_users_html(current_user):
|
| 112 |
server.prune_server()
|
| 113 |
+
html_list = "<div style='display: flex; flex-direction: column; gap: 8px; font-family: sans-serif;'>"
|
| 114 |
for user, data in server.active_users.items():
|
| 115 |
pic = data.get('pic') or "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 116 |
label = f"{user}" + (" (You)" if user == current_user else "")
|
| 117 |
+
html_list += f"""
|
| 118 |
<div style="display: flex; align-items: center; gap: 10px; font-size: 0.85em; padding: 4px;">
|
| 119 |
<img src="{pic}" style="width: 22px; height: 22px; border-radius: 50%;">
|
| 120 |
<span style="color: #ccc;">{label}</span>
|
| 121 |
<div style="width: 8px; height: 8px; background: #4ade80; border-radius: 50%; margin-left: auto;"></div>
|
| 122 |
</div>
|
| 123 |
"""
|
| 124 |
+
return html_list + "</div>"
|
| 125 |
|
| 126 |
# --- CSS ---
|
| 127 |
custom_css = """
|
|
|
|
| 128 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
| 129 |
|
| 130 |
body, .gradio-container, .main-title, #chat-container, #side-panel, textarea, button {
|
|
|
|
| 167 |
}
|
| 168 |
"""
|
| 169 |
|
| 170 |
+
with gr.Blocks(css=custom_css) as demo:
|
|
|
|
| 171 |
user_session = gr.State(None)
|
| 172 |
current_room = gr.State("Main")
|
| 173 |
dm_target = gr.State(None)
|
|
|
|
| 198 |
|
| 199 |
with gr.Row():
|
| 200 |
msg_input = gr.Textbox(
|
| 201 |
+
placeholder="Type a message (HTML is disabled)...",
|
| 202 |
show_label=False,
|
| 203 |
scale=10,
|
| 204 |
elem_id="msg-input"
|
|
|
|
| 234 |
|
| 235 |
def send_msg(text, session, room, dm_user):
|
| 236 |
if not text or not session: return ""
|
| 237 |
+
# The server class now handles sanitization
|
| 238 |
if dm_user:
|
| 239 |
server.send_dm(session['name'], dm_user, text, session['pic'])
|
| 240 |
else:
|
|
|
|
| 255 |
room_list.change(lambda name: (name, None), room_list, [current_room, dm_target])
|
| 256 |
start_dm_btn.click(lambda target: ("Main", target), dm_user_input, [current_room, dm_target])
|
| 257 |
|
| 258 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|