Antaram commited on
Commit
a299189
·
verified ·
1 Parent(s): decab88

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +319 -244
templates/index.html CHANGED
@@ -3,384 +3,459 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Antaram Chat Pro</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
-
11
  <style>
12
- body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #f4f4f5; -webkit-tap-highlight-color: transparent; }
 
 
 
 
 
 
 
 
 
 
13
 
14
- /* Animations */
15
- @keyframes slideUp { 0% { opacity:0; transform:translateY(10px); } 100% { opacity:1; transform:translateY(0); } }
16
- .animate-enter { animation: slideUp 0.3s ease-out forwards; }
 
 
 
 
17
 
18
- /* Glass UI */
19
- .glass { background: rgba(255,255,255,0.03); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.05); }
20
- .glass-strong { background: rgba(20,20,23,0.85); backdrop-filter: blur(16px); border-top: 1px solid rgba(255,255,255,0.08); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- /* AI Styles */
23
- .ai-bubble { background: linear-gradient(135deg, #172554 0%, #0f172a 100%); border: 1px solid rgba(59,130,246,0.2); }
24
- .ai-cursor::after { content: '▋'; animation: blink 1s infinite; color: #60a5fa; font-size: 0.8em; margin-left: 2px; }
25
- @keyframes blink { 50% { opacity: 0; } }
26
-
27
- /* Autocomplete Menu */
28
- #suggestionBox { z-index: 100; transition: all 0.2s; transform-origin: bottom left; }
29
- .suggestion-item { padding: 8px 12px; cursor: pointer; transition: background 0.1s; display: flex; align-items: center; gap: 8px; }
30
- .suggestion-item:hover, .suggestion-item.active { background: rgba(255,255,255,0.1); }
31
-
32
- /* Scrollbar */
33
  .custom-scroll::-webkit-scrollbar { width: 4px; }
34
- .custom-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
 
35
 
36
- /* Reply Line */
37
- .reply-line { border-left: 2px solid #60a5fa; padding-left: 8px; margin-bottom: 4px; opacity: 0.7; font-size: 0.75rem; }
38
  </style>
39
  </head>
40
- <body class="w-full h-[100dvh] overflow-hidden flex flex-col relative">
41
 
42
- <!-- Background -->
43
  <div class="absolute inset-0 z-0 pointer-events-none">
44
- <div class="absolute inset-0 bg-gradient-to-b from-black/60 to-[#09090b]"></div>
45
- <img src="https://images.unsplash.com/photo-1614850523459-c2f4c699c52e?q=80&w=2670&auto=format&fit=crop" class="w-full h-full object-cover opacity-20">
 
 
46
  </div>
47
 
48
- <!-- Suggestion Box (Floating) -->
49
- <div id="suggestionBox" class="hidden absolute glass bg-[#18181b] rounded-xl shadow-2xl w-64 max-h-48 overflow-y-auto text-sm border border-white/10 bottom-20 left-4">
50
- <!-- Items injected by JS -->
 
 
51
  </div>
52
 
53
- <!-- Login View -->
54
- <div id="homeView" class="relative z-10 w-full h-full flex items-center justify-center p-6">
55
- <div class="w-full max-w-sm glass p-8 rounded-[32px] shadow-2xl animate-enter">
56
- <h1 class="text-3xl font-semibold text-center mb-1">Antaram.ai</h1>
57
- <p class="text-center text-white/40 text-xs tracking-widest uppercase mb-8">Collaborative Intelligence</p>
58
-
59
- <div class="space-y-4">
60
- <input type="text" id="usernameInput" placeholder="Your Name" class="w-full bg-white/5 border border-white/10 rounded-2xl p-4 text-white focus:outline-none focus:border-blue-500/50 transition-all placeholder-white/20">
61
- <div id="roomInputContainer" class="hidden">
62
- <input type="text" id="roomInput" placeholder="Room ID" class="w-full bg-white/5 border border-white/10 rounded-2xl p-4 text-white focus:outline-none font-mono text-blue-400">
 
 
 
63
  </div>
64
- <button onclick="createRoom()" id="createBtn" class="w-full bg-[#f4f4f5] text-black font-semibold rounded-2xl p-4 hover:bg-white transition-all shadow-lg mt-2">Create Room</button>
65
- <button onclick="toggleJoin()" id="joinToggleBtn" class="w-full bg-white/5 text-white/70 font-medium rounded-2xl p-4 hover:bg-white/10 transition-all">Join Existing</button>
66
- <button onclick="joinRoom()" id="joinConfirmBtn" class="hidden w-full bg-blue-600 text-white font-semibold rounded-2xl p-4 hover:bg-blue-500 transition-all shadow-lg shadow-blue-900/40">Enter Room</button>
 
67
  </div>
68
  </div>
69
  </div>
70
 
71
- <!-- Chat View -->
72
  <div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col">
73
 
74
  <!-- Header -->
75
- <header class="flex-none h-16 px-4 flex justify-between items-center glass z-20">
76
  <div class="flex items-center gap-3">
77
- <button onclick="leaveRoom()" class="p-2 hover:bg-white/10 rounded-full transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg></button>
78
  <div>
79
- <h2 class="text-sm font-bold text-white tracking-wide">ROOM <span id="displayRoomId" class="text-blue-400 font-mono"></span></h2>
80
- <p class="text-[10px] text-white/50 flex items-center gap-1"><span class="w-1.5 h-1.5 bg-green-500 rounded-full"></span> Online</p>
81
  </div>
82
  </div>
83
- <button onclick="copyLink()" class="text-xs bg-white/5 hover:bg-white/10 px-3 py-1.5 rounded-lg border border-white/10 transition-colors">Share Link</button>
 
 
 
84
  </header>
85
 
86
  <!-- Messages -->
87
- <main class="flex-1 overflow-y-auto custom-scroll p-4 space-y-3 pb-32" id="messages">
88
- <!-- Injected -->
89
  </main>
90
 
91
- <!-- Input Area (Fixed) -->
92
- <div class="fixed bottom-0 w-full glass-strong z-30 px-3 py-3">
93
-
94
- <!-- Reply Preview -->
95
- <div id="replyPreview" class="hidden mx-auto max-w-3xl mb-2 bg-[#18181b]/90 border-l-2 border-blue-500 rounded-r-lg p-2 flex justify-between items-center">
96
- <div>
97
- <span class="text-[10px] text-blue-400 font-bold block">Replying to <span id="replyToUser">User</span></span>
98
- <span class="text-xs text-white/70 truncate block max-w-[200px]" id="replyToText">Message content...</span>
 
 
 
 
 
 
99
  </div>
100
- <button onclick="cancelReply()" class="p-1 hover:text-white text-white/40">&times;</button>
101
- </div>
102
 
103
- <!-- File Preview -->
104
- <div id="filePreview" class="hidden mx-auto max-w-3xl mb-2 bg-[#18181b]/90 rounded-lg p-2 flex justify-between items-center">
105
- <span class="text-xs text-white/80" id="fileName">file.name</span>
106
- <button onclick="clearFile()" class="p-1 hover:text-white text-white/40">&times;</button>
107
- </div>
108
 
109
- <!-- Input Bar -->
110
- <div class="max-w-3xl mx-auto flex items-end gap-2">
111
- <input type="file" id="fileInput" class="hidden" onchange="handleFileSelect()">
112
- <button onclick="document.getElementById('fileInput').click()" class="p-3 text-white/40 hover:text-white transition-colors"><svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg></button>
113
-
114
- <div class="flex-1 relative">
115
- <input type="text" id="messageInput" placeholder="Message... (@ for users, : for emoji)" autocomplete="off"
116
- class="w-full bg-white/5 border border-white/10 rounded-[24px] px-4 py-3 focus:outline-none focus:bg-white/10 transition-all text-sm text-white placeholder-white/30"
117
- onkeyup="handleInput(event)" onkeydown="navigateSuggestions(event)">
 
 
 
 
 
118
  </div>
119
-
120
- <button onclick="sendMessage()" class="bg-[#f4f4f5] text-black p-3 rounded-full hover:bg-white transition-transform active:scale-95 shadow-lg"><svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg></button>
121
  </div>
122
  </div>
123
  </div>
124
 
125
- <!-- Logic -->
126
  <script>
127
- // --- Config ---
128
  const EMOJIS = {
129
- 'smile': '😊', 'laugh': '😂', 'fire': '🔥', 'heart': '❤️', 'thumbsup': '👍',
130
- 'cry': '😭', 'think': '🤔', 'rocket': '🚀', 'check': '', 'star': '⭐',
131
- 'eyes': '👀', 'skull': '💀', 'party': '🎉', 'sad': '😔', 'cool': '😎'
132
  };
133
 
134
- let ws = null;
135
- let roomId = null;
136
- let username = localStorage.getItem('antaram_user') || '';
137
  let activeUsers = new Set(['Antaram.ai']);
138
- let replyContext = null; // { id, username, text }
139
- let selectedFile = null;
140
- let currentAiDiv = null;
141
 
142
- // --- Init ---
143
- if(username) document.getElementById('usernameInput').value = username;
144
- const serverRoom = "{{ room_id }}" !== "None" ? "{{ room_id }}" : new URLSearchParams(window.location.search).get('room');
145
-
146
- if(serverRoom) {
147
- toggleJoin();
148
- document.getElementById('roomInput').value = serverRoom;
149
- }
150
 
151
- // --- Core Functions ---
152
  function toggleJoin() {
153
  document.getElementById('createBtn').classList.add('hidden');
154
- document.getElementById('joinToggleBtn').classList.add('hidden');
155
  document.getElementById('roomInputContainer').classList.remove('hidden');
156
- document.getElementById('joinConfirmBtn').classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
 
159
  function createRoom() {
160
  username = document.getElementById('usernameInput').value.trim();
161
- if(!username) return alert("Name required");
162
  localStorage.setItem('antaram_user', username);
163
-
164
- fetch('/create-room', { method: 'POST' })
165
- .then(r => r.json())
166
- .then(d => connect(d.room_id));
167
  }
168
 
169
  function joinRoom() {
170
  username = document.getElementById('usernameInput').value.trim();
171
  roomId = document.getElementById('roomInput').value.trim().toUpperCase();
172
- if(!username || !roomId) return alert("Details required");
173
  localStorage.setItem('antaram_user', username);
174
  connect(roomId);
175
  }
176
 
177
  function connect(rid) {
178
  roomId = rid;
179
- document.getElementById('displayRoomId').innerText = rid;
180
  document.getElementById('homeView').classList.add('hidden');
181
  document.getElementById('chatView').classList.remove('hidden');
182
 
183
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
184
- ws = new WebSocket(`${protocol}//${window.location.host}/ws/${rid}`);
185
 
186
- ws.onopen = () => ws.send(JSON.stringify({ type: 'join', username }));
187
- ws.onmessage = (e) => handleData(JSON.parse(e.data));
188
- ws.onclose = () => alert("Connection Lost");
 
 
 
 
 
189
  }
190
 
 
191
  function handleData(data) {
192
- const list = document.getElementById('messages');
193
 
194
- if(data.type === 'history') {
195
- data.data.forEach(msg => renderMessage(msg));
196
- list.scrollTop = list.scrollHeight;
197
- return;
198
- }
199
-
200
- if(data.type === 'system') {
201
  if(data.users) {
202
  activeUsers = new Set(data.users);
203
  activeUsers.add('Antaram.ai');
204
  }
205
- // Optional: Render system msg
206
- return;
207
- }
208
-
209
- if(data.type === 'ai_start') {
210
- const div = document.createElement('div');
211
- div.className = 'flex w-full justify-start animate-enter';
212
- div.innerHTML = `<div class="max-w-[85%]"><span class="text-[10px] text-blue-400 font-bold ml-1">ANTARAM AI</span><div class="ai-bubble p-3 rounded-[20px] rounded-tl-sm text-sm ai-cursor leading-relaxed"></div></div>`;
213
- list.appendChild(div);
214
- currentAiDiv = div.querySelector('.ai-cursor');
215
- list.scrollTop = list.scrollHeight;
216
- return;
217
- }
218
- if(data.type === 'ai_chunk' && currentAiDiv) {
219
- currentAiDiv.innerText += data.chunk;
220
- list.scrollTop = list.scrollHeight;
221
- return;
222
  }
223
- if(data.type === 'ai_end' && currentAiDiv) {
224
- currentAiDiv.innerHTML = marked.parse(currentAiDiv.innerText);
225
- currentAiDiv.classList.remove('ai-cursor');
226
- currentAiDiv = null;
227
- return;
228
- }
229
-
230
- if(data.type === 'message') renderMessage(data);
231
- list.scrollTop = list.scrollHeight;
232
  }
233
 
234
  function renderMessage(msg) {
235
- const list = document.getElementById('messages');
236
  const isSelf = msg.username === username;
237
- const div = document.createElement('div');
238
- div.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter mb-1`;
239
 
240
- // Time Formatting
241
- const time = new Date(msg.timestamp * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
242
 
 
243
  let content = msg.text.replace(/@antaram.ai/gi, '<span class="text-blue-400 font-bold">@antaram.ai</span>');
244
 
245
- // File Rendering
246
- if(msg.file) {
247
- if(msg.file.file_type.startsWith('image')) {
248
- content += `<br><img src="${msg.file.file_url}" class="mt-2 rounded-lg max-h-48 border border-white/10">`;
249
- } else {
250
- content += `<br><a href="${msg.file.file_url}" target="_blank" class="text-blue-400 underline text-xs mt-1 block">📎 ${msg.file.original_name}</a>`;
251
- }
252
  }
253
 
254
- // Reply UI inside bubble
255
- let replyBlock = '';
256
- if(msg.reply_to && msg.reply_content) {
257
- replyBlock = `
258
- <div class="mb-1 bg-black/20 rounded p-1 border-l-2 border-white/30 text-[10px] opacity-80 cursor-pointer">
259
- <span class="font-bold block truncate">${msg.reply_content}</span>
260
- </div>`;
261
  }
262
-
263
- const bg = isSelf ? 'bg-[#f4f4f5] text-black' : 'bg-[#1e1e21] text-white border border-white/10';
264
-
265
- div.innerHTML = `
266
- <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'} group">
267
  ${!isSelf ? `<span class="text-[10px] text-white/40 ml-1 mb-0.5">${msg.username}</span>` : ''}
268
- <div class="${bg} px-3 py-2 rounded-[18px] ${isSelf?'rounded-tr-sm':'rounded-tl-sm'} shadow-sm text-[14px] leading-snug break-words relative cursor-pointer select-none" ondblclick="initReply('${msg.id}', '${msg.username}', '${msg.text.replace(/'/g, "\\'")}')">
269
- ${replyBlock}
 
270
  ${content}
271
- <span class="text-[9px] opacity-40 float-right mt-1 ml-2 select-none">${time}</span>
272
  </div>
273
- </div>
274
- `;
275
- list.appendChild(div);
276
  }
277
 
278
- // --- Reply System ---
279
  function initReply(id, user, text) {
280
- replyContext = { id, user, text };
281
  document.getElementById('replyPreview').classList.remove('hidden');
282
- document.getElementById('replyToUser').innerText = user;
283
- document.getElementById('replyToText').innerText = text;
284
  document.getElementById('messageInput').focus();
285
  }
286
  function cancelReply() {
287
- replyContext = null;
288
  document.getElementById('replyPreview').classList.add('hidden');
289
  }
290
 
291
- // --- Input & Send ---
292
- function sendMessage() {
293
- if(selectedFile) return uploadFile();
294
- const input = document.getElementById('messageInput');
295
- const text = input.value.trim();
296
- if(!text) return;
297
-
298
- const payload = {
299
- type: 'message',
300
- username: username,
301
- text: text,
302
- reply_to: replyContext?.id,
303
- reply_content: replyContext?.text
304
- };
305
-
306
- ws.send(JSON.stringify(payload));
307
- input.value = '';
308
- cancelReply();
309
- }
310
-
311
- async function uploadFile() {
312
- const fd = new FormData();
313
- fd.append('file', selectedFile);
314
- const res = await fetch(`/upload-file/${roomId}`, { method: 'POST', body: fd }).then(r=>r.json());
315
- if(res.success) {
316
- ws.send(JSON.stringify({
317
- type: 'message', username, text: '',
318
- file: res.file_info,
319
- reply_to: replyContext?.id
320
- }));
321
- clearFile();
322
- cancelReply();
323
- }
324
- }
325
-
326
- function handleInput(e) {
327
- if(e.key === 'Enter') return sendMessage();
328
- const val = e.target.value;
329
- const cursor = e.target.selectionStart;
330
 
331
- // Autocomplete Logic
332
- const lastWord = val.slice(0, cursor).split(' ').pop();
333
- const box = document.getElementById('suggestionBox');
 
334
 
335
- if(lastWord.startsWith('@')) {
336
  const query = lastWord.slice(1).toLowerCase();
337
  const matches = Array.from(activeUsers).filter(u => u.toLowerCase().includes(query));
338
- showSuggestions(matches, '@');
339
- } else if(lastWord.startsWith(':')) {
340
  const query = lastWord.slice(1).toLowerCase();
341
  const matches = Object.keys(EMOJIS).filter(k => k.includes(query));
342
- showSuggestions(matches, ':', true);
343
  } else {
344
- box.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  }
346
  }
347
 
348
- function showSuggestions(items, trigger, isEmoji=false) {
349
- const box = document.getElementById('suggestionBox');
350
- if(items.length === 0) { box.classList.add('hidden'); return; }
351
 
352
- box.innerHTML = items.map(item => `
353
- <div class="suggestion-item hover:bg-white/10 p-2 cursor-pointer" onclick="applySuggestion('${item}', '${trigger}', ${isEmoji})">
354
- ${isEmoji ? `<span class="text-lg">${EMOJIS[item]}</span>` : '<span class="w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center text-[10px] text-white font-bold">'+item[0]+'</span>'}
355
- <span>${item}</span>
 
 
 
356
  </div>
357
  `).join('');
358
- box.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
  function applySuggestion(val, trigger, isEmoji) {
362
  const input = document.getElementById('messageInput');
363
  const cursor = input.selectionStart;
364
  const text = input.value;
 
365
  const lastSpace = text.lastIndexOf(' ', cursor - 1);
366
- const start = lastSpace + 1; // start of the trigger word
367
 
368
- const replacement = isEmoji ? EMOJIS[val] : trigger + val + " ";
 
369
 
370
- const newText = text.substring(0, start) + replacement + text.substring(cursor);
371
- input.value = newText;
372
- document.getElementById('suggestionBox').classList.add('hidden');
373
  input.focus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
 
376
- // --- File Handling ---
377
  function handleFileSelect() {
378
  const f = document.getElementById('fileInput').files[0];
379
- if(f) { selectedFile = f; document.getElementById('fileName').innerText = f.name; document.getElementById('filePreview').classList.remove('hidden'); }
380
  }
381
  function clearFile() { selectedFile = null; document.getElementById('fileInput').value = ''; document.getElementById('filePreview').classList.add('hidden'); }
382
- function copyLink() { navigator.clipboard.writeText(window.location.href); alert('Link Copied'); }
383
- function leaveRoom() { if(ws) ws.close(); location.reload(); }
384
  </script>
385
  </body>
386
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Antaram Chat</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+
11
  <style>
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #11100f;
15
+ color: #F0EFE9;
16
+ -webkit-tap-highlight-color: transparent;
17
+ }
18
+
19
+ /* --- Antaram Animations --- */
20
+ @keyframes slideUpFade { 0% { opacity: 0; transform: translateY(30px); } 100% { opacity: 1; transform: translateY(0); } }
21
+ @keyframes scaleIn { 0% { opacity: 0; transform: scale(0.95); } 100% { opacity: 1; transform: scale(1); } }
22
+ .animate-enter { animation: slideUpFade 0.6s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
23
 
24
+ /* --- Glass & UI --- */
25
+ .glass-panel {
26
+ background: rgba(255, 255, 255, 0.03);
27
+ backdrop-filter: blur(20px);
28
+ -webkit-backdrop-filter: blur(20px);
29
+ border: 1px solid rgba(255, 255, 255, 0.08);
30
+ }
31
 
32
+ /* --- Chat Bubbles (The Antaram Look) --- */
33
+ .bubble-self { background-color: #F0EFE9; color: #1C1A19; }
34
+ .bubble-other { background-color: rgba(255, 255, 255, 0.08); color: #ffffff; border: 1px solid rgba(255, 255, 255, 0.05); }
35
+ .bubble-ai { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 1px solid rgba(59, 130, 246, 0.3); }
36
+
37
+ /* --- Autocomplete Menu --- */
38
+ #suggestions {
39
+ position: absolute;
40
+ bottom: 100%;
41
+ left: 0;
42
+ width: 100%;
43
+ max-height: 200px;
44
+ overflow-y: auto;
45
+ background: rgba(20, 20, 20, 0.95);
46
+ backdrop-filter: blur(10px);
47
+ border: 1px solid rgba(255, 255, 255, 0.1);
48
+ border-radius: 12px;
49
+ margin-bottom: 8px;
50
+ z-index: 50;
51
+ display: none;
52
+ box-shadow: 0 -10px 40px rgba(0,0,0,0.5);
53
+ }
54
+ .suggestion-item {
55
+ padding: 10px 16px;
56
+ cursor: pointer;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 10px;
60
+ border-bottom: 1px solid rgba(255,255,255,0.03);
61
+ transition: background 0.1s;
62
+ }
63
+ .suggestion-item.selected, .suggestion-item:hover { background: rgba(255, 255, 255, 0.1); }
64
 
65
+ /* --- Utils --- */
 
 
 
 
 
 
 
 
 
 
66
  .custom-scroll::-webkit-scrollbar { width: 4px; }
67
+ .custom-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
68
+ .mobile-input { padding-bottom: env(safe-area-inset-bottom, 20px); }
69
 
70
+ .ai-cursor::after { content: '▋'; animation: blink 1s infinite; color: #60a5fa; margin-left: 2px; }
71
+ @keyframes blink { 50% { opacity: 0; } }
72
  </style>
73
  </head>
74
+ <body class="relative w-full h-[100dvh] overflow-hidden flex flex-col">
75
 
76
+ <!-- DESERT BACKGROUND -->
77
  <div class="absolute inset-0 z-0 pointer-events-none">
78
+ <img src="https://images.unsplash.com/photo-1494438639946-1ebd1d20bf85?q=80&w=2068&auto=format&fit=crop"
79
+ class="w-full h-full object-cover opacity-60" alt="Desert">
80
+ <div class="absolute inset-0 bg-gradient-to-b from-[#11100f]/40 via-[#11100f]/90 to-[#11100f]"></div>
81
+ <div class="absolute inset-0 bg-black/40 backdrop-blur-[2px]"></div>
82
  </div>
83
 
84
+ <!-- TOAST -->
85
+ <div id="toast" class="fixed top-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none opacity-0 transition-opacity duration-300">
86
+ <div class="glass-panel px-6 py-2 rounded-full text-sm font-medium shadow-2xl flex items-center gap-2">
87
+ <span class="text-green-400">✓</span> <span id="toastMsg">Notification</span>
88
+ </div>
89
  </div>
90
 
91
+ <!-- VIEW 1: LOGIN -->
92
+ <div id="homeView" class="relative z-10 w-full h-full flex items-center justify-center p-4">
93
+ <div class="w-full max-w-sm animate-enter">
94
+ <div class="glass-panel rounded-[32px] p-8 shadow-2xl text-center">
95
+ <h1 class="text-4xl font-medium tracking-tight text-white mb-1">Antaram.</h1>
96
+ <p class="text-white/40 text-xs font-medium tracking-widest uppercase mb-8">Premium Chat Experience</p>
97
+
98
+ <input type="text" id="usernameInput" placeholder="Enter Identity"
99
+ class="w-full bg-black/20 text-white px-5 py-4 rounded-2xl border border-white/5 focus:outline-none focus:bg-black/40 transition-all text-center placeholder-white/20 mb-4">
100
+
101
+ <div id="roomInputContainer" class="hidden mb-4">
102
+ <input type="text" id="roomInput" placeholder="Room ID"
103
+ class="w-full bg-black/20 text-white px-5 py-4 rounded-2xl border border-white/5 focus:outline-none text-center font-mono text-blue-300 uppercase">
104
  </div>
105
+
106
+ <button onclick="createRoom()" id="createBtn" class="w-full bg-[#F0EFE9] text-black px-6 py-4 rounded-2xl font-semibold shadow-lg hover:scale-[1.02] transition-transform mb-3">Create Room</button>
107
+ <button onclick="toggleJoin()" id="joinBtn" class="w-full bg-white/5 text-white/70 px-6 py-4 rounded-2xl font-medium hover:bg-white/10 transition-colors">Join Existing</button>
108
+ <button onclick="joinRoom()" id="enterBtn" class="hidden w-full bg-[#F0EFE9] text-black px-6 py-4 rounded-2xl font-semibold hover:scale-[1.02] transition-transform">Enter Room</button>
109
  </div>
110
  </div>
111
  </div>
112
 
113
+ <!-- VIEW 2: CHAT -->
114
  <div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col">
115
 
116
  <!-- Header -->
117
+ <header class="flex-none px-4 py-4 flex justify-between items-center glass-panel border-b-0 rounded-b-[24px] mx-2 mt-2 z-20">
118
  <div class="flex items-center gap-3">
119
+ <button onclick="leaveRoom()" class="p-2 bg-white/5 rounded-full hover:bg-white/10"><svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg></button>
120
  <div>
121
+ <h1 class="text-sm font-semibold tracking-wide">Room <span id="displayRoomId" class="opacity-70 font-mono"></span></h1>
122
+ <div class="flex items-center gap-1.5"><div class="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div><span class="text-[10px] opacity-60">Live</span></div>
123
  </div>
124
  </div>
125
+ <button onclick="copyLink()" class="flex items-center gap-2 px-3 py-1.5 bg-white/5 rounded-lg text-xs font-medium hover:bg-white/10 border border-white/5">
126
+ <span>Share</span>
127
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
128
+ </button>
129
  </header>
130
 
131
  <!-- Messages -->
132
+ <main class="flex-1 w-full max-w-3xl mx-auto px-4 overflow-hidden flex flex-col relative">
133
+ <div id="messages" class="flex-1 overflow-y-auto custom-scroll space-y-4 pt-4 pb-32"></div>
134
  </main>
135
 
136
+ <!-- Fixed Input Area -->
137
+ <div class="fixed bottom-0 left-0 w-full z-30 px-3 mobile-input">
138
+ <div class="max-w-3xl mx-auto relative">
139
+
140
+ <!-- Autocomplete Menu -->
141
+ <div id="suggestions"></div>
142
+
143
+ <!-- Reply Preview -->
144
+ <div id="replyPreview" class="hidden absolute bottom-full left-0 w-full glass-panel rounded-t-xl p-3 flex justify-between items-center mb-1 border-b border-white/10 bg-[#0a0a0a]/90">
145
+ <div class="border-l-2 border-blue-400 pl-3">
146
+ <span class="text-[10px] text-blue-400 font-bold block mb-0.5">Replying to <span id="replyUser">User</span></span>
147
+ <span class="text-xs text-white/60 truncate block max-w-[250px]" id="replyText">...</span>
148
+ </div>
149
+ <button onclick="cancelReply()" class="p-2 opacity-50 hover:opacity-100">&times;</button>
150
  </div>
 
 
151
 
152
+ <!-- File Preview -->
153
+ <div id="filePreview" class="hidden absolute bottom-full left-0 w-full glass-panel rounded-t-xl p-3 flex justify-between items-center mb-1 bg-[#0a0a0a]/90">
154
+ <span class="text-xs text-white/80" id="fileName">file</span>
155
+ <button onclick="clearFile()" class="opacity-50 hover:opacity-100">&times;</button>
156
+ </div>
157
 
158
+ <!-- Input Bar -->
159
+ <div class="glass-panel rounded-[26px] p-1.5 flex items-center gap-2 shadow-2xl bg-[#000000]/60">
160
+ <input type="file" id="fileInput" class="hidden" onchange="handleFileSelect()">
161
+ <button onclick="document.getElementById('fileInput').click()" class="p-3 text-white/40 hover:text-white transition-colors rounded-full">
162
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
163
+ </button>
164
+
165
+ <input type="text" id="messageInput" placeholder="Message..." autocomplete="off"
166
+ class="flex-1 bg-transparent text-white px-2 py-3 focus:outline-none placeholder-white/30 text-[15px] min-w-0 font-light"
167
+ onkeydown="handleKeydown(event)" onkeyup="handleKeyup(event)">
168
+
169
+ <button onclick="sendMessage()" class="bg-[#F0EFE9] text-black w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 shadow-lg active:scale-95 transition-transform">
170
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
171
+ </button>
172
  </div>
 
 
173
  </div>
174
  </div>
175
  </div>
176
 
 
177
  <script>
178
+ // --- DATA & CONFIG ---
179
  const EMOJIS = {
180
+ 'heart': '❤️', 'fire': '🔥', 'smile': '😊', 'laugh': '😂', 'thumbs': '👍',
181
+ 'check': '', 'rocket': '🚀', 'eyes': '👀', 'skull': '💀', 'star': '⭐',
182
+ 'think': '🤔', 'cry': '😭', 'party': '🎉', '100': '💯', 'pray': '🙏'
183
  };
184
 
185
+ let ws = null, roomId = null, username = localStorage.getItem('antaram_user') || '';
 
 
186
  let activeUsers = new Set(['Antaram.ai']);
187
+ let replyCtx = null, selectedFile = null, currentAiEl = null;
188
+ let suggestionIndex = -1, suggestionList = [];
 
189
 
190
+ // --- INIT ---
191
+ const serverRoomId = "{{ room_id }}" !== "None" ? "{{ room_id }}" : new URLSearchParams(window.location.search).get('room');
192
+ if (username) document.getElementById('usernameInput').value = username;
193
+ if (serverRoomId) { toggleJoin(); document.getElementById('roomInput').value = serverRoomId; }
 
 
 
 
194
 
195
+ // --- VIEWS ---
196
  function toggleJoin() {
197
  document.getElementById('createBtn').classList.add('hidden');
198
+ document.getElementById('joinBtn').classList.add('hidden');
199
  document.getElementById('roomInputContainer').classList.remove('hidden');
200
+ document.getElementById('enterBtn').classList.remove('hidden');
201
+ }
202
+
203
+ function showToast(msg) {
204
+ const t = document.getElementById('toast');
205
+ document.getElementById('toastMsg').innerText = msg;
206
+ t.classList.remove('opacity-0');
207
+ setTimeout(() => t.classList.add('opacity-0'), 2500);
208
+ }
209
+
210
+ function copyLink() {
211
+ // Robust share link logic
212
+ const url = `${window.location.origin}/room/${roomId}`;
213
+ if (navigator.clipboard && window.isSecureContext) {
214
+ navigator.clipboard.writeText(url).then(() => showToast("Link Copied"));
215
+ } else {
216
+ // Fallback
217
+ alert(`Copy this link:\n${url}`);
218
+ }
219
  }
220
 
221
+ // --- CONNECTION ---
222
  function createRoom() {
223
  username = document.getElementById('usernameInput').value.trim();
224
+ if (!username) return showToast("Name required");
225
  localStorage.setItem('antaram_user', username);
226
+ fetch('/create-room', {method:'POST'}).then(r=>r.json()).then(d=>connect(d.room_id));
 
 
 
227
  }
228
 
229
  function joinRoom() {
230
  username = document.getElementById('usernameInput').value.trim();
231
  roomId = document.getElementById('roomInput').value.trim().toUpperCase();
232
+ if (!username || !roomId) return showToast("Name & ID required");
233
  localStorage.setItem('antaram_user', username);
234
  connect(roomId);
235
  }
236
 
237
  function connect(rid) {
238
  roomId = rid;
239
+ document.getElementById('displayRoomId').textContent = rid;
240
  document.getElementById('homeView').classList.add('hidden');
241
  document.getElementById('chatView').classList.remove('hidden');
242
 
243
+ const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
244
+ ws = new WebSocket(`${proto}//${window.location.host}/ws/${rid}`);
245
 
246
+ ws.onopen = () => ws.send(JSON.stringify({type:'join', username}));
247
+ ws.onmessage = e => handleData(JSON.parse(e.data));
248
+ ws.onclose = e => { if(e.code===1008) { alert("Invalid Room"); leaveRoom(); } };
249
+ }
250
+
251
+ function leaveRoom() {
252
+ if(ws) ws.close();
253
+ window.location.href = "/";
254
  }
255
 
256
+ // --- MESSAGING ---
257
  function handleData(data) {
258
+ const box = document.getElementById('messages');
259
 
260
+ if (data.type === 'history') {
261
+ data.data.forEach(renderMessage);
262
+ } else if (data.type === 'system') {
 
 
 
 
263
  if(data.users) {
264
  activeUsers = new Set(data.users);
265
  activeUsers.add('Antaram.ai');
266
  }
267
+ } else if (data.type === 'ai_start') {
268
+ const el = document.createElement('div');
269
+ el.className = 'flex w-full justify-start animate-enter';
270
+ el.innerHTML = `<div class="max-w-[85%]"><span class="text-[10px] text-blue-400 font-bold ml-1">ANTARAM AI</span><div class="bubble-ai p-3 rounded-[20px] rounded-tl-sm text-sm ai-cursor leading-relaxed text-blue-50"></div></div>`;
271
+ box.appendChild(el);
272
+ currentAiEl = el.querySelector('.ai-cursor');
273
+ } else if (data.type === 'ai_chunk' && currentAiEl) {
274
+ currentAiEl.innerText += data.chunk;
275
+ } else if (data.type === 'ai_end' && currentAiEl) {
276
+ currentAiEl.innerHTML = marked.parse(currentAiEl.innerText);
277
+ currentAiEl.classList.remove('ai-cursor');
278
+ currentAiEl = null;
279
+ } else if (data.type === 'message') {
280
+ renderMessage(data);
 
 
 
281
  }
282
+
283
+ box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
 
 
 
 
 
 
 
284
  }
285
 
286
  function renderMessage(msg) {
287
+ const box = document.getElementById('messages');
288
  const isSelf = msg.username === username;
 
 
289
 
290
+ const el = document.createElement('div');
291
+ el.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter`;
292
 
293
+ const bgClass = isSelf ? 'bubble-self' : 'bubble-other';
294
  let content = msg.text.replace(/@antaram.ai/gi, '<span class="text-blue-400 font-bold">@antaram.ai</span>');
295
 
296
+ // File
297
+ if (msg.file) {
298
+ const f = msg.file;
299
+ content += f.file_type.startsWith('image')
300
+ ? `<img src="${f.file_url}" class="mt-2 rounded-lg max-h-48 border border-black/10">`
301
+ : `<a href="${f.file_url}" target="_blank" class="block mt-1 text-xs underline opacity-80">📎 ${f.original_name}</a>`;
 
302
  }
303
 
304
+ // Reply Quote
305
+ let replyHTML = '';
306
+ if (msg.reply_to && msg.reply_content) {
307
+ replyHTML = `
308
+ <div class="mb-1 border-l-2 ${isSelf ? 'border-black/20 bg-black/5' : 'border-white/30 bg-white/5'} p-1 rounded text-[10px] opacity-80 cursor-pointer">
309
+ <span class="font-bold block">${msg.reply_content}</span>
310
+ </div>`;
311
  }
312
+
313
+ // Double Tap to Reply
314
+ el.innerHTML = `
315
+ <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'}">
 
316
  ${!isSelf ? `<span class="text-[10px] text-white/40 ml-1 mb-0.5">${msg.username}</span>` : ''}
317
+ <div class="${bgClass} px-4 py-2.5 rounded-[22px] ${isSelf?'rounded-tr-sm':'rounded-tl-sm'} shadow-sm text-[15px] leading-snug break-words cursor-pointer select-none"
318
+ ondblclick="initReply('${msg.id}', '${msg.username}', '${msg.text.replace(/'/g,"\\'")}')">
319
+ ${replyHTML}
320
  ${content}
 
321
  </div>
322
+ </div>`;
323
+ box.appendChild(el);
 
324
  }
325
 
326
+ // --- REPLY SYSTEM ---
327
  function initReply(id, user, text) {
328
+ replyCtx = { id, user, text };
329
  document.getElementById('replyPreview').classList.remove('hidden');
330
+ document.getElementById('replyUser').innerText = user;
331
+ document.getElementById('replyText').innerText = text;
332
  document.getElementById('messageInput').focus();
333
  }
334
  function cancelReply() {
335
+ replyCtx = null;
336
  document.getElementById('replyPreview').classList.add('hidden');
337
  }
338
 
339
+ // --- AUTOCOMPLETE (KEYBOARD DRIVEN) ---
340
+ function handleKeyup(e) {
341
+ if(e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown') return; // Handled in keydown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
+ const input = e.target;
344
+ const val = input.value;
345
+ const cursor = input.selectionStart;
346
+ const lastWord = val.slice(0, cursor).split(/\s/).pop();
347
 
348
+ if (lastWord.startsWith('@')) {
349
  const query = lastWord.slice(1).toLowerCase();
350
  const matches = Array.from(activeUsers).filter(u => u.toLowerCase().includes(query));
351
+ renderSuggestions(matches, '@');
352
+ } else if (lastWord.startsWith(':')) {
353
  const query = lastWord.slice(1).toLowerCase();
354
  const matches = Object.keys(EMOJIS).filter(k => k.includes(query));
355
+ renderSuggestions(matches, ':', true);
356
  } else {
357
+ hideSuggestions();
358
+ }
359
+ }
360
+
361
+ function handleKeydown(e) {
362
+ const list = document.getElementById('suggestions');
363
+ if (list.style.display === 'block') {
364
+ if (e.key === 'ArrowUp') {
365
+ e.preventDefault();
366
+ suggestionIndex = Math.max(0, suggestionIndex - 1);
367
+ highlightSuggestion();
368
+ } else if (e.key === 'ArrowDown') {
369
+ e.preventDefault();
370
+ suggestionIndex = Math.min(suggestionList.length - 1, suggestionIndex + 1);
371
+ highlightSuggestion();
372
+ } else if (e.key === 'Enter') {
373
+ e.preventDefault();
374
+ if (suggestionIndex >= 0 && suggestionList[suggestionIndex]) {
375
+ applySuggestion(suggestionList[suggestionIndex].val, suggestionList[suggestionIndex].trigger, suggestionList[suggestionIndex].isEmoji);
376
+ }
377
+ }
378
+ } else if (e.key === 'Enter') {
379
+ sendMessage();
380
  }
381
  }
382
 
383
+ function renderSuggestions(matches, trigger, isEmoji = false) {
384
+ const box = document.getElementById('suggestions');
385
+ if (matches.length === 0) return hideSuggestions();
386
 
387
+ suggestionList = matches.map(m => ({ val: m, trigger, isEmoji }));
388
+ suggestionIndex = 0;
389
+
390
+ box.innerHTML = matches.map((m, i) => `
391
+ <div class="suggestion-item ${i===0?'selected':''}" onclick="applySuggestion('${m}', '${trigger}', ${isEmoji})">
392
+ ${isEmoji ? `<span class="text-lg w-6 text-center">${EMOJIS[m]}</span>` : `<div class="w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center text-[10px] font-bold text-white">${m[0]}</div>`}
393
+ <span class="text-sm text-white/90">${m}</span>
394
  </div>
395
  `).join('');
396
+ box.style.display = 'block';
397
+ }
398
+
399
+ function highlightSuggestion() {
400
+ const items = document.querySelectorAll('.suggestion-item');
401
+ items.forEach((el, i) => {
402
+ if (i === suggestionIndex) {
403
+ el.classList.add('selected');
404
+ el.scrollIntoView({ block: 'nearest' });
405
+ } else el.classList.remove('selected');
406
+ });
407
+ }
408
+
409
+ function hideSuggestions() {
410
+ document.getElementById('suggestions').style.display = 'none';
411
+ suggestionIndex = -1;
412
  }
413
 
414
  function applySuggestion(val, trigger, isEmoji) {
415
  const input = document.getElementById('messageInput');
416
  const cursor = input.selectionStart;
417
  const text = input.value;
418
+ // Find start of word
419
  const lastSpace = text.lastIndexOf(' ', cursor - 1);
420
+ const start = lastSpace + 1;
421
 
422
+ const insert = isEmoji ? EMOJIS[val] : `${trigger}${val} `;
423
+ const after = text.substring(cursor);
424
 
425
+ input.value = text.substring(0, start) + insert + after;
 
 
426
  input.focus();
427
+ hideSuggestions();
428
+ }
429
+
430
+ // --- SENDING ---
431
+ function sendMessage() {
432
+ if (selectedFile) return uploadFile();
433
+ const txt = document.getElementById('messageInput').value.trim();
434
+ if (!txt) return;
435
+
436
+ ws.send(JSON.stringify({
437
+ type: 'message', username, text: txt,
438
+ reply_to: replyCtx?.id, reply_content: replyCtx?.text
439
+ }));
440
+
441
+ document.getElementById('messageInput').value = '';
442
+ cancelReply();
443
+ }
444
+
445
+ async function uploadFile() {
446
+ const fd = new FormData(); fd.append('file', selectedFile);
447
+ const res = await fetch(`/upload-file/${roomId}`, {method:'POST', body:fd}).then(r=>r.json());
448
+ if (res.success) {
449
+ ws.send(JSON.stringify({ type:'message', username, text:'', file:res.file_info, reply_to: replyCtx?.id }));
450
+ clearFile(); cancelReply();
451
+ }
452
  }
453
 
 
454
  function handleFileSelect() {
455
  const f = document.getElementById('fileInput').files[0];
456
+ if(f) { selectedFile = f; document.getElementById('fileName').textContent = f.name; document.getElementById('filePreview').classList.remove('hidden'); }
457
  }
458
  function clearFile() { selectedFile = null; document.getElementById('fileInput').value = ''; document.getElementById('filePreview').classList.add('hidden'); }
 
 
459
  </script>
460
  </body>
461
  </html>