Antaram commited on
Commit
3593ea7
·
verified ·
1 Parent(s): e8cfa55

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +306 -270
templates/index.html CHANGED
@@ -2,12 +2,14 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <!-- Viewport tweak for mobile inputs -->
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7
- <title>Antaram Chat</title>
8
  <script src="https://cdn.tailwindcss.com"></script>
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
 
 
 
 
11
  <style>
12
  body {
13
  font-family: 'Inter', sans-serif;
@@ -21,20 +23,16 @@
21
  0% { opacity: 0; transform: translateY(20px); }
22
  100% { opacity: 1; transform: translateY(0); }
23
  }
24
- @keyframes fadeIn {
25
- 0% { opacity: 0; }
26
- 100% { opacity: 1; }
27
- }
28
- @keyframes toastSlide {
29
- 0% { transform: translateY(-100%); opacity: 0; }
30
- 100% { transform: translateY(0); opacity: 1; }
31
  }
32
 
33
  .animate-enter { animation: slideUpFade 0.5s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
34
  .fade-in { animation: fadeIn 0.4s ease-out forwards; }
35
- .toast-enter { animation: toastSlide 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
36
 
37
- /* --- UI Polish --- */
38
  .glass-panel {
39
  background: rgba(255, 255, 255, 0.03);
40
  backdrop-filter: blur(16px);
@@ -42,376 +40,414 @@
42
  border: 1px solid rgba(255, 255, 255, 0.08);
43
  }
44
 
45
- .btn-arrow { transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1); }
46
- .group:hover .btn-arrow { transform: translate(2px, -2px); }
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- /* Custom Scrollbar */
49
  .custom-scroll::-webkit-scrollbar { width: 4px; }
50
  .custom-scroll::-webkit-scrollbar-track { background: transparent; }
51
- .custom-scroll::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.1); border-radius: 20px; }
52
-
53
- /* Mobile specific fixes */
54
- .mobile-input-bar {
55
- padding-bottom: env(safe-area-inset-bottom, 20px);
56
- }
57
  </style>
58
  </head>
59
  <body class="relative w-full h-[100dvh] overflow-hidden flex flex-col">
60
 
61
- <!-- BACKGROUND LAYER -->
62
- <div class="absolute inset-0 z-0 pointer-events-none">
63
- <img src="https://images.unsplash.com/photo-1494438639946-1ebd1d20bf85?q=80&w=2068&auto=format&fit=crop"
64
- class="w-full h-full object-cover opacity-60" alt="bg">
65
- <div class="absolute inset-0 bg-gradient-to-b from-[#11100f]/40 via-[#11100f]/90 to-[#11100f]"></div>
66
- <div class="absolute inset-0 bg-black/40 backdrop-blur-[2px]"></div>
67
  </div>
68
 
69
- <!-- TOAST NOTIFICATION -->
70
- <div id="toast" class="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 hidden">
71
- <div class="glass-panel px-6 py-3 rounded-full shadow-2xl flex items-center gap-3 toast-enter">
72
- <div class="bg-green-500/20 text-green-400 p-1 rounded-full">
73
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg>
74
- </div>
75
- <span class="text-sm font-medium tracking-wide" id="toastMsg">Link Copied</span>
76
  </div>
77
  </div>
78
 
79
- <!-- HOME VIEW -->
80
  <div id="homeView" class="relative z-10 w-full h-full flex items-center justify-center p-4">
81
  <div class="w-full max-w-sm animate-enter">
82
-
83
- <div class="glass-panel rounded-[32px] p-6 md:p-8 shadow-2xl">
84
- <div class="text-center mb-8">
85
- <h1 class="text-4xl font-medium tracking-tight text-white mb-1">Connect.</h1>
86
- <p class="text-white/40 text-xs font-medium tracking-widest uppercase">Secure Realtime Chat</p>
 
 
87
  </div>
88
 
89
- <!-- Inputs -->
90
- <div class="space-y-4 mb-6">
91
  <div>
92
- <label class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-1 ml-2">Identity</label>
93
- <input type="text" id="usernameInputHome" placeholder="Display Name" maxlength="20"
94
- class="w-full bg-black/20 text-white px-5 py-4 rounded-2xl border border-white/5 focus:border-white/30 focus:bg-black/40 focus:outline-none transition-all placeholder-white/20 text-sm">
 
 
 
 
 
 
 
95
  </div>
96
  </div>
97
 
98
- <!-- Actions -->
99
- <button onclick="createRoom()"
100
- 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 transform active:scale-[0.98] shadow-lg mb-6">
101
- <span id="createBtnText">Start New Room</span>
102
- <div class="btn-arrow"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M7 7h10v10"/></svg></div>
103
- </button>
104
-
105
- <div class="relative flex py-2 items-center">
106
- <div class="flex-grow border-t border-white/10"></div>
107
- <span class="flex-shrink-0 mx-4 text-white/30 text-[10px] uppercase font-bold tracking-wider">Or Join</span>
108
- <div class="flex-grow border-t border-white/10"></div>
109
- </div>
110
-
111
- <div class="flex gap-2 mt-4">
112
- <input type="text" id="joinRoomId" placeholder="Room ID"
113
- class="flex-1 bg-black/20 text-white px-5 py-3 rounded-2xl border border-white/5 focus:border-white/30 focus:outline-none text-sm placeholder-white/20 uppercase font-mono tracking-wider">
114
- <button onclick="joinRoom()" class="bg-white/10 hover:bg-white/20 active:bg-white/30 text-white font-medium px-5 py-3 rounded-2xl border border-white/5 transition-colors">
115
- Join
116
  </button>
117
  </div>
 
 
 
 
 
118
  </div>
119
  </div>
120
  </div>
121
 
122
- <!-- CHAT VIEW -->
123
  <div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col fade-in">
124
 
125
- <!-- Chat Header -->
126
  <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">
127
  <div class="flex items-center gap-3">
128
- <div onclick="leaveRoom()" class="bg-white/5 p-2 rounded-full active:bg-white/10 cursor-pointer">
129
  <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>
130
- </div>
131
- <div class="flex flex-col">
132
  <div class="flex items-center gap-2">
133
- <h1 class="text-sm font-semibold text-white tracking-wide">Room <span id="roomIdDisplay" class="font-mono text-white/70"></span></h1>
134
- <!-- Share Button -->
135
- <button onclick="copyShareLink()" class="text-white/40 hover:text-green-400 transition-colors">
136
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
137
- </button>
 
138
  </div>
139
- <p class="text-[10px] text-green-400/80 font-medium flex items-center gap-1">
140
- <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>
141
- <span id="userCount">1</span> Online
142
- </p>
143
  </div>
144
  </div>
 
 
 
 
 
145
  </header>
146
 
147
- <!-- Messages Area -->
148
  <main class="flex-1 w-full max-w-3xl mx-auto px-4 overflow-hidden flex flex-col relative">
149
- <div id="messages" class="flex-1 overflow-y-auto custom-scroll space-y-3 pt-4 pb-[100px]">
150
- <!-- Messages go here -->
151
  </div>
152
  </main>
153
 
154
- <!-- Fixed Bottom Input Area -->
155
- <div class="fixed bottom-0 left-0 w-full z-30 px-3 pb-3 mobile-input-bar">
156
-
157
- <!-- Floating File Info -->
158
- <div id="selectedFileInfo" class="mx-auto max-w-3xl mb-2 glass-panel px-4 py-2 rounded-xl flex items-center justify-between hidden animate-enter">
159
- <span class="text-xs text-white/80 truncate max-w-[200px]" id="fileNameDisplay">file.name</span>
160
- <button onclick="clearFile()" class="text-white/40 p-1"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
161
  </div>
162
 
163
- <!-- Input Bar -->
164
- <div class="max-w-3xl mx-auto glass-panel rounded-[26px] p-1.5 flex items-center gap-2 shadow-2xl bg-black/40">
165
  <input type="file" id="fileInput" class="hidden" onchange="handleFileSelect()">
166
  <button onclick="document.getElementById('fileInput').click()"
167
- class="p-3 text-white/50 hover:text-white active:bg-white/10 rounded-full transition-colors flex-shrink-0">
168
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
169
  </button>
170
 
171
- <input type="text" id="messageInput" placeholder="Message..." autocomplete="off"
172
- class="flex-1 bg-transparent text-white px-2 py-3 focus:outline-none placeholder-white/30 text-[15px] font-light min-w-0"
173
  onkeypress="if(event.key === 'Enter') sendMessage()">
174
 
175
  <button onclick="sendMessage()"
176
- class="bg-[#F0EFE9] text-black w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 shadow-lg active:scale-90 transition-transform">
177
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
178
  </button>
179
  </div>
180
  </div>
181
  </div>
182
 
 
183
  <script>
 
184
  let ws = null;
185
  let currentRoomId = null;
186
- let currentUsername = 'Guest';
187
  let selectedFile = null;
 
 
 
 
188
 
189
- // 1. URL Parameter Check (Share Link Logic)
190
  window.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
191
  const urlParams = new URLSearchParams(window.location.search);
192
- const roomParam = urlParams.get('room');
193
- if (roomParam) {
194
- document.getElementById('joinRoomId').value = roomParam;
195
- // Optional: Highlight the input or focus user name
196
- document.getElementById('usernameInputHome').focus();
 
 
 
 
 
 
 
 
197
  }
198
  });
199
 
200
- // 2. Copy Link Logic
201
- function copyShareLink() {
202
- if (!currentRoomId) return;
203
- // Constructs: https://antaram-chatbot.hf.space/?room=ABCD
204
- const shareUrl = `${window.location.origin}/?room=${currentRoomId}`;
205
-
206
- navigator.clipboard.writeText(shareUrl).then(() => {
207
- showToast("Link Copied!");
208
- }).catch(err => {
209
- showToast("Failed to copy");
210
- });
211
  }
212
 
213
  function showToast(msg) {
214
- const toast = document.getElementById('toast');
215
  document.getElementById('toastMsg').innerText = msg;
216
- toast.classList.remove('hidden');
217
- setTimeout(() => {
218
- toast.classList.add('hidden');
219
- }, 2500);
220
  }
221
 
222
- function showHome() {
223
- document.getElementById('homeView').classList.remove('hidden');
224
- document.getElementById('chatView').classList.add('hidden');
225
- if (ws) { ws.close(); ws = null; }
226
- currentRoomId = null;
227
- clearFile();
228
- document.getElementById('messages').innerHTML = '';
229
- // Clear URL param on leave so reload doesn't auto-join
230
- window.history.pushState({}, document.title, window.location.pathname);
231
  }
232
 
233
- function showChat(roomId) {
234
- document.getElementById('homeView').classList.add('hidden');
235
- document.getElementById('chatView').classList.remove('hidden');
236
- document.getElementById('roomIdDisplay').textContent = roomId;
237
- currentRoomId = roomId;
238
- document.getElementById('messageInput').focus();
239
- }
240
 
241
- function getUsername() {
242
- return document.getElementById('usernameInputHome').value.trim();
 
 
 
 
 
 
 
 
243
  }
244
 
245
  async function createRoom() {
246
- currentUsername = getUsername();
247
- if (!currentUsername) {
248
- document.getElementById('usernameInputHome').focus();
249
- showToast("Enter a name first");
250
- return;
251
- }
252
-
253
- const btnText = document.getElementById('createBtnText');
254
- btnText.textContent = "Creating...";
255
 
256
  try {
257
- const response = await fetch('/create-room', { method: 'POST' });
258
- const data = await response.json();
259
- if (data.success) joinChatRoom(data.room_id);
260
- } catch (error) {
261
- alert('Server connection failed.');
262
- } finally {
263
- btnText.textContent = "Start New Room";
264
  }
265
  }
266
 
267
  function joinRoom() {
268
- currentUsername = getUsername();
269
- if (!currentUsername) {
270
- document.getElementById('usernameInputHome').focus();
271
- showToast("Enter a name first");
272
- return;
273
- }
274
- const roomId = document.getElementById('joinRoomId').value.trim().toUpperCase();
275
- if (!roomId) {
276
- showToast("Enter Room ID");
277
- return;
278
- }
279
- joinChatRoom(roomId);
280
  }
281
 
282
- function joinChatRoom(roomId) {
283
- showChat(roomId);
284
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
285
- const wsUrl = `${protocol}//${window.location.host}/ws/${roomId}`;
 
 
 
 
 
 
 
 
 
286
 
287
- ws = new WebSocket(wsUrl);
 
 
288
 
289
- ws.onopen = () => console.log("Connected");
290
- ws.onmessage = (event) => handleIncomingData(JSON.parse(event.data));
291
- ws.onclose = (event) => {
292
- if (event.code === 1008) {
293
- alert('Room not found or invalid.');
294
- showHome();
 
 
295
  }
296
  };
297
  }
298
 
299
- function handleIncomingData(data) {
300
- if (data.type === 'system') {
301
- addMessage(data.message, 'system');
302
- if(data.userCount !== undefined) document.getElementById('userCount').textContent = data.userCount;
303
- } else if (data.type === 'message') {
304
- if (data.file) addFileMessage(data.username, data.file);
305
- else addMessage(data.text, 'message', data.username);
306
- }
307
- }
308
 
309
- function handleFileSelect() {
310
- const fileInput = document.getElementById('fileInput');
311
- if (fileInput.files.length > 0) {
312
- selectedFile = fileInput.files[0];
313
- document.getElementById('fileNameDisplay').textContent = `📎 ${selectedFile.name}`;
314
- document.getElementById('selectedFileInfo').classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
315
  }
316
- }
317
 
318
- function clearFile() {
319
- selectedFile = null;
320
- document.getElementById('fileInput').value = '';
321
- document.getElementById('selectedFileInfo').classList.add('hidden');
322
- }
 
 
323
 
324
- async function sendFile() {
325
- if (!selectedFile) return;
326
- const formData = new FormData();
327
- formData.append('file', selectedFile);
328
-
329
- // Add temp loading msg
330
- addMessage(`Uploading ${selectedFile.name}...`, 'system');
331
-
332
- try {
333
- const response = await fetch(`/upload-file/${currentRoomId}`, { method: 'POST', body: formData });
334
- const data = await response.json();
335
- if (data.success) {
336
- ws.send(JSON.stringify({ type: 'message', username: currentUsername, file: data.file_info }));
337
- clearFile();
338
- }
339
- } catch (e) { showToast("Upload failed"); }
340
- }
341
 
342
- function sendMessage() {
343
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
344
- if (selectedFile) { sendFile(); return; }
 
 
 
 
 
345
 
346
- const input = document.getElementById('messageInput');
347
- const text = input.value.trim();
348
- if (!text) return;
 
 
349
 
350
- ws.send(JSON.stringify({ type: 'message', username: currentUsername, text: text }));
351
- input.value = '';
352
- }
 
353
 
354
- function addMessage(text, type, username = '') {
355
- const container = document.getElementById('messages');
356
- const el = document.createElement('div');
357
- const isSelf = username === currentUsername;
358
-
359
- if (type === 'system') {
360
- el.className = 'flex justify-center my-3';
361
- el.innerHTML = `<span class="text-[10px] uppercase font-bold tracking-widest text-white/30 bg-white/5 px-2 py-1 rounded-md">${text}</span>`;
362
- } else {
363
- el.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter`;
364
- const bg = isSelf ? 'bg-[#F0EFE9] text-black' : 'glass-panel text-white';
365
-
366
  el.innerHTML = `
367
  <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'}">
368
- ${!isSelf ? `<span class="text-[10px] text-white/50 mb-1 ml-1">${username}</span>` : ''}
369
  <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">
370
- ${text}
371
  </div>
372
  </div>`;
 
 
 
 
 
 
 
 
 
 
 
373
  }
374
- container.appendChild(el);
375
  container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
376
  }
377
 
378
- function addFileMessage(username, fileInfo) {
379
- const container = document.getElementById('messages');
380
- const el = document.createElement('div');
381
- const isSelf = username === currentUsername;
382
-
383
- el.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter`;
384
- const bg = isSelf ? 'bg-[#F0EFE9] text-black' : 'glass-panel text-white';
385
-
386
- let content = fileInfo.file_type.startsWith('image/')
387
- ? `<img src="${fileInfo.file_url}" class="rounded-xl mt-1 max-h-48 object-cover"/>`
388
- : `<a href="${fileInfo.file_url}" target="_blank" class="flex items-center gap-2 underline text-sm pt-1">Download ${fileInfo.original_name}</a>`;
389
-
390
- el.innerHTML = `
391
- <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'}">
392
- ${!isSelf ? `<span class="text-[10px] text-white/50 mb-1 ml-1">${username}</span>` : ''}
393
- <div class="${bg} p-2 rounded-[18px] ${isSelf ? 'rounded-tr-sm' : 'rounded-tl-sm'} shadow-sm">
394
- ${content}
395
- </div>
396
- </div>`;
397
- container.appendChild(el);
398
- container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
399
  }
400
-
401
- // Enter key handling for home inputs
402
- document.getElementById('usernameInputHome').addEventListener('keypress', (e) => {
403
- if(e.key === 'Enter') {
404
- if(document.getElementById('joinRoomId').value) joinRoom();
405
- else createRoom();
 
 
 
406
  }
407
- });
408
-
409
- function leaveRoom() {
410
- if (ws) ws.close();
411
- showHome();
412
  }
413
-
414
- showHome();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  </script>
416
  </body>
417
  </html>
 
2
  <html lang="en">
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;
 
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);
 
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>