Zfintech89 commited on
Commit
b475683
·
0 Parent(s):

initial commit

Browse files
Files changed (4) hide show
  1. app.py +229 -0
  2. dockerfile +27 -0
  3. requirements.txt +4 -0
  4. templates/index.html +1085 -0
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi.templating import Jinja2Templates
5
+ import json
6
+ import asyncio
7
+ from datetime import datetime
8
+ from typing import Dict, List, Set
9
+ import base64
10
+ import mimetypes
11
+ import os
12
+
13
+ app = FastAPI(title="Tri-Chat API", description="Real-time chat with WebSocket support")
14
+
15
+ # Setup templates
16
+ templates = Jinja2Templates(directory="templates")
17
+
18
+ # Connection Manager to handle WebSocket connections
19
+ class ConnectionManager:
20
+ def __init__(self):
21
+ # Store active connections by room
22
+ self.active_connections: Dict[str, List[Dict]] = {}
23
+ # Store message history by room (in-memory for demo)
24
+ self.message_history: Dict[str, List[Dict]] = {}
25
+
26
+ async def connect(self, websocket: WebSocket, room: str, username: str):
27
+ await websocket.accept()
28
+
29
+ # Initialize room if it doesn't exist
30
+ if room not in self.active_connections:
31
+ self.active_connections[room] = []
32
+ self.message_history[room] = []
33
+
34
+ # Add connection to room
35
+ connection_info = {
36
+ "websocket": websocket,
37
+ "username": username,
38
+ "joined_at": datetime.now().isoformat()
39
+ }
40
+ self.active_connections[room].append(connection_info)
41
+
42
+ # Send join notification
43
+ join_message = {
44
+ "type": "system",
45
+ "message": f"{username} joined the room",
46
+ "timestamp": datetime.now().isoformat(),
47
+ "room": room
48
+ }
49
+ await self.broadcast_to_room(room, join_message)
50
+
51
+ # Send message history to new user
52
+ for message in self.message_history[room]:
53
+ await websocket.send_text(json.dumps(message))
54
+
55
+ def disconnect(self, websocket: WebSocket, room: str):
56
+ if room in self.active_connections:
57
+ # Find and remove the connection
58
+ for conn in self.active_connections[room]:
59
+ if conn["websocket"] == websocket:
60
+ self.active_connections[room].remove(conn)
61
+ return conn["username"]
62
+ return None
63
+
64
+ async def broadcast_to_room(self, room: str, message: dict):
65
+ if room not in self.active_connections:
66
+ return
67
+
68
+ # Store message in history
69
+ self.message_history[room].append(message)
70
+
71
+ # Keep only last 100 messages per room
72
+ if len(self.message_history[room]) > 100:
73
+ self.message_history[room] = self.message_history[room][-100:]
74
+
75
+ # Send to all connections in room
76
+ disconnected = []
77
+ for connection_info in self.active_connections[room]:
78
+ try:
79
+ await connection_info["websocket"].send_text(json.dumps(message))
80
+ except:
81
+ disconnected.append(connection_info)
82
+
83
+ # Remove disconnected clients
84
+ for conn in disconnected:
85
+ self.active_connections[room].remove(conn)
86
+
87
+ def get_room_users(self, room: str) -> List[str]:
88
+ if room not in self.active_connections:
89
+ return []
90
+ return [conn["username"] for conn in self.active_connections[room]]
91
+
92
+ # Global connection manager instance
93
+ manager = ConnectionManager()
94
+
95
+ @app.get("/", response_class=HTMLResponse)
96
+ async def get_chat_page():
97
+ """Serve the chat HTML page"""
98
+ try:
99
+ with open("templates/index.html", "r", encoding="utf-8") as f:
100
+ html_content = f.read()
101
+ return HTMLResponse(content=html_content)
102
+ except FileNotFoundError:
103
+ return HTMLResponse(
104
+ content="<h1>Error: templates/index.html not found</h1><p>Please make sure the templates directory exists with index.html</p>",
105
+ status_code=404
106
+ )
107
+
108
+ @app.websocket("/ws/{room}")
109
+ async def websocket_endpoint(websocket: WebSocket, room: str, username: str):
110
+ """WebSocket endpoint for real-time chat"""
111
+
112
+ # Validate inputs
113
+ if not username or len(username.strip()) == 0:
114
+ await websocket.close(code=1008, reason="Username is required")
115
+ return
116
+
117
+ if not room or len(room.strip()) == 0:
118
+ room = "global"
119
+
120
+ # Sanitize inputs
121
+ username = username.strip()[:20] # Limit username length
122
+ room = room.strip()[:30] # Limit room name length
123
+
124
+ await manager.connect(websocket, room, username)
125
+
126
+ try:
127
+ while True:
128
+ # Receive message from client
129
+ data = await websocket.receive_text()
130
+ message_data = json.loads(data)
131
+
132
+ # Validate message type
133
+ if message_data.get("type") not in ["text", "file"]:
134
+ continue
135
+
136
+ # Process text message
137
+ if message_data["type"] == "text":
138
+ text_content = message_data.get("text", "").strip()
139
+ if len(text_content) == 0:
140
+ continue
141
+
142
+ # Sanitize and limit text length
143
+ text_content = text_content[:500]
144
+
145
+ message = {
146
+ "type": "text",
147
+ "username": username,
148
+ "text": text_content,
149
+ "timestamp": datetime.now().isoformat(),
150
+ "room": room
151
+ }
152
+
153
+ await manager.broadcast_to_room(room, message)
154
+
155
+ # Process file message
156
+ elif message_data["type"] == "file":
157
+ file_name = message_data.get("fileName", "unknown")[:100]
158
+ file_type = message_data.get("fileType", "application/octet-stream")
159
+ file_size = message_data.get("fileSize", 0)
160
+ file_data = message_data.get("fileData", "")
161
+
162
+ # Validate file size (5MB limit)
163
+ if file_size > 5 * 1024 * 1024:
164
+ await websocket.send_text(json.dumps({
165
+ "type": "error",
166
+ "message": "File size exceeds 5MB limit"
167
+ }))
168
+ continue
169
+
170
+ # Validate base64 data
171
+ try:
172
+ base64.b64decode(file_data)
173
+ except Exception:
174
+ await websocket.send_text(json.dumps({
175
+ "type": "error",
176
+ "message": "Invalid file data"
177
+ }))
178
+ continue
179
+
180
+ message = {
181
+ "type": "file",
182
+ "username": username,
183
+ "fileName": file_name,
184
+ "fileType": file_type,
185
+ "fileSize": file_size,
186
+ "fileData": file_data,
187
+ "timestamp": datetime.now().isoformat(),
188
+ "room": room
189
+ }
190
+
191
+ await manager.broadcast_to_room(room, message)
192
+
193
+ except WebSocketDisconnect:
194
+ disconnected_username = manager.disconnect(websocket, room)
195
+ if disconnected_username:
196
+ leave_message = {
197
+ "type": "system",
198
+ "message": f"{disconnected_username} left the room",
199
+ "timestamp": datetime.now().isoformat(),
200
+ "room": room
201
+ }
202
+ await manager.broadcast_to_room(room, leave_message)
203
+
204
+ @app.get("/api/rooms")
205
+ async def get_active_rooms():
206
+ """Get list of active chat rooms"""
207
+ rooms = []
208
+ for room_name, connections in manager.active_connections.items():
209
+ if connections: # Only include rooms with active connections
210
+ rooms.append({
211
+ "name": room_name,
212
+ "user_count": len(connections),
213
+ "users": [conn["username"] for conn in connections]
214
+ })
215
+ return {"rooms": rooms}
216
+
217
+ @app.get("/api/rooms/{room}/users")
218
+ async def get_room_users(room: str):
219
+ """Get list of users in a specific room"""
220
+ users = manager.get_room_users(room)
221
+ return {
222
+ "room": room,
223
+ "users": users,
224
+ "user_count": len(users)
225
+ }
226
+
227
+ if __name__ == "__main__":
228
+ import uvicorn
229
+ uvicorn.run(app, host="0.0.0.0", port=8000)
dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ curl \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first for caching
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY . .
21
+
22
+ # Create non-root user for security
23
+ RUN useradd -m appuser && chown -R appuser /app
24
+ USER appuser
25
+
26
+ # Hugging Face requires apps to run on port 7860
27
+ CMD uvicorn app:app --host 0.0.0.0 --port 7860
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ websockets==12.0
4
+ python-multipart==0.0.6
templates/index.html ADDED
@@ -0,0 +1,1085 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Tri-Chat - Premium Chat Experience</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ /* Light theme */
11
+ --primary-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
12
+ --secondary-bg: rgba(255, 255, 255, 0.8);
13
+ --card-bg: rgba(255, 255, 255, 0.9);
14
+ --text-primary: #1a202c;
15
+ --text-secondary: #4a5568;
16
+ --text-muted: #718096;
17
+ --border-color: rgba(0, 0, 0, 0.08);
18
+ --accent-primary: #3b82f6;
19
+ --accent-secondary: #60a5fa;
20
+ --accent-success: #10b981;
21
+ --accent-danger: #ef4444;
22
+ --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.05);
23
+ --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1);
24
+ --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.12);
25
+ --blur-light: blur(10px);
26
+ --blur-medium: blur(20px);
27
+ }
28
+
29
+ [data-theme="dark"] {
30
+ --primary-bg: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
31
+ --secondary-bg: rgba(30, 41, 59, 0.8);
32
+ --card-bg: rgba(30, 41, 59, 0.9);
33
+ --text-primary: #f1f5f9;
34
+ --text-secondary: #cbd5e1;
35
+ --text-muted: #94a3b8;
36
+ --border-color: rgba(255, 255, 255, 0.1);
37
+ --accent-primary: #3b82f6;
38
+ --accent-secondary: #60a5fa;
39
+ --accent-success: #10b981;
40
+ --accent-danger: #ef4444;
41
+ --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.3);
42
+ --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.4);
43
+ --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.5);
44
+ }
45
+
46
+ * {
47
+ margin: 0;
48
+ padding: 0;
49
+ box-sizing: border-box;
50
+ }
51
+
52
+ body {
53
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
54
+ background: var(--primary-bg);
55
+ color: var(--text-primary);
56
+ height: 100vh;
57
+ display: flex;
58
+ flex-direction: column;
59
+ font-size: 14px;
60
+ line-height: 1.5;
61
+ transition: all 0.3s ease;
62
+ }
63
+
64
+ .header {
65
+ background: var(--card-bg);
66
+ backdrop-filter: var(--blur-medium);
67
+ padding: 1.5rem;
68
+ border-bottom: 1px solid var(--border-color);
69
+ box-shadow: var(--shadow-light);
70
+ position: relative;
71
+ }
72
+
73
+ .header::before {
74
+ content: '';
75
+ position: absolute;
76
+ top: 0;
77
+ left: 0;
78
+ right: 0;
79
+ height: 3px;
80
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
81
+ }
82
+
83
+ .header-top {
84
+ display: flex;
85
+ justify-content: space-between;
86
+ align-items: center;
87
+ margin-bottom: 1.5rem;
88
+ }
89
+
90
+ .logo {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 0.75rem;
94
+ }
95
+
96
+ .logo i {
97
+ font-size: 1.5rem;
98
+ color: var(--accent-primary);
99
+ }
100
+
101
+ .header h1 {
102
+ color: var(--text-primary);
103
+ font-size: 1.5rem;
104
+ font-weight: 700;
105
+ letter-spacing: -0.025em;
106
+ }
107
+
108
+ .theme-toggle {
109
+ background: var(--secondary-bg);
110
+ border: 1px solid var(--border-color);
111
+ border-radius: 12px;
112
+ padding: 0.5rem;
113
+ cursor: pointer;
114
+ transition: all 0.2s ease;
115
+ color: var(--text-secondary);
116
+ }
117
+
118
+ .theme-toggle:hover {
119
+ background: var(--card-bg);
120
+ transform: translateY(-1px);
121
+ box-shadow: var(--shadow-light);
122
+ }
123
+
124
+ .connection-form {
125
+ display: flex;
126
+ gap: 1rem;
127
+ align-items: center;
128
+ flex-wrap: wrap;
129
+ }
130
+
131
+ .input-group {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 0.25rem;
135
+ min-width: 180px;
136
+ }
137
+
138
+ .input-group label {
139
+ font-size: 0.75rem;
140
+ font-weight: 500;
141
+ color: var(--text-muted);
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.05em;
144
+ }
145
+
146
+ .connection-form input {
147
+ padding: 0.75rem 1rem;
148
+ border: 1px solid var(--border-color);
149
+ border-radius: 12px;
150
+ background: var(--secondary-bg);
151
+ backdrop-filter: var(--blur-light);
152
+ color: var(--text-primary);
153
+ font-size: 0.875rem;
154
+ transition: all 0.2s ease;
155
+ outline: none;
156
+ }
157
+
158
+ .connection-form input:focus {
159
+ border-color: var(--accent-primary);
160
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
161
+ }
162
+
163
+ .connection-form input::placeholder {
164
+ color: var(--text-muted);
165
+ }
166
+
167
+ .btn {
168
+ padding: 0.75rem 1.5rem;
169
+ border: none;
170
+ border-radius: 12px;
171
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
172
+ color: white;
173
+ cursor: pointer;
174
+ font-weight: 600;
175
+ font-size: 0.875rem;
176
+ transition: all 0.2s ease;
177
+ position: relative;
178
+ overflow: hidden;
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 0.5rem;
182
+ box-shadow: var(--shadow-light);
183
+ }
184
+
185
+ .btn::before {
186
+ content: '';
187
+ position: absolute;
188
+ top: 0;
189
+ left: -100%;
190
+ width: 100%;
191
+ height: 100%;
192
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
193
+ transition: left 0.5s;
194
+ }
195
+
196
+ .btn:hover::before {
197
+ left: 100%;
198
+ }
199
+
200
+ .btn:hover {
201
+ transform: translateY(-2px);
202
+ box-shadow: var(--shadow-medium);
203
+ }
204
+
205
+ .btn:active {
206
+ transform: translateY(0);
207
+ }
208
+
209
+ .btn:disabled {
210
+ background: var(--text-muted);
211
+ cursor: not-allowed;
212
+ transform: none;
213
+ opacity: 0.6;
214
+ }
215
+
216
+ .btn-secondary {
217
+ background: transparent;
218
+ border: 1px solid var(--border-color);
219
+ color: var(--text-secondary);
220
+ }
221
+
222
+ .btn-secondary:hover {
223
+ background: var(--secondary-bg);
224
+ }
225
+
226
+ .main-content {
227
+ display: flex;
228
+ flex: 1;
229
+ overflow: hidden;
230
+ gap: 1px;
231
+ }
232
+
233
+ .sidebar {
234
+ width: 280px;
235
+ background: var(--card-bg);
236
+ backdrop-filter: var(--blur-light);
237
+ border-right: 1px solid var(--border-color);
238
+ display: flex;
239
+ flex-direction: column;
240
+ transition: all 0.3s ease;
241
+ }
242
+
243
+ .sidebar-header {
244
+ padding: 1.5rem;
245
+ border-bottom: 1px solid var(--border-color);
246
+ }
247
+
248
+ .sidebar-header h3 {
249
+ color: var(--text-primary);
250
+ font-size: 1rem;
251
+ font-weight: 600;
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 0.5rem;
255
+ }
256
+
257
+ .sidebar-header i {
258
+ color: var(--accent-success);
259
+ }
260
+
261
+ .sidebar-content {
262
+ flex: 1;
263
+ padding: 1rem;
264
+ overflow-y: auto;
265
+ }
266
+
267
+ .user-list {
268
+ list-style: none;
269
+ display: flex;
270
+ flex-direction: column;
271
+ gap: 0.5rem;
272
+ }
273
+
274
+ .user-list li {
275
+ padding: 0.75rem 1rem;
276
+ color: var(--text-secondary);
277
+ background: var(--secondary-bg);
278
+ border-radius: 8px;
279
+ transition: all 0.2s ease;
280
+ display: flex;
281
+ align-items: center;
282
+ gap: 0.75rem;
283
+ }
284
+
285
+ .user-list li:hover {
286
+ background: var(--card-bg);
287
+ transform: translateX(2px);
288
+ }
289
+
290
+ .user-list li::before {
291
+ content: '';
292
+ width: 8px;
293
+ height: 8px;
294
+ border-radius: 50%;
295
+ background: var(--accent-success);
296
+ flex-shrink: 0;
297
+ }
298
+
299
+ .chat-container {
300
+ flex: 1;
301
+ display: flex;
302
+ flex-direction: column;
303
+ background: var(--secondary-bg);
304
+ backdrop-filter: var(--blur-light);
305
+ }
306
+
307
+ .messages {
308
+ flex: 1;
309
+ overflow-y: auto;
310
+ padding: 1.5rem;
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 1rem;
314
+ scroll-behavior: smooth;
315
+ }
316
+
317
+ .messages::-webkit-scrollbar {
318
+ width: 6px;
319
+ }
320
+
321
+ .messages::-webkit-scrollbar-track {
322
+ background: transparent;
323
+ }
324
+
325
+ .messages::-webkit-scrollbar-thumb {
326
+ background: var(--border-color);
327
+ border-radius: 3px;
328
+ }
329
+
330
+ .messages::-webkit-scrollbar-thumb:hover {
331
+ background: var(--text-muted);
332
+ }
333
+
334
+ .message {
335
+ background: var(--card-bg);
336
+ backdrop-filter: var(--blur-light);
337
+ border-radius: 16px;
338
+ padding: 1rem 1.25rem;
339
+ max-width: 75%;
340
+ border: 1px solid var(--border-color);
341
+ box-shadow: var(--shadow-light);
342
+ position: relative;
343
+ animation: messageSlide 0.3s ease-out;
344
+ }
345
+
346
+ @keyframes messageSlide {
347
+ from {
348
+ opacity: 0;
349
+ transform: translateY(10px);
350
+ }
351
+ to {
352
+ opacity: 1;
353
+ transform: translateY(0);
354
+ }
355
+ }
356
+
357
+ .message.own {
358
+ align-self: flex-end;
359
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
360
+ color: white;
361
+ border: none;
362
+ }
363
+
364
+ .message.own .username,
365
+ .message.own .timestamp {
366
+ color: rgba(255, 255, 255, 0.9);
367
+ }
368
+
369
+ .message.system {
370
+ align-self: center;
371
+ background: var(--secondary-bg);
372
+ border: 1px solid var(--border-color);
373
+ font-style: italic;
374
+ color: var(--text-muted);
375
+ max-width: 60%;
376
+ text-align: center;
377
+ font-size: 0.8rem;
378
+ }
379
+
380
+ .message-header {
381
+ display: flex;
382
+ justify-content: space-between;
383
+ align-items: center;
384
+ margin-bottom: 0.5rem;
385
+ font-size: 0.75rem;
386
+ }
387
+
388
+ .username {
389
+ font-weight: 600;
390
+ color: var(--accent-primary);
391
+ }
392
+
393
+ .message.own .username {
394
+ color: rgba(255, 255, 255, 0.9);
395
+ }
396
+
397
+ .timestamp {
398
+ color: var(--text-muted);
399
+ font-size: 0.7rem;
400
+ }
401
+
402
+ .message-content {
403
+ word-wrap: break-word;
404
+ line-height: 1.4;
405
+ }
406
+
407
+ .file-preview img {
408
+ max-width: 200px;
409
+ max-height: 150px;
410
+ border-radius: 8px;
411
+ margin-top: 0.75rem;
412
+ box-shadow: var(--shadow-light);
413
+ }
414
+
415
+ .file-download {
416
+ display: inline-flex;
417
+ align-items: center;
418
+ gap: 0.5rem;
419
+ margin-top: 0.75rem;
420
+ padding: 0.5rem 1rem;
421
+ background: var(--secondary-bg);
422
+ border-radius: 8px;
423
+ color: var(--accent-primary);
424
+ text-decoration: none;
425
+ font-size: 0.8rem;
426
+ transition: all 0.2s ease;
427
+ border: 1px solid var(--border-color);
428
+ }
429
+
430
+ .file-download:hover {
431
+ background: var(--card-bg);
432
+ transform: translateY(-1px);
433
+ box-shadow: var(--shadow-light);
434
+ }
435
+
436
+ .input-area {
437
+ padding: 1.5rem;
438
+ background: var(--card-bg);
439
+ backdrop-filter: var(--blur-medium);
440
+ border-top: 1px solid var(--border-color);
441
+ }
442
+
443
+ .input-row {
444
+ display: flex;
445
+ gap: 1rem;
446
+ align-items: flex-end;
447
+ }
448
+
449
+ #messageInput {
450
+ flex: 1;
451
+ padding: 1rem 1.25rem;
452
+ border: 1px solid var(--border-color);
453
+ border-radius: 20px;
454
+ background: var(--secondary-bg);
455
+ backdrop-filter: var(--blur-light);
456
+ color: var(--text-primary);
457
+ resize: none;
458
+ outline: none;
459
+ transition: all 0.2s ease;
460
+ font-family: inherit;
461
+ max-height: 100px;
462
+ min-height: 44px;
463
+ }
464
+
465
+ #messageInput:focus {
466
+ border-color: var(--accent-primary);
467
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
468
+ }
469
+
470
+ #messageInput::placeholder {
471
+ color: var(--text-muted);
472
+ }
473
+
474
+ .file-input-wrapper {
475
+ position: relative;
476
+ }
477
+
478
+ .file-input-wrapper input[type=file] {
479
+ position: absolute;
480
+ opacity: 0;
481
+ width: 100%;
482
+ height: 100%;
483
+ cursor: pointer;
484
+ }
485
+
486
+ .file-input-label {
487
+ display: flex;
488
+ align-items: center;
489
+ justify-content: center;
490
+ width: 44px;
491
+ height: 44px;
492
+ background: var(--secondary-bg);
493
+ border: 1px solid var(--border-color);
494
+ border-radius: 50%;
495
+ cursor: pointer;
496
+ transition: all 0.2s ease;
497
+ color: var(--text-secondary);
498
+ }
499
+
500
+ .file-input-label:hover {
501
+ background: var(--accent-primary);
502
+ color: white;
503
+ transform: translateY(-2px);
504
+ box-shadow: var(--shadow-light);
505
+ }
506
+
507
+ .status {
508
+ padding: 0.75rem 1rem;
509
+ text-align: center;
510
+ font-size: 0.8rem;
511
+ border-radius: 8px;
512
+ margin-top: 1rem;
513
+ font-weight: 500;
514
+ }
515
+
516
+ .status.connected {
517
+ color: var(--accent-success);
518
+ background: rgba(16, 185, 129, 0.1);
519
+ border: 1px solid rgba(16, 185, 129, 0.2);
520
+ }
521
+
522
+ .status.disconnected {
523
+ color: var(--accent-danger);
524
+ background: rgba(239, 68, 68, 0.1);
525
+ border: 1px solid rgba(239, 68, 68, 0.2);
526
+ }
527
+
528
+ .status.neutral {
529
+ color: var(--text-muted);
530
+ background: var(--secondary-bg);
531
+ border: 1px solid var(--border-color);
532
+ }
533
+
534
+ /* Mobile optimizations */
535
+ @media (max-width: 768px) {
536
+ .header {
537
+ padding: 1rem;
538
+ }
539
+
540
+ .header-top {
541
+ margin-bottom: 1rem;
542
+ }
543
+
544
+ .header h1 {
545
+ font-size: 1.25rem;
546
+ }
547
+
548
+ .connection-form {
549
+ flex-direction: column;
550
+ gap: 0.75rem;
551
+ width: 100%;
552
+ }
553
+
554
+ .input-group {
555
+ width: 100%;
556
+ min-width: auto;
557
+ }
558
+
559
+ .connection-form input,
560
+ .btn {
561
+ width: 100%;
562
+ }
563
+
564
+ .main-content {
565
+ flex-direction: column;
566
+ height: calc(100vh - 200px);
567
+ }
568
+
569
+ .sidebar {
570
+ width: 100%;
571
+ height: 120px;
572
+ flex-direction: row;
573
+ border-right: none;
574
+ border-bottom: 1px solid var(--border-color);
575
+ }
576
+
577
+ .sidebar-header {
578
+ min-width: 120px;
579
+ border-right: 1px solid var(--border-color);
580
+ border-bottom: none;
581
+ }
582
+
583
+ .sidebar-header h3 {
584
+ font-size: 0.9rem;
585
+ }
586
+
587
+ .sidebar-content {
588
+ overflow-x: auto;
589
+ overflow-y: hidden;
590
+ }
591
+
592
+ .user-list {
593
+ flex-direction: row;
594
+ white-space: nowrap;
595
+ padding-bottom: 0.5rem;
596
+ }
597
+
598
+ .user-list li {
599
+ flex-shrink: 0;
600
+ font-size: 0.8rem;
601
+ padding: 0.5rem 0.75rem;
602
+ }
603
+
604
+ .message {
605
+ max-width: 90%;
606
+ padding: 0.75rem 1rem;
607
+ }
608
+
609
+ .messages {
610
+ padding: 1rem;
611
+ }
612
+
613
+ .input-area {
614
+ padding: 1rem;
615
+ }
616
+
617
+ .input-row {
618
+ gap: 0.75rem;
619
+ }
620
+ }
621
+
622
+ @media (max-width: 480px) {
623
+ .header {
624
+ padding: 0.75rem;
625
+ }
626
+
627
+ .messages {
628
+ padding: 0.75rem;
629
+ gap: 0.75rem;
630
+ }
631
+
632
+ .message {
633
+ padding: 0.75rem;
634
+ border-radius: 12px;
635
+ }
636
+
637
+ .input-area {
638
+ padding: 0.75rem;
639
+ }
640
+ }
641
+
642
+ /* Smooth transitions for theme switching */
643
+ * {
644
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
645
+ }
646
+ </style>
647
+ </head>
648
+ <body>
649
+ <div class="header">
650
+ <div class="header-top">
651
+ <div class="logo">
652
+ <i class="fas fa-comments"></i>
653
+ <h1>Tri-Chat</h1>
654
+ </div>
655
+ <button class="theme-toggle" id="themeToggle">
656
+ <i class="fas fa-moon" id="themeIcon"></i>
657
+ </button>
658
+ </div>
659
+
660
+ <div class="connection-form">
661
+ <div class="input-group">
662
+ <label for="usernameInput">Your Name</label>
663
+ <input type="text" id="usernameInput" placeholder="Enter your name" maxlength="20">
664
+ </div>
665
+ <div class="input-group">
666
+ <label for="roomInput">Room</label>
667
+ <input type="text" id="roomInput" placeholder="global" maxlength="30">
668
+ </div>
669
+ <button class="btn" id="connectBtn">
670
+ <i class="fas fa-plug"></i>
671
+ Connect
672
+ </button>
673
+ <button class="btn btn-secondary" id="disconnectBtn" style="display: none;">
674
+ <i class="fas fa-sign-out-alt"></i>
675
+ Disconnect
676
+ </button>
677
+ </div>
678
+
679
+ <div class="status neutral" id="status">
680
+ <i class="fas fa-info-circle"></i>
681
+ Enter your name and click Connect to start chatting
682
+ </div>
683
+ </div>
684
+
685
+ <div class="main-content">
686
+ <div class="sidebar">
687
+ <div class="sidebar-header">
688
+ <h3>
689
+ <i class="fas fa-users"></i>
690
+ Online
691
+ </h3>
692
+ </div>
693
+ <div class="sidebar-content">
694
+ <ul class="user-list" id="userList"></ul>
695
+ </div>
696
+ </div>
697
+
698
+ <div class="chat-container">
699
+ <div class="messages" id="messages"></div>
700
+
701
+ <div class="input-area" style="display: none;" id="inputArea">
702
+ <div class="input-row">
703
+ <div class="file-input-wrapper">
704
+ <input type="file" id="fileInput" accept="*/*">
705
+ <label for="fileInput" class="file-input-label">
706
+ <i class="fas fa-paperclip"></i>
707
+ </label>
708
+ </div>
709
+ <textarea id="messageInput" placeholder="Type your message..." maxlength="500" rows="1"></textarea>
710
+ <button class="btn" id="sendBtn">
711
+ <i class="fas fa-paper-plane"></i>
712
+ Send
713
+ </button>
714
+ </div>
715
+ </div>
716
+ </div>
717
+ </div>
718
+
719
+ <script>
720
+ class TriChat {
721
+ constructor() {
722
+ this.ws = null;
723
+ this.username = '';
724
+ this.room = 'global';
725
+ this.isConnected = false;
726
+ this.currentTheme = 'light';
727
+
728
+ this.initElements();
729
+ this.bindEvents();
730
+ this.initTheme();
731
+ this.setupMessageInputResize();
732
+ }
733
+
734
+ initElements() {
735
+ this.elements = {
736
+ usernameInput: document.getElementById('usernameInput'),
737
+ roomInput: document.getElementById('roomInput'),
738
+ connectBtn: document.getElementById('connectBtn'),
739
+ disconnectBtn: document.getElementById('disconnectBtn'),
740
+ status: document.getElementById('status'),
741
+ messages: document.getElementById('messages'),
742
+ messageInput: document.getElementById('messageInput'),
743
+ sendBtn: document.getElementById('sendBtn'),
744
+ fileInput: document.getElementById('fileInput'),
745
+ inputArea: document.getElementById('inputArea'),
746
+ userList: document.getElementById('userList'),
747
+ themeToggle: document.getElementById('themeToggle'),
748
+ themeIcon: document.getElementById('themeIcon')
749
+ };
750
+ }
751
+
752
+ initTheme() {
753
+ const savedTheme = localStorage.getItem('tri-chat-theme') || 'light';
754
+ this.setTheme(savedTheme);
755
+ }
756
+
757
+ setTheme(theme) {
758
+ this.currentTheme = theme;
759
+ document.documentElement.setAttribute('data-theme', theme);
760
+
761
+ if (theme === 'dark') {
762
+ this.elements.themeIcon.className = 'fas fa-sun';
763
+ } else {
764
+ this.elements.themeIcon.className = 'fas fa-moon';
765
+ }
766
+
767
+ localStorage.setItem('tri-chat-theme', theme);
768
+ }
769
+
770
+ toggleTheme() {
771
+ const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
772
+ this.setTheme(newTheme);
773
+ }
774
+
775
+ setupMessageInputResize() {
776
+ this.elements.messageInput.addEventListener('input', () => {
777
+ this.elements.messageInput.style.height = 'auto';
778
+ this.elements.messageInput.style.height = Math.min(this.elements.messageInput.scrollHeight, 100) + 'px';
779
+ });
780
+ }
781
+
782
+ bindEvents() {
783
+ this.elements.connectBtn.addEventListener('click', () => this.connect());
784
+ this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
785
+ this.elements.sendBtn.addEventListener('click', () => this.sendMessage());
786
+ this.elements.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
787
+ this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
788
+
789
+ this.elements.messageInput.addEventListener('keypress', (e) => {
790
+ if (e.key === 'Enter' && !e.shiftKey) {
791
+ e.preventDefault();
792
+ this.sendMessage();
793
+ }
794
+ });
795
+
796
+ this.elements.usernameInput.addEventListener('keypress', (e) => {
797
+ if (e.key === 'Enter') {
798
+ this.connect();
799
+ }
800
+ });
801
+ }
802
+
803
+ async connect() {
804
+ const username = this.elements.usernameInput.value.trim();
805
+ const room = this.elements.roomInput.value.trim() || 'global';
806
+
807
+ if (!username) {
808
+ this.showNotification('Please enter your name', 'error');
809
+ this.elements.usernameInput.focus();
810
+ return;
811
+ }
812
+
813
+ this.username = username;
814
+ this.room = room;
815
+
816
+ try {
817
+ this.updateStatus('Connecting...', 'neutral');
818
+
819
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
820
+ const wsUrl = `${protocol}//${window.location.host}/ws/${room}?username=${encodeURIComponent(username)}`;
821
+
822
+ this.ws = new WebSocket(wsUrl);
823
+
824
+ this.ws.onopen = () => {
825
+ this.isConnected = true;
826
+ this.updateUI();
827
+ this.updateStatus(`Connected to ${room}`, 'connected');
828
+ this.showNotification('Successfully connected!', 'success');
829
+ };
830
+
831
+ this.ws.onmessage = (event) => {
832
+ const message = JSON.parse(event.data);
833
+ this.displayMessage(message);
834
+ if (message.type === 'user_list') {
835
+ this.updateUserList(message.users);
836
+ }
837
+ };
838
+
839
+ this.ws.onclose = () => {
840
+ this.isConnected = false;
841
+ this.updateUI();
842
+ this.updateStatus('Disconnected', 'disconnected');
843
+ };
844
+
845
+ this.ws.onerror = (error) => {
846
+ console.error('WebSocket error:', error);
847
+ this.updateStatus('Connection error', 'disconnected');
848
+ this.showNotification('Connection failed', 'error');
849
+ };
850
+
851
+ } catch (error) {
852
+ console.error('Connection failed:', error);
853
+ this.updateStatus('Failed to connect', 'disconnected');
854
+ this.showNotification('Failed to connect', 'error');
855
+ }
856
+ }
857
+
858
+ disconnect() {
859
+ if (this.ws) {
860
+ this.ws.close();
861
+ }
862
+ }
863
+
864
+ updateUI() {
865
+ if (this.isConnected) {
866
+ this.elements.connectBtn.style.display = 'none';
867
+ this.elements.disconnectBtn.style.display = 'flex';
868
+ this.elements.inputArea.style.display = 'block';
869
+ this.elements.usernameInput.disabled = true;
870
+ this.elements.roomInput.disabled = true;
871
+ this.elements.messageInput.focus();
872
+ } else {
873
+ this.elements.connectBtn.style.display = 'flex';
874
+ this.elements.disconnectBtn.style.display = 'none';
875
+ this.elements.inputArea.style.display = 'none';
876
+ this.elements.usernameInput.disabled = false;
877
+ this.elements.roomInput.disabled = false;
878
+ this.elements.messages.innerHTML = '';
879
+ this.elements.userList.innerHTML = '';
880
+ }
881
+ }
882
+
883
+ updateStatus(text, type) {
884
+ this.elements.status.innerHTML = `
885
+ <i class="fas ${type === 'connected' ? 'fa-check-circle' : type === 'disconnected' ? 'fa-times-circle' : 'fa-info-circle'}"></i>
886
+ ${text}
887
+ `;
888
+ this.elements.status.className = 'status ' + type;
889
+ }
890
+
891
+ showNotification(message, type) {
892
+ // Simple notification - you could enhance this with a toast library
893
+ console.log(`${type.toUpperCase()}: ${message}`);
894
+ }
895
+
896
+ updateUserList(users) {
897
+ this.elements.userList.innerHTML = '';
898
+ users.forEach(user => {
899
+ const li = document.createElement('li');
900
+ li.textContent = user;
901
+ this.elements.userList.appendChild(li);
902
+ });
903
+ }
904
+
905
+ sendMessage() {
906
+ const text = this.elements.messageInput.value.trim();
907
+ if (!text || !this.isConnected) return;
908
+
909
+ const message = {
910
+ type: 'text',
911
+ username: this.username,
912
+ text: text,
913
+ timestamp: new Date().toISOString(),
914
+ room: this.room
915
+ };
916
+
917
+ this.ws.send(JSON.stringify(message));
918
+ this.elements.messageInput.value = '';
919
+ this.elements.messageInput.style.height = 'auto';
920
+ }
921
+
922
+ async handleFileSelect(event) {
923
+ const file = event.target.files[0];
924
+ if (!file) return;
925
+
926
+ if (file.size > 5 * 1024 * 1024) {
927
+ this.showNotification('File size must be less than 5MB', 'error');
928
+ return;
929
+ }
930
+
931
+ try {
932
+ const base64Data = await this.fileToBase64(file);
933
+
934
+ const message = {
935
+ type: 'file',
936
+ username: this.username,
937
+ fileName: file.name,
938
+ fileType: file.type,
939
+ fileSize: file.size,
940
+ fileData: base64Data,
941
+ timestamp: new Date().toISOString(),
942
+ room: this.room
943
+ };
944
+
945
+ this.ws.send(JSON.stringify(message));
946
+ event.target.value = '';
947
+ this.showNotification('File uploaded successfully', 'success');
948
+
949
+ } catch (error) {
950
+ console.error('File upload error:', error);
951
+ this.showNotification('Failed to upload file', 'error');
952
+ }
953
+ }
954
+
955
+ fileToBase64(file) {
956
+ return new Promise((resolve, reject) => {
957
+ const reader = new FileReader();
958
+ reader.onload = () => resolve(reader.result.split(',')[1]);
959
+ reader.onerror = reject;
960
+ reader.readAsDataURL(file);
961
+ });
962
+ }
963
+
964
+ displayMessage(message) {
965
+ const messageElement = document.createElement('div');
966
+ messageElement.className = 'message';
967
+
968
+ if (message.type === 'system') {
969
+ messageElement.className += ' system';
970
+ messageElement.innerHTML = `
971
+ <div class="message-content">
972
+ <i class="fas fa-info-circle"></i>
973
+ ${this.escapeHtml(message.message)}
974
+ </div>
975
+ `;
976
+ } else {
977
+ if (message.username === this.username) {
978
+ messageElement.className += ' own';
979
+ }
980
+
981
+ const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
982
+ hour: '2-digit',
983
+ minute: '2-digit'
984
+ });
985
+
986
+ if (message.type === 'text') {
987
+ messageElement.innerHTML = `
988
+ <div class="message-header">
989
+ <span class="username">${this.escapeHtml(message.username)}</span>
990
+ <span class="timestamp">${timestamp}</span>
991
+ </div>
992
+ <div class="message-content">${this.escapeHtml(message.text)}</div>
993
+ `;
994
+ } else if (message.type === 'file') {
995
+ const downloadUrl = 'data:' + message.fileType + ';base64,' + message.fileData;
996
+ let preview = '';
997
+ let fileIcon = 'fas fa-file';
998
+
999
+ if (message.fileType.startsWith('image/')) {
1000
+ preview = `<div class="file-preview"><img src="${downloadUrl}" alt="${message.fileName}"></div>`;
1001
+ fileIcon = 'fas fa-image';
1002
+ } else if (message.fileType.startsWith('video/')) {
1003
+ fileIcon = 'fas fa-video';
1004
+ } else if (message.fileType.startsWith('audio/')) {
1005
+ fileIcon = 'fas fa-music';
1006
+ } else if (message.fileType.includes('pdf')) {
1007
+ fileIcon = 'fas fa-file-pdf';
1008
+ } else if (message.fileType.includes('document') || message.fileType.includes('text')) {
1009
+ fileIcon = 'fas fa-file-alt';
1010
+ }
1011
+
1012
+ messageElement.innerHTML = `
1013
+ <div class="message-header">
1014
+ <span class="username">${this.escapeHtml(message.username)}</span>
1015
+ <span class="timestamp">${timestamp}</span>
1016
+ </div>
1017
+ <div class="message-content">
1018
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
1019
+ <i class="${fileIcon}"></i>
1020
+ <strong>${this.escapeHtml(message.fileName)}</strong>
1021
+ </div>
1022
+ <div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem;">
1023
+ ${this.formatFileSize(message.fileSize)}
1024
+ </div>
1025
+ ${preview}
1026
+ <a href="${downloadUrl}" download="${message.fileName}" class="file-download">
1027
+ <i class="fas fa-download"></i>
1028
+ Download
1029
+ </a>
1030
+ </div>
1031
+ `;
1032
+ }
1033
+ }
1034
+
1035
+ this.elements.messages.appendChild(messageElement);
1036
+ this.elements.messages.scrollTop = this.elements.messages.scrollHeight;
1037
+
1038
+ // Add subtle animation
1039
+ messageElement.style.opacity = '0';
1040
+ messageElement.style.transform = 'translateY(10px)';
1041
+
1042
+ requestAnimationFrame(() => {
1043
+ messageElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
1044
+ messageElement.style.opacity = '1';
1045
+ messageElement.style.transform = 'translateY(0)';
1046
+ });
1047
+ }
1048
+
1049
+ escapeHtml(text) {
1050
+ const div = document.createElement('div');
1051
+ div.textContent = text;
1052
+ return div.innerHTML;
1053
+ }
1054
+
1055
+ formatFileSize(bytes) {
1056
+ if (bytes === 0) return '0 Bytes';
1057
+ const k = 1024;
1058
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1059
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1060
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1061
+ }
1062
+ }
1063
+
1064
+ // Initialize the chat application
1065
+ document.addEventListener('DOMContentLoaded', () => {
1066
+ const triChat = new TriChat();
1067
+
1068
+ // Add some welcome messages for demo
1069
+ if (!triChat.isConnected) {
1070
+ setTimeout(() => {
1071
+ const welcomeMessage = document.createElement('div');
1072
+ welcomeMessage.className = 'message system';
1073
+ welcomeMessage.innerHTML = `
1074
+ <div class="message-content">
1075
+ <i class="fas fa-rocket"></i>
1076
+ Welcome to Tri-Chat! Connect with your name to start chatting.
1077
+ </div>
1078
+ `;
1079
+ triChat.elements.messages.appendChild(welcomeMessage);
1080
+ }, 500);
1081
+ }
1082
+ });
1083
+ </script>
1084
+ </body>
1085
+ </html>