messenger / app /static /index.html
lvwerra's picture
lvwerra HF Staff
Upload folder using huggingface_hub
d4fd753 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Messenger</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f0f0f; color: #e0e0e0; height: 100dvh;
display: flex; flex-direction: column;
}
header {
padding: 12px 16px; background: #1a1a1a; border-bottom: 1px solid #2a2a2a;
font-weight: 600; font-size: 16px; flex-shrink: 0;
}
header span { color: #888; font-weight: 400; font-size: 13px; margin-left: 8px; }
/* Landing */
#landing {
display: flex; align-items: center; justify-content: center;
flex: 1; padding: 20px;
}
#landing .card {
background: #1a1a1a; border-radius: 12px; padding: 32px;
max-width: 400px; width: 100%; text-align: center;
}
#landing h1 { font-size: 24px; margin-bottom: 8px; }
#landing p { color: #888; margin-bottom: 24px; font-size: 14px; }
#landing input {
width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333;
background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px;
outline: none;
}
#landing input:focus { border-color: #5b7bd5; }
#landing button {
width: 100%; padding: 12px; border-radius: 8px; border: none;
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
cursor: pointer;
}
#landing button:hover { background: #4a6bc4; }
/* Join dialog */
#join-dialog {
display: flex; align-items: center; justify-content: center;
flex: 1; padding: 20px;
}
#join-dialog .card {
background: #1a1a1a; border-radius: 12px; padding: 32px;
max-width: 400px; width: 100%; text-align: center;
}
#join-dialog h2 { font-size: 20px; margin-bottom: 16px; }
#join-dialog input {
width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333;
background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px;
outline: none;
}
#join-dialog input:focus { border-color: #5b7bd5; }
#join-dialog button {
width: 100%; padding: 12px; border-radius: 8px; border: none;
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
cursor: pointer;
}
#join-dialog button:hover { background: #4a6bc4; }
/* Chat */
#chat { display: none; flex-direction: column; flex: 1; min-height: 0; }
#messages {
flex: 1; overflow-y: auto; padding: 16px; display: flex;
flex-direction: column; gap: 4px;
}
.msg {
padding: 6px 0; line-height: 1.4;
}
.msg .name { font-weight: 600; margin-right: 8px; }
.msg .time { color: #555; font-size: 11px; margin-left: 6px; }
.msg.self .name { color: #5b7bd5; }
.msg.system { color: #666; font-style: italic; font-size: 13px; }
#input-bar {
padding: 12px 16px; background: #1a1a1a; border-top: 1px solid #2a2a2a;
display: flex; gap: 8px; flex-shrink: 0;
}
#msg-input {
flex: 1; padding: 10px 14px; border-radius: 8px; border: 1px solid #333;
background: #0f0f0f; color: #e0e0e0; font-size: 15px; outline: none;
}
#msg-input:focus { border-color: #5b7bd5; }
#send-btn {
padding: 10px 20px; border-radius: 8px; border: none;
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
cursor: pointer;
}
#send-btn:hover { background: #4a6bc4; }
.hidden { display: none !important; }
</style>
</head>
<body>
<header id="header">
Messenger <span id="room-label"></span>
</header>
<!-- Landing: create or go to a room -->
<div id="landing" class="hidden">
<div class="card">
<h1>Messenger</h1>
<p>Create a chat room and share the link</p>
<input type="text" id="room-name-input" placeholder="Room name (e.g. my-project)">
<button onclick="goToRoom()">Create Room</button>
</div>
</div>
<!-- Join dialog: pick a display name -->
<div id="join-dialog" class="hidden">
<div class="card">
<h2 id="join-title">Join room</h2>
<input type="text" id="display-name" placeholder="Your name" autofocus>
<button onclick="joinRoom()">Join</button>
</div>
</div>
<!-- Chat view -->
<div id="chat">
<div id="messages"></div>
<div id="input-bar">
<input type="text" id="msg-input" placeholder="Type a message..." autofocus>
<button id="send-btn" onclick="sendMessage()">Send</button>
</div>
</div>
<script>
const path = window.location.pathname;
const roomMatch = path.match(/^\/ch\/([^/]+)/);
const roomName = roomMatch ? roomMatch[1] : null;
let token = null;
let userId = null;
let lastMessageId = null;
let pollTimer = null;
// Determine which view to show
if (!roomName) {
document.getElementById('landing').classList.remove('hidden');
document.getElementById('chat').style.display = 'none';
} else {
document.getElementById('room-label').textContent = '#' + roomName;
// Check for saved session
const saved = localStorage.getItem('messenger_' + roomName);
if (saved) {
const s = JSON.parse(saved);
token = s.token;
userId = s.userId;
startChat();
} else {
document.getElementById('join-dialog').classList.remove('hidden');
document.getElementById('join-title').textContent = 'Join #' + roomName;
document.getElementById('chat').style.display = 'none';
}
}
function goToRoom() {
const name = document.getElementById('room-name-input').value.trim()
.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
if (name) window.location.href = '/ch/' + name;
}
async function joinRoom() {
const name = document.getElementById('display-name').value.trim();
if (!name) return;
try {
const resp = await fetch('/api/ch/' + roomName + '/join', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name}),
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.detail || 'Join failed');
token = data.token;
userId = name;
localStorage.setItem('messenger_' + roomName, JSON.stringify({token, userId}));
startChat();
} catch (e) {
alert('Failed to join: ' + e.message);
}
}
function startChat() {
document.getElementById('landing').classList.add('hidden');
document.getElementById('join-dialog').classList.add('hidden');
document.getElementById('chat').style.display = 'flex';
document.getElementById('msg-input').focus();
loadMessages();
pollTimer = setInterval(loadMessages, 2000);
}
async function loadMessages() {
try {
const resp = await fetch(`/api/ch/${roomName}/messages?token=${encodeURIComponent(token)}&limit=100`);
const data = await resp.json();
renderMessages(data.messages || []);
} catch (e) {
console.error('Failed to load messages:', e);
}
}
function renderMessages(msgs) {
const container = document.getElementById('messages');
const wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50;
// Check if new messages arrived
const lastId = msgs.length ? msgs[msgs.length - 1].id : null;
if (lastId === lastMessageId) return;
lastMessageId = lastId;
container.innerHTML = '';
for (const msg of msgs) {
const div = document.createElement('div');
const senderName = msg.sender_name || msg.sender.split(':')[0].replace('@', '');
const isSelf = senderName.toLowerCase() === userId?.toLowerCase() ||
msg.sender_name === userId;
div.className = 'msg' + (isSelf ? ' self' : '');
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
div.innerHTML = `<span class="name">${esc(senderName)}</span>${esc(msg.text)}<span class="time">${time}</span>`;
container.appendChild(div);
}
if (wasAtBottom) container.scrollTop = container.scrollHeight;
}
async function sendMessage() {
const input = document.getElementById('msg-input');
const text = input.value.trim();
if (!text || !token) return;
input.value = '';
try {
await fetch(`/api/ch/${roomName}/send`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token, text}),
});
loadMessages();
} catch (e) {
console.error('Send failed:', e);
}
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// Enter to send
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (document.activeElement === document.getElementById('msg-input')) {
e.preventDefault();
sendMessage();
} else if (document.activeElement === document.getElementById('display-name')) {
e.preventDefault();
joinRoom();
} else if (document.activeElement === document.getElementById('room-name-input')) {
e.preventDefault();
goToRoom();
}
}
});
</script>
</body>
</html>