| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Antaram Chat</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| |
| <style> |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: #11100f; |
| color: #F0EFE9; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| |
| @keyframes slideUpFade { 0% { opacity: 0; transform: translateY(30px); } 100% { opacity: 1; transform: translateY(0); } } |
| @keyframes scaleIn { 0% { opacity: 0; transform: scale(0.95); } 100% { opacity: 1; transform: scale(1); } } |
| .animate-enter { animation: slideUpFade 0.6s cubic-bezier(0.19, 1, 0.22, 1) forwards; } |
| |
| |
| .glass-panel { |
| background: rgba(255, 255, 255, 0.03); |
| backdrop-filter: blur(20px); |
| -webkit-backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| } |
| |
| |
| .bubble-self { background-color: #F0EFE9; color: #1C1A19; } |
| .bubble-other { background-color: rgba(255, 255, 255, 0.08); color: #ffffff; border: 1px solid rgba(255, 255, 255, 0.05); } |
| .bubble-ai { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 1px solid rgba(59, 130, 246, 0.3); } |
| |
| |
| #suggestions { |
| position: absolute; |
| bottom: 100%; |
| left: 0; |
| width: 100%; |
| max-height: 200px; |
| overflow-y: auto; |
| background: rgba(20, 20, 20, 0.95); |
| backdrop-filter: blur(10px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| border-radius: 12px; |
| margin-bottom: 8px; |
| z-index: 50; |
| display: none; |
| box-shadow: 0 -10px 40px rgba(0,0,0,0.5); |
| } |
| .suggestion-item { |
| padding: 10px 16px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| border-bottom: 1px solid rgba(255,255,255,0.03); |
| transition: background 0.1s; |
| } |
| .suggestion-item.selected, .suggestion-item:hover { background: rgba(255, 255, 255, 0.1); } |
| |
| |
| .custom-scroll::-webkit-scrollbar { width: 4px; } |
| .custom-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; } |
| .mobile-input { padding-bottom: env(safe-area-inset-bottom, 20px); } |
| |
| .ai-cursor::after { content: '▋'; animation: blink 1s infinite; color: #60a5fa; margin-left: 2px; } |
| @keyframes blink { 50% { opacity: 0; } } |
| </style> |
| </head> |
| <body class="relative w-full h-[100dvh] overflow-hidden flex flex-col"> |
|
|
| |
| <div class="absolute inset-0 z-0 pointer-events-none"> |
| <img src="https://images.unsplash.com/photo-1494438639946-1ebd1d20bf85?q=80&w=2068&auto=format&fit=crop" |
| class="w-full h-full object-cover opacity-60" alt="Desert"> |
| <div class="absolute inset-0 bg-gradient-to-b from-[#11100f]/40 via-[#11100f]/90 to-[#11100f]"></div> |
| <div class="absolute inset-0 bg-black/40 backdrop-blur-[2px]"></div> |
| </div> |
|
|
| |
| <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"> |
| <div class="glass-panel px-6 py-2 rounded-full text-sm font-medium shadow-2xl flex items-center gap-2"> |
| <span class="text-green-400">✓</span> <span id="toastMsg">Notification</span> |
| </div> |
| </div> |
|
|
| |
| <div id="homeView" class="relative z-10 w-full h-full flex items-center justify-center p-4"> |
| <div class="w-full max-w-sm animate-enter"> |
| <div class="glass-panel rounded-[32px] p-8 shadow-2xl text-center"> |
| <h1 class="text-4xl font-medium tracking-tight text-white mb-1">Antaram.</h1> |
| <p class="text-white/40 text-xs font-medium tracking-widest uppercase mb-8">Premium Chat Experience</p> |
| |
| <input type="text" id="usernameInput" placeholder="Enter Identity" |
| 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"> |
| |
| <div id="roomInputContainer" class="hidden mb-4"> |
| <input type="text" id="roomInput" placeholder="Room ID" |
| 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"> |
| </div> |
|
|
| <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> |
| <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> |
| <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> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col"> |
| |
| |
| <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"> |
| <div class="flex items-center gap-3"> |
| <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> |
| <div> |
| <h1 class="text-sm font-semibold tracking-wide">Room <span id="displayRoomId" class="opacity-70 font-mono"></span></h1> |
| <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> |
| </div> |
| </div> |
| <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"> |
| <span>Share</span> |
| <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> |
| </button> |
| </header> |
|
|
| |
| <main class="flex-1 w-full max-w-3xl mx-auto px-4 overflow-hidden flex flex-col relative"> |
| <div id="messages" class="flex-1 overflow-y-auto custom-scroll space-y-4 pt-4 pb-32"></div> |
| </main> |
|
|
| |
| <div class="fixed bottom-0 left-0 w-full z-30 px-3 mobile-input"> |
| <div class="max-w-3xl mx-auto relative"> |
| |
| |
| <div id="suggestions"></div> |
|
|
| |
| <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"> |
| <div class="border-l-2 border-blue-400 pl-3"> |
| <span class="text-[10px] text-blue-400 font-bold block mb-0.5">Replying to <span id="replyUser">User</span></span> |
| <span class="text-xs text-white/60 truncate block max-w-[250px]" id="replyText">...</span> |
| </div> |
| <button onclick="cancelReply()" class="p-2 opacity-50 hover:opacity-100">×</button> |
| </div> |
|
|
| |
| <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"> |
| <span class="text-xs text-white/80" id="fileName">file</span> |
| <button onclick="clearFile()" class="opacity-50 hover:opacity-100">×</button> |
| </div> |
|
|
| |
| <div class="glass-panel rounded-[26px] p-1.5 flex items-center gap-2 shadow-2xl bg-[#000000]/60"> |
| <input type="file" id="fileInput" class="hidden" onchange="handleFileSelect()"> |
| <button onclick="document.getElementById('fileInput').click()" class="p-3 text-white/40 hover:text-white transition-colors rounded-full"> |
| <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> |
| </button> |
|
|
| <input type="text" id="messageInput" placeholder="Message..." autocomplete="off" |
| class="flex-1 bg-transparent text-white px-2 py-3 focus:outline-none placeholder-white/30 text-[15px] min-w-0 font-light" |
| onkeydown="handleKeydown(event)" onkeyup="handleKeyup(event)"> |
| |
| <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"> |
| <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> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const EMOJIS = { |
| 'heart': '❤️', 'fire': '🔥', 'smile': '😊', 'laugh': '😂', 'thumbs': '👍', |
| 'check': '✅', 'rocket': '🚀', 'eyes': '👀', 'skull': '💀', 'star': '⭐', |
| 'think': '🤔', 'cry': '😭', 'party': '🎉', '100': '💯', 'pray': '🙏' |
| }; |
| |
| let ws = null, roomId = null, username = localStorage.getItem('antaram_user') || ''; |
| let activeUsers = new Set(['Antaram.ai']); |
| let replyCtx = null, selectedFile = null, currentAiEl = null; |
| let suggestionIndex = -1, suggestionList = []; |
| |
| |
| const serverRoomId = "{{ room_id }}" !== "None" ? "{{ room_id }}" : new URLSearchParams(window.location.search).get('room'); |
| if (username) document.getElementById('usernameInput').value = username; |
| if (serverRoomId) { toggleJoin(); document.getElementById('roomInput').value = serverRoomId; } |
| |
| |
| function toggleJoin() { |
| document.getElementById('createBtn').classList.add('hidden'); |
| document.getElementById('joinBtn').classList.add('hidden'); |
| document.getElementById('roomInputContainer').classList.remove('hidden'); |
| document.getElementById('enterBtn').classList.remove('hidden'); |
| } |
| |
| function showToast(msg) { |
| const t = document.getElementById('toast'); |
| document.getElementById('toastMsg').innerText = msg; |
| t.classList.remove('opacity-0'); |
| setTimeout(() => t.classList.add('opacity-0'), 2500); |
| } |
| |
| function copyLink() { |
| |
| const url = `${window.location.origin}/room/${roomId}`; |
| if (navigator.clipboard && window.isSecureContext) { |
| navigator.clipboard.writeText(url).then(() => showToast("Link Copied")); |
| } else { |
| |
| alert(`Copy this link:\n${url}`); |
| } |
| } |
| |
| |
| function createRoom() { |
| username = document.getElementById('usernameInput').value.trim(); |
| if (!username) return showToast("Name required"); |
| localStorage.setItem('antaram_user', username); |
| fetch('/create-room', {method:'POST'}).then(r=>r.json()).then(d=>connect(d.room_id)); |
| } |
| |
| function joinRoom() { |
| username = document.getElementById('usernameInput').value.trim(); |
| roomId = document.getElementById('roomInput').value.trim().toUpperCase(); |
| if (!username || !roomId) return showToast("Name & ID required"); |
| localStorage.setItem('antaram_user', username); |
| connect(roomId); |
| } |
| |
| function connect(rid) { |
| roomId = rid; |
| document.getElementById('displayRoomId').textContent = rid; |
| document.getElementById('homeView').classList.add('hidden'); |
| document.getElementById('chatView').classList.remove('hidden'); |
| |
| const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| ws = new WebSocket(`${proto}//${window.location.host}/ws/${rid}`); |
| |
| ws.onopen = () => ws.send(JSON.stringify({type:'join', username})); |
| ws.onmessage = e => handleData(JSON.parse(e.data)); |
| ws.onclose = e => { if(e.code===1008) { alert("Invalid Room"); leaveRoom(); } }; |
| } |
| |
| function leaveRoom() { |
| if(ws) ws.close(); |
| window.location.href = "/"; |
| } |
| |
| |
| function handleData(data) { |
| const box = document.getElementById('messages'); |
| |
| if (data.type === 'history') { |
| data.data.forEach(renderMessage); |
| } else if (data.type === 'system') { |
| if(data.users) { |
| activeUsers = new Set(data.users); |
| activeUsers.add('Antaram.ai'); |
| } |
| } else if (data.type === 'ai_start') { |
| const el = document.createElement('div'); |
| el.className = 'flex w-full justify-start animate-enter'; |
| 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>`; |
| box.appendChild(el); |
| currentAiEl = el.querySelector('.ai-cursor'); |
| } else if (data.type === 'ai_chunk' && currentAiEl) { |
| currentAiEl.innerText += data.chunk; |
| } else if (data.type === 'ai_end' && currentAiEl) { |
| currentAiEl.innerHTML = marked.parse(currentAiEl.innerText); |
| currentAiEl.classList.remove('ai-cursor'); |
| currentAiEl = null; |
| } else if (data.type === 'message') { |
| renderMessage(data); |
| } |
| |
| box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' }); |
| } |
| |
| function renderMessage(msg) { |
| const box = document.getElementById('messages'); |
| const isSelf = msg.username === username; |
| |
| const el = document.createElement('div'); |
| el.className = `flex w-full ${isSelf ? 'justify-end' : 'justify-start'} animate-enter`; |
| |
| const bgClass = isSelf ? 'bubble-self' : 'bubble-other'; |
| let content = msg.text.replace(/@antaram.ai/gi, '<span class="text-blue-400 font-bold">@antaram.ai</span>'); |
| |
| |
| if (msg.file) { |
| const f = msg.file; |
| content += f.file_type.startsWith('image') |
| ? `<img src="${f.file_url}" class="mt-2 rounded-lg max-h-48 border border-black/10">` |
| : `<a href="${f.file_url}" target="_blank" class="block mt-1 text-xs underline opacity-80">📎 ${f.original_name}</a>`; |
| } |
| |
| |
| let replyHTML = ''; |
| if (msg.reply_to && msg.reply_content) { |
| replyHTML = ` |
| <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"> |
| <span class="font-bold block">${msg.reply_content}</span> |
| </div>`; |
| } |
| |
| |
| el.innerHTML = ` |
| <div class="max-w-[85%] flex flex-col ${isSelf ? 'items-end' : 'items-start'}"> |
| ${!isSelf ? `<span class="text-[10px] text-white/40 ml-1 mb-0.5">${msg.username}</span>` : ''} |
| <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" |
| ondblclick="initReply('${msg.id}', '${msg.username}', '${msg.text.replace(/'/g,"\\'")}')"> |
| ${replyHTML} |
| ${content} |
| </div> |
| </div>`; |
| box.appendChild(el); |
| } |
| |
| // --- REPLY SYSTEM --- |
| function initReply(id, user, text) { |
| replyCtx = { id, user, text }; |
| document.getElementById('replyPreview').classList.remove('hidden'); |
| document.getElementById('replyUser').innerText = user; |
| document.getElementById('replyText').innerText = text; |
| document.getElementById('messageInput').focus(); |
| } |
| function cancelReply() { |
| replyCtx = null; |
| document.getElementById('replyPreview').classList.add('hidden'); |
| } |
| |
| // --- AUTOCOMPLETE (KEYBOARD DRIVEN) --- |
| function handleKeyup(e) { |
| if(e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown') return; // Handled in keydown |
| |
| const input = e.target; |
| const val = input.value; |
| const cursor = input.selectionStart; |
| const lastWord = val.slice(0, cursor).split(/\s/).pop(); |
| |
| if (lastWord.startsWith('@')) { |
| const query = lastWord.slice(1).toLowerCase(); |
| const matches = Array.from(activeUsers).filter(u => u.toLowerCase().includes(query)); |
| renderSuggestions(matches, '@'); |
| } else if (lastWord.startsWith(':')) { |
| const query = lastWord.slice(1).toLowerCase(); |
| const matches = Object.keys(EMOJIS).filter(k => k.includes(query)); |
| renderSuggestions(matches, ':', true); |
| } else { |
| hideSuggestions(); |
| } |
| } |
| |
| function handleKeydown(e) { |
| const list = document.getElementById('suggestions'); |
| if (list.style.display === 'block') { |
| if (e.key === 'ArrowUp') { |
| e.preventDefault(); |
| suggestionIndex = Math.max(0, suggestionIndex - 1); |
| highlightSuggestion(); |
| } else if (e.key === 'ArrowDown') { |
| e.preventDefault(); |
| suggestionIndex = Math.min(suggestionList.length - 1, suggestionIndex + 1); |
| highlightSuggestion(); |
| } else if (e.key === 'Enter') { |
| e.preventDefault(); |
| if (suggestionIndex >= 0 && suggestionList[suggestionIndex]) { |
| applySuggestion(suggestionList[suggestionIndex].val, suggestionList[suggestionIndex].trigger, suggestionList[suggestionIndex].isEmoji); |
| } |
| } |
| } else if (e.key === 'Enter') { |
| sendMessage(); |
| } |
| } |
| |
| function renderSuggestions(matches, trigger, isEmoji = false) { |
| const box = document.getElementById('suggestions'); |
| if (matches.length === 0) return hideSuggestions(); |
| |
| suggestionList = matches.map(m => ({ val: m, trigger, isEmoji })); |
| suggestionIndex = 0; |
| |
| box.innerHTML = matches.map((m, i) => ` |
| <div class="suggestion-item ${i===0?'selected':''}" onclick="applySuggestion('${m}', '${trigger}', ${isEmoji})"> |
| ${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>`} |
| <span class="text-sm text-white/90">${m}</span> |
| </div> |
| `).join(''); |
| box.style.display = 'block'; |
| } |
| |
| function highlightSuggestion() { |
| const items = document.querySelectorAll('.suggestion-item'); |
| items.forEach((el, i) => { |
| if (i === suggestionIndex) { |
| el.classList.add('selected'); |
| el.scrollIntoView({ block: 'nearest' }); |
| } else el.classList.remove('selected'); |
| }); |
| } |
| |
| function hideSuggestions() { |
| document.getElementById('suggestions').style.display = 'none'; |
| suggestionIndex = -1; |
| } |
| |
| function applySuggestion(val, trigger, isEmoji) { |
| const input = document.getElementById('messageInput'); |
| const cursor = input.selectionStart; |
| const text = input.value; |
| // Find start of word |
| const lastSpace = text.lastIndexOf(' ', cursor - 1); |
| const start = lastSpace + 1; |
| |
| const insert = isEmoji ? EMOJIS[val] : `${trigger}${val} `; |
| const after = text.substring(cursor); |
| |
| input.value = text.substring(0, start) + insert + after; |
| input.focus(); |
| hideSuggestions(); |
| } |
| |
| // --- SENDING --- |
| function sendMessage() { |
| if (selectedFile) return uploadFile(); |
| const txt = document.getElementById('messageInput').value.trim(); |
| if (!txt) return; |
| |
| ws.send(JSON.stringify({ |
| type: 'message', username, text: txt, |
| reply_to: replyCtx?.id, reply_content: replyCtx?.text |
| })); |
| |
| document.getElementById('messageInput').value = ''; |
| cancelReply(); |
| } |
| |
| async function uploadFile() { |
| const fd = new FormData(); fd.append('file', selectedFile); |
| const res = await fetch(`/upload-file/${roomId}`, {method:'POST', body:fd}).then(r=>r.json()); |
| if (res.success) { |
| ws.send(JSON.stringify({ type:'message', username, text:'', file:res.file_info, reply_to: replyCtx?.id })); |
| clearFile(); cancelReply(); |
| } |
| } |
| |
| function handleFileSelect() { |
| const f = document.getElementById('fileInput').files[0]; |
| if(f) { selectedFile = f; document.getElementById('fileName').textContent = f.name; document.getElementById('filePreview').classList.remove('hidden'); } |
| } |
| function clearFile() { selectedFile = null; document.getElementById('fileInput').value = ''; document.getElementById('filePreview').classList.add('hidden'); } |
| </script> |
| </body> |
| </html> |