chatbot / templates /index.html
Antaram's picture
Update templates/index.html
a299189 verified
<!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;
}
/* --- Antaram Animations --- */
@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 & UI --- */
.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);
}
/* --- Chat Bubbles (The Antaram Look) --- */
.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); }
/* --- Autocomplete Menu --- */
#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); }
/* --- Utils --- */
.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">
<!-- DESERT BACKGROUND -->
<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>
<!-- TOAST -->
<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>
<!-- VIEW 1: LOGIN -->
<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>
<!-- VIEW 2: CHAT -->
<div id="chatView" class="hidden relative z-10 w-full h-full flex flex-col">
<!-- Header -->
<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>
<!-- Messages -->
<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>
<!-- Fixed Input Area -->
<div class="fixed bottom-0 left-0 w-full z-30 px-3 mobile-input">
<div class="max-w-3xl mx-auto relative">
<!-- Autocomplete Menu -->
<div id="suggestions"></div>
<!-- Reply Preview -->
<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">&times;</button>
</div>
<!-- File Preview -->
<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">&times;</button>
</div>
<!-- Input Bar -->
<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>
// --- DATA & CONFIG ---
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 = [];
// --- INIT ---
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; }
// --- VIEWS ---
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() {
// Robust share link logic
const url = `${window.location.origin}/room/${roomId}`;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url).then(() => showToast("Link Copied"));
} else {
// Fallback
alert(`Copy this link:\n${url}`);
}
}
// --- CONNECTION ---
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 = "/";
}
// --- MESSAGING ---
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>');
// File
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>`;
}
// Reply Quote
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>`;
}
// Double Tap to Reply
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>