// app.js (() => { const WS_URI = "wss://promptly-az40.onrender.com/ws"; const statusEl = document.getElementById("status"); const connectBtn = document.getElementById("connectBtn"); const disconnectBtn = document.getElementById("disconnectBtn"); const modeText = document.getElementById("modeText"); const messagesEl = document.getElementById("messages"); const inputEl = document.getElementById("input"); const sendBtn = document.getElementById("sendBtn"); const fileBtn = document.getElementById("fileBtn"); const filePicker = document.getElementById("filePicker"); const loginOverlay = document.getElementById("loginOverlay"); const loginForm = document.getElementById("loginForm"); const loginUsername = document.getElementById("loginUsername"); const loginPassword = document.getElementById("loginPassword"); const loginGuest = document.getElementById("loginGuest"); const themeToggle = document.getElementById("themeToggle"); const root = document.getElementById("root"); const onlineCountEl = document.getElementById("onlineCount"); let ws = null; let handshakeStep = 0; let awaiting = null; let lastUsername = ""; let connected = false; let pendingLogin = null; // {username,password} // Utility function stripAnsi(str){ return String(str).replace(/\x1b\[[0-9;]*m/g,''); } function setStatus(connectedNow){ connected = !!connectedNow; if(connected){ statusEl.classList.remove("disconnected"); statusEl.classList.add("connected"); statusEl.textContent = "Connected"; disconnectBtn.disabled = false; connectBtn.disabled = true; }else{ statusEl.classList.remove("connected"); statusEl.classList.add("disconnected"); statusEl.textContent = "Disconnected"; disconnectBtn.disabled = true; connectBtn.disabled = false; } } function appendMessage(content, cls='system'){ const el = document.createElement('div'); el.className = 'msg ' + (cls === 'me' ? 'me' : (cls === 'file' ? 'file' : 'system')); if(typeof content === 'string'){ el.textContent = content; } else if(content.type === 'file'){ // content: {filename, blob, isImage} const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `📥 Received ${content.filename}`; el.appendChild(meta); if(content.isImage){ const wrap = document.createElement('a'); wrap.href = URL.createObjectURL(content.blob); wrap.download = content.filename; wrap.className = 'img-wrap'; const img = document.createElement('img'); img.src = wrap.href; img.alt = content.filename; wrap.appendChild(img); el.appendChild(wrap); // download link below const dl = document.createElement('a'); dl.href = wrap.href; dl.download = content.filename; dl.className = 'download-link muted'; dl.textContent = `Download ${content.filename}`; el.appendChild(dl); // revoke after a bit when user navigates away setTimeout(()=>URL.revokeObjectURL(wrap.href), 60000); } else { const dl = document.createElement('a'); dl.href = URL.createObjectURL(content.blob); dl.download = content.filename; dl.className = 'download-link muted'; dl.textContent = `Download ${content.filename}`; el.appendChild(dl); setTimeout(()=>URL.revokeObjectURL(dl.href), 60000); } } else { el.textContent = JSON.stringify(content); } messagesEl.appendChild(el); messagesEl.scrollTo({top: messagesEl.scrollHeight, behavior: 'smooth'}); } function connect(){ if(ws) try{ ws.close() }catch(e){} ws = new WebSocket(WS_URI); ws.binaryType = 'arraybuffer'; ws.addEventListener('open', ()=>{ setStatus(true); appendMessage('🟢 Connected to ' + WS_URI); handshakeStep = 0; awaiting = null; modeText.textContent = 'Handshake'; // if pending login already provided, we will act when user submits login }); ws.addEventListener('message', (ev) => { let data = ev.data; if(data instanceof ArrayBuffer){ // binary from server: try to treat as file (unlikely); skip for now appendMessage('📎 Received binary data (unsupported preview)', 'system'); return; } else { data = stripAnsi(String(data)); } // Detect server-sent file marker: '📥 filename base64' if(data.startsWith('📥')){ const m = data.match(/^📥\s+(\S+)\s+([\s\S]+)$/); if(m){ const fname = m[1]; const b64 = m[2]; try{ const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); const blob = new Blob([bytes]); const isImage = /\.(png|jpe?g|gif|webp|svg)$/i.test(fname); appendMessage({type:'file', filename: fname, blob, isImage}, 'file'); return; }catch(e){ appendMessage('❌ File parse error: ' + e.toString()); } } } appendMessage(data, 'system'); // handshake flow handling if(data.toLowerCase().includes('please enter password') || data.toLowerCase().includes('enter password')){ awaiting = 'password'; modeText.textContent = 'Entering password'; } else if(data.toLowerCase().includes('enter your username') || data.toLowerCase().includes('enter username')){ awaiting = 'username'; modeText.textContent = 'Entering username'; } else if(data.toLowerCase().includes('welcome') || data.toLowerCase().includes('joined the chat')){ awaiting = null; modeText.textContent = 'Chat'; } else if(data.toLowerCase().includes('online users')){ // try to parse count const m = data.match(/Online users\s*\(?(\d+)\)?/i); if(m) onlineCountEl.textContent = m[1]; } }); ws.addEventListener('close', ()=>{ appendMessage('🔴 Connection closed.'); setStatus(false); ws = null; }); ws.addEventListener('error', (e)=>{ appendMessage('❌ Connection error. Check server & console.'); setStatus(false); }); } function disconnect(){ if(ws) try{ ws.close(); }catch(e){} ws = null; setStatus(false); appendMessage('Disconnected by client.'); } // send helper function sendText(msg){ if(!ws || ws.readyState !== WebSocket.OPEN){ appendMessage('❗ Not connected.', 'system'); return; } ws.send(msg); appendMessage(`[You] ${msg}`, 'me'); } // file upload: read file -> base64 -> send as /send filename base64 (server expects same) filePicker.addEventListener('change', async (e)=>{ const f = e.target.files && e.target.files[0]; if(!f) return; const reader = new FileReader(); reader.onload = () => { try{ const ab = reader.result; const bytes = new Uint8Array(ab); let binary = ''; const chunk = 0x8000; for(let i=0;i appendMessage('❌ Could not read file.', 'system'); reader.readAsArrayBuffer(f); }); fileBtn.addEventListener('click', ()=> filePicker.click()); // login handling: send password then username preserving handshake order loginForm.addEventListener('submit', (ev)=>{ ev.preventDefault(); const username = loginUsername.value.trim(); const password = loginPassword.value; pendingLogin = {username, password}; // ensure connected if(!ws || ws.readyState !== WebSocket.OPEN){ appendMessage('Connecting to server...', 'system'); connect(); // wait until open const waitOpen = () => new Promise(res=>{ if(ws && ws.readyState === WebSocket.OPEN) return res(); const t = setInterval(()=>{ if(ws && ws.readyState === WebSocket.OPEN){ clearInterval(t); res(); } }, 150); // timeout fallback setTimeout(()=>res(), 5000); }); waitOpen().then(()=> doHandshakeLogin(pendingLogin)); } else doHandshakeLogin(pendingLogin); }); loginGuest.addEventListener('click', ()=>{ loginUsername.value = 'Guest' + Math.floor(Math.random()*9000+1000); loginPassword.value = ''; loginForm.dispatchEvent(new Event('submit', {cancelable:true})); }); function doHandshakeLogin({username,password}){ // send password then username with slight delay to emulate terminal client handshake try{ if(password) ws.send(password); appendMessage('[You] (password entered)', 'me'); setTimeout(()=>{ ws.send(username); appendMessage('[You] ' + username, 'me'); // hide login overlay and switch to chat loginOverlay.style.display = 'none'; // keep username stored lastUsername = username; modeText.textContent = 'Chat'; inputEl.focus(); }, 300); }catch(e){ appendMessage('❌ Login send error: ' + e.toString(), 'system'); } } // message send sendBtn.addEventListener('click', ()=>{ const txt = inputEl.value.trim(); if(!txt) return; sendText(txt); inputEl.value = ''; }); inputEl.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendBtn.click(); } }); // connect/disconnect controls connectBtn.addEventListener('click', ()=> connect()); disconnectBtn.addEventListener('click', ()=> disconnect()); // theme toggle themeToggle.addEventListener('click', ()=>{ if(root.classList.contains('theme-dark')){ root.classList.remove('theme-dark'); root.classList.add('theme-light'); document.documentElement.style.setProperty('--bg-dark','#f5f7fb'); document.documentElement.style.setProperty('--text','#071029'); themeToggle.textContent = '🌞'; } else { root.classList.remove('theme-light'); root.classList.add('theme-dark'); themeToggle.textContent = '🌗'; document.documentElement.style.removeProperty('--bg-dark'); document.documentElement.style.removeProperty('--text'); } }); // auto-connect on load window.addEventListener('load', ()=>{ connect(); // show login overlay (auto) loginOverlay.style.display = 'flex'; loginUsername.focus(); // register sw if available if('serviceWorker' in navigator){ navigator.serviceWorker.register('service-worker.js').catch(()=>{/*ignore*/}); } }); // expose for debugging window._promptly = {connect,disconnect, wsRef: ()=> ws}; })();