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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +285 -352
templates/index.html CHANGED
@@ -3,451 +3,384 @@
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 AI</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
-
10
- <!-- Marked.js for AI Markdown rendering -->
11
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
 
13
  <style>
14
- body {
15
- font-family: 'Inter', sans-serif;
16
- background-color: #11100f;
17
- color: #F0EFE9;
18
- -webkit-tap-highlight-color: transparent;
19
- }
20
-
21
- /* --- Animations --- */
22
- @keyframes slideUpFade {
23
- 0% { opacity: 0; transform: translateY(20px); }
24
- 100% { opacity: 1; transform: translateY(0); }
25
- }
26
- @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
27
- @keyframes pulse-ring {
28
- 0% { transform: scale(0.8); opacity: 0.5; }
29
- 100% { transform: scale(1.2); opacity: 0; }
30
- }
31
-
32
- .animate-enter { animation: slideUpFade 0.5s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
33
- .fade-in { animation: fadeIn 0.4s ease-out forwards; }
34
-
35
- /* --- Glass & UI --- */
36
- .glass-panel {
37
- background: rgba(255, 255, 255, 0.03);
38
- backdrop-filter: blur(16px);
39
- -webkit-backdrop-filter: blur(16px);
40
- border: 1px solid rgba(255, 255, 255, 0.08);
41
- }
42
 
43
- /* AI Message Specifics */
44
- .ai-bubble {
45
- background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
46
- border: 1px solid rgba(59, 130, 246, 0.2);
47
- }
48
- .ai-cursor::after {
49
- content: '▋';
50
- display: inline-block;
51
- vertical-align: bottom;
52
- animation: blink 1s step-start infinite;
53
- color: #60a5fa;
54
- font-size: 0.8em;
55
- margin-left: 2px;
56
- }
57
  @keyframes blink { 50% { opacity: 0; } }
58
 
 
 
 
 
 
59
  /* Scrollbar */
60
  .custom-scroll::-webkit-scrollbar { width: 4px; }
61
- .custom-scroll::-webkit-scrollbar-track { background: transparent; }
62
- .custom-scroll::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.15); border-radius: 20px; }
63
 
64
- /* Mobile Input fix */
65
- .mobile-input-bar { padding-bottom: env(safe-area-inset-bottom, 20px); }
66
  </style>
67
  </head>
68
- <body class="relative w-full h-[100dvh] overflow-hidden flex flex-col">
69
 
70
  <!-- Background -->
71
- <div class="absolute inset-0 z-0 pointer-events-none select-none">
72
- <img src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop"
73
- class="w-full h-full object-cover opacity-40" alt="bg">
74
- <div class="absolute inset-0 bg-gradient-to-b from-[#11100f]/50 via-[#11100f]/95 to-[#11100f]"></div>
75
- <div class="absolute inset-0 bg-black/40 backdrop-blur-[1px]"></div>
76
  </div>
77
 
78
- <!-- Toast Notification -->
79
- <div id="toast" class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 hidden transition-all duration-300">
80
- <div class="glass-panel px-6 py-3 rounded-full shadow-2xl flex items-center gap-3">
81
- <span class="text-sm font-medium tracking-wide" id="toastMsg">Notification</span>
82
- </div>
83
  </div>
84
 
85
- <!-- VIEW 1: LOGIN / HOME -->
86
- <div id="homeView" class="relative z-10 w-full h-full flex items-center justify-center p-4">
87
- <div class="w-full max-w-sm animate-enter">
88
- <div class="glass-panel rounded-[32px] p-6 md:p-8 shadow-2xl relative overflow-hidden">
89
- <!-- Decorative Orb -->
90
- <div class="absolute -top-10 -right-10 w-32 h-32 bg-blue-500/20 rounded-full blur-3xl"></div>
91
-
92
- <div class="text-center mb-8 relative">
93
- <h1 class="text-4xl font-medium tracking-tight text-white mb-1">Antaram.ai</h1>
94
- <p class="text-white/40 text-xs font-medium tracking-widest uppercase">Collaborative Intelligence</p>
95
- </div>
96
-
97
- <!-- Input Group -->
98
- <div class="space-y-5 mb-8">
99
- <div>
100
- <label class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2 ml-2">Identity</label>
101
- <input type="text" id="usernameInput" placeholder="Your Name" maxlength="25"
102
- class="w-full bg-black/30 text-white px-5 py-4 rounded-2xl border border-white/10 focus:border-blue-500/50 focus:bg-black/50 focus:outline-none transition-all placeholder-white/20 text-sm">
103
- </div>
104
-
105
- <!-- Dynamic Room Input (Auto-filled if URL has ID) -->
106
- <div id="roomInputContainer" class="hidden">
107
- <label class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2 ml-2">Room ID</label>
108
- <input type="text" id="roomInput" placeholder="Room Code"
109
- class="w-full bg-black/30 text-white px-5 py-4 rounded-2xl border border-white/10 focus:outline-none text-sm font-mono tracking-wider text-blue-300">
110
- </div>
111
- </div>
112
-
113
- <!-- Buttons -->
114
- <div id="actionButtons">
115
- <button onclick="createRoom()"
116
- class="group w-full bg-[#F0EFE9] text-[#1C1A19] hover:bg-white px-6 py-4 rounded-2xl flex items-center justify-center gap-3 font-semibold text-base transition-all shadow-lg active:scale-[0.98] mb-4">
117
- <span>Create New Space</span>
118
- </button>
119
-
120
- <button onclick="toggleJoinInput()"
121
- class="w-full bg-white/5 hover:bg-white/10 text-white px-6 py-4 rounded-2xl font-medium text-sm transition-colors border border-white/5">
122
- Join Existing Room
123
- </button>
124
  </div>
125
-
126
- <!-- Join Confirm Button (Hidden initially) -->
127
- <button id="joinConfirmBtn" onclick="joinRoom()" class="hidden w-full bg-blue-600 hover:bg-blue-500 text-white px-6 py-4 rounded-2xl font-semibold transition-all shadow-lg shadow-blue-900/20">
128
- Join Room
129
- </button>
130
  </div>
131
  </div>
132
  </div>
133
 
134
- <!-- VIEW 2: CHAT INTERFACE -->
135
- <div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col fade-in">
136
 
137
  <!-- Header -->
138
- <header class="flex-none pt-4 pb-3 px-4 flex justify-between items-center glass-panel border-b-0 rounded-b-[24px] mx-2 mt-2 z-20">
139
  <div class="flex items-center gap-3">
140
- <button onclick="leaveRoom()" class="bg-white/5 p-2 rounded-full active:bg-white/10 hover:bg-white/10 transition-colors">
141
- <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"></path></svg>
142
- </button>
143
  <div>
144
- <div class="flex items-center gap-2">
145
- <span class="text-xs font-bold text-white/40 uppercase tracking-wider">Room</span>
146
- <h1 id="displayRoomId" class="text-sm font-bold text-white font-mono tracking-wide">...</h1>
147
- </div>
148
- <div class="flex items-center gap-2 mt-0.5">
149
- <span class="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]"></span>
150
- <p class="text-[10px] text-white/60"><span id="userCount">1</span> Active</p>
151
- </div>
152
  </div>
153
  </div>
154
-
155
- <button onclick="copyLink()" class="bg-blue-500/10 text-blue-400 px-3 py-1.5 rounded-lg text-xs font-medium border border-blue-500/20 hover:bg-blue-500/20 transition-colors flex items-center gap-2">
156
- <span>Share</span>
157
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
158
- </button>
159
  </header>
160
 
161
- <!-- Chat Area -->
162
- <main class="flex-1 w-full max-w-3xl mx-auto px-4 overflow-hidden flex flex-col relative">
163
- <div id="messages" class="flex-1 overflow-y-auto custom-scroll space-y-4 pt-4 pb-[110px]">
164
- <!-- Chat Messages Injected Here -->
165
- </div>
166
  </main>
167
 
168
- <!-- Input Area -->
169
- <div class="fixed bottom-0 left-0 w-full z-30 px-3 pb-3 mobile-input-bar transition-transform duration-300">
 
 
 
 
 
 
 
 
 
 
170
  <!-- File Preview -->
171
- <div id="filePreview" class="mx-auto max-w-3xl mb-2 glass-panel px-4 py-2 rounded-xl flex items-center justify-between hidden animate-enter">
172
- <span class="text-xs text-blue-300 truncate max-w-[200px]" id="fileName">file</span>
173
- <button onclick="clearFile()" class="text-white/40 hover:text-white p-1">&times;</button>
174
  </div>
175
 
176
- <!-- Main Input -->
177
- <div class="max-w-3xl mx-auto glass-panel rounded-[26px] p-1.5 flex items-center gap-2 shadow-2xl bg-[#0a0a0a]/60 backdrop-blur-xl border-white/10">
178
  <input type="file" id="fileInput" class="hidden" onchange="handleFileSelect()">
179
- <button onclick="document.getElementById('fileInput').click()"
180
- class="p-3 text-white/40 hover:text-white transition-colors rounded-full">
181
- <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>
182
- </button>
183
-
184
- <input type="text" id="messageInput" placeholder="Type @antaram.ai to chat with AI..." autocomplete="off"
185
- class="flex-1 bg-transparent text-white px-2 py-3 focus:outline-none placeholder-white/20 text-[15px] min-w-0"
186
- onkeypress="if(event.key === 'Enter') sendMessage()">
187
 
188
- <button onclick="sendMessage()"
189
- class="bg-[#F0EFE9] hover:bg-white text-black w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 shadow-lg active:scale-90 transition-transform">
190
- <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>
191
- </button>
 
 
 
192
  </div>
193
  </div>
194
  </div>
195
 
196
- <!-- JAVASCRIPT LOGIC -->
197
  <script>
198
- // --- State Variables ---
 
 
 
 
 
 
199
  let ws = null;
200
- let currentRoomId = null;
201
- let username = '';
 
 
202
  let selectedFile = null;
203
-
204
- // Use a variable passed from Python if using the route /room/{id}, otherwise null
205
- // Using Jinja2 syntax to inject variable safely
206
- const serverRoomId = "{{ room_id }}" !== "None" ? "{{ room_id }}" : null;
207
-
208
- // --- Initialization ---
209
- window.addEventListener('DOMContentLoaded', () => {
210
- // 1. Recover Username
211
- const storedName = localStorage.getItem('antaram_username');
212
- if (storedName) {
213
- document.getElementById('usernameInput').value = storedName;
214
- }
215
 
216
- // 2. Handle Dynamic Route or URL Param
217
- const urlParams = new URLSearchParams(window.location.search);
218
- const paramRoom = urlParams.get('room');
219
-
220
- // Priority: Server Route > URL Param
221
- const targetRoom = serverRoomId || paramRoom;
222
-
223
- if (targetRoom) {
224
- // UI State: Show Join inputs immediately
225
- toggleJoinInput();
226
- document.getElementById('roomInput').value = targetRoom;
227
- // If we have a name, maybe auto-focus the join button
228
- if (storedName) {
229
- document.getElementById('joinConfirmBtn').focus();
230
- }
231
- }
232
- });
233
 
234
- // --- View Transitions ---
235
- function toggleJoinInput() {
236
- document.getElementById('actionButtons').classList.add('hidden');
 
237
  document.getElementById('roomInputContainer').classList.remove('hidden');
238
  document.getElementById('joinConfirmBtn').classList.remove('hidden');
239
- document.getElementById('roomInput').focus();
240
- }
241
-
242
- function showToast(msg) {
243
- const t = document.getElementById('toast');
244
- document.getElementById('toastMsg').innerText = msg;
245
- t.classList.remove('hidden');
246
- setTimeout(() => t.classList.add('hidden'), 3000);
247
- }
248
-
249
- function copyLink() {
250
- // Construct cleaner URL using the dynamic route format if possible
251
- const url = `${window.location.origin}/room/${currentRoomId}`;
252
- navigator.clipboard.writeText(url).then(() => showToast('Link Copied to Clipboard'));
253
- }
254
-
255
- // --- Actions ---
256
-
257
- function saveUsername() {
258
- const val = document.getElementById('usernameInput').value.trim();
259
- if (!val) {
260
- showToast("Please enter your name");
261
- document.getElementById('usernameInput').focus();
262
- return null;
263
- }
264
- localStorage.setItem('antaram_username', val);
265
- username = val;
266
- return val;
267
  }
268
 
269
- async function createRoom() {
270
- if (!saveUsername()) return;
271
-
272
- const btn = document.querySelector('button[onclick="createRoom()"]');
273
- const originalText = btn.innerHTML;
274
- btn.innerHTML = `<span class="animate-pulse">Creating Space...</span>`;
275
 
276
- try {
277
- const res = await fetch('/create-room', { method: 'POST' });
278
- const data = await res.json();
279
- if (data.success) connectToRoom(data.room_id);
280
- } catch (e) {
281
- showToast("Server Error");
282
- btn.innerHTML = originalText;
283
- }
284
  }
285
 
286
  function joinRoom() {
287
- if (!saveUsername()) return;
288
- const rid = document.getElementById('roomInput').value.trim().toUpperCase();
289
- if (!rid) return showToast("Enter Room ID");
290
- connectToRoom(rid);
291
- }
292
-
293
- function leaveRoom() {
294
- if (ws) ws.close();
295
- document.getElementById('chatView').classList.add('hidden');
296
- document.getElementById('homeView').classList.remove('hidden');
297
- // Reset URL to clean root
298
- window.history.pushState({}, '', '/');
299
  }
300
 
301
- // --- WebSocket Logic ---
302
-
303
- function connectToRoom(rid) {
304
- currentRoomId = rid;
305
- document.getElementById('displayRoomId').textContent = rid;
306
-
307
- // Switch Views
308
  document.getElementById('homeView').classList.add('hidden');
309
  document.getElementById('chatView').classList.remove('hidden');
310
 
311
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
312
  ws = new WebSocket(`${protocol}//${window.location.host}/ws/${rid}`);
313
-
314
- ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
315
- ws.onclose = (e) => {
316
- if (e.code === 1008) {
317
- alert("Room does not exist or expired.");
318
- leaveRoom();
319
- }
320
- };
321
  }
322
 
323
- // --- AI Response State ---
324
- let currentAiMsgDiv = null;
325
-
326
- function handleMessage(data) {
327
- const container = document.getElementById('messages');
328
-
329
- // 1. AI Start (Create bubble)
330
- if (data.type === 'ai_start') {
331
- const wrapper = document.createElement('div');
332
- wrapper.className = 'flex w-full justify-start animate-enter';
333
- wrapper.innerHTML = `
334
- <div class="max-w-[90%] flex flex-col items-start">
335
- <span class="text-[10px] text-blue-400 mb-1 ml-1 font-bold">ANTARAM AI</span>
336
- <div class="ai-bubble px-4 py-3 rounded-[18px] rounded-tl-sm text-[15px] leading-relaxed text-blue-50 shadow-lg ai-cursor" id="ai-streaming-${Date.now()}">
337
- </div>
338
- </div>`;
339
- container.appendChild(wrapper);
340
- // Save reference to the inner content div
341
- currentAiMsgDiv = wrapper.querySelector('.ai-cursor');
342
- container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
343
  return;
344
  }
345
 
346
- // 2. AI Chunk (Append text)
347
- if (data.type === 'ai_chunk' && currentAiMsgDiv) {
348
- // Append text node safely
349
- currentAiMsgDiv.innerHTML += data.chunk; // Note: For robust markdown, we'd buffer and re-render marked
350
- container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
 
351
  return;
352
  }
353
 
354
- // 3. AI End (Remove cursor, Render Markdown)
355
- if (data.type === 'ai_end' && currentAiMsgDiv) {
356
- currentAiMsgDiv.classList.remove('ai-cursor');
357
- // Parse Markdown
358
- currentAiMsgDiv.innerHTML = marked.parse(currentAiMsgDiv.innerText);
359
- currentAiMsgDiv = null;
 
 
 
 
 
 
 
 
 
 
 
 
360
  return;
361
  }
362
 
363
- // 4. Standard User Message
364
- if (data.type === 'message') {
365
- const el = document.createElement('div');
366
- const isSelf = data.username === username;
367
-
368
- el.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter`;
369
- const bg = isSelf ? 'bg-[#F0EFE9] text-black' : 'glass-panel text-white';
370
- const sender = isSelf ? '' : `<span class="text-[10px] text-white/40 mb-1 ml-1">${data.username}</span>`;
371
-
372
- let content = data.text;
373
- // Highlight @antaram.ai
374
- if (content.includes('@antaram.ai')) {
375
- content = content.replace(/@antaram.ai/g, '<span class="text-blue-400 font-bold">@antaram.ai</span>');
376
- }
377
 
378
- // If file
379
- if (data.file) {
380
- content = renderFile(data.file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
-
383
- el.innerHTML = `
384
- <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'}">
385
- ${sender}
386
- <div class="${bg} px-4 py-2.5 rounded-[18px] ${isSelf ? 'rounded-tr-sm' : 'rounded-tl-sm'} shadow-sm text-[15px] leading-snug break-words">
387
- ${content}
388
- </div>
389
- </div>`;
390
-
391
- container.appendChild(el);
392
  }
393
 
394
- // 5. System Messages
395
- if (data.type === 'system') {
396
- const el = document.createElement('div');
397
- el.className = 'flex justify-center my-4';
398
- el.innerHTML = `<span class="text-[10px] uppercase font-bold tracking-widest text-white/20 bg-white/5 px-2 py-1 rounded-md border border-white/5">${data.message}</span>`;
399
- container.appendChild(el);
400
- if(data.userCount) document.getElementById('userCount').textContent = data.userCount;
401
  }
402
 
403
- container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
- function renderFile(f) {
407
- if (f.file_type.startsWith('image/')) {
408
- return `<img src="${f.file_url}" class="rounded-lg mt-1 max-h-48 border border-black/10">`;
409
- }
410
- return `<a href="${f.file_url}" target="_blank" class="flex items-center gap-2 underline text-sm">📎 ${f.original_name}</a>`;
 
 
 
 
 
 
411
  }
412
 
413
- // --- Input Logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- function handleFileSelect() {
416
- const f = document.getElementById('fileInput').files[0];
417
- if (f) {
418
- selectedFile = f;
419
- document.getElementById('fileName').textContent = f.name;
420
- document.getElementById('filePreview').classList.remove('hidden');
 
 
 
 
 
 
421
  }
422
  }
423
- function clearFile() {
424
- selectedFile = null;
425
- document.getElementById('fileInput').value = "";
426
- document.getElementById('filePreview').classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  }
428
 
429
- async function sendMessage() {
430
- if (selectedFile) {
431
- const fd = new FormData();
432
- fd.append('file', selectedFile);
433
- try {
434
- const res = await fetch(`/upload-file/${currentRoomId}`, { method: 'POST', body: fd });
435
- const dat = await res.json();
436
- if(dat.success) {
437
- ws.send(JSON.stringify({ type:'message', username, file: dat.file_info }));
438
- clearFile();
439
- }
440
- } catch(e) { showToast("Upload Failed"); }
441
- return;
442
- }
443
 
 
444
  const input = document.getElementById('messageInput');
445
- const txt = input.value.trim();
446
- if (!txt) return;
 
 
 
 
 
 
 
 
 
 
447
 
448
- ws.send(JSON.stringify({ type:'message', username, text: txt }));
449
- input.value = '';
 
 
450
  }
 
 
 
451
  </script>
452
  </body>
453
  </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 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>