Promptly / app.js
Inayatgaming's picture
Update app.js
8f408db verified
// 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<bytes.length;i+=chunk){
const sub = bytes.subarray(i, Math.min(i+chunk, bytes.length));
binary += String.fromCharCode.apply(null, sub);
}
const b64 = btoa(binary);
const payload = `/send ${f.name} ${b64}`;
if(ws && ws.readyState === WebSocket.OPEN){
ws.send(payload);
appendMessage(`[file] ${f.name}`, 'me');
} else appendMessage('❗ Not connected.', 'system');
}catch(err){
appendMessage('❌ File read error: ' + err.toString(), 'system');
}
};
reader.onerror = ()=> 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};
})();