This chat widget was implemented to provide real-time interaction between site visitors and support/admin via Telegram. Visitors can type messages directly in the widget — and every message is delivered to a Telegram bot.
Each user is assigned a unique UID which allows the admin to respond to each message separately. Admin replies (sent via /reply_uid_xxxxxx command) appear directly in the chat interface of the user, creating a smooth support flow.
The widget supports message bubbles, avatars, a typing indicator, automatic scroll, persistent message history using localStorage, and visual feedback for delivery. Messages are removed from the queue on the server once shown.
View Live Block// Check for UID
let UID = localStorage.getItem('chat_uid');
if (!UID) {
UID = 'uid_' + Math.random().toString(36).substring(2, 10);
localStorage.setItem('chat_uid', UID);
}
let greeted = localStorage.getItem('greeted_' + UID);
let lastReplyShown = localStorage.getItem('last_reply_' + UID) || '';
const history = JSON.parse(localStorage.getItem('chat_history_' + UID) || '[]');
history.forEach(({ text, type }) => addMessage(text, type, false));
function scrollToBottom() {
messages.scrollTop = messages.scrollHeight;
}
function addMessage(text, type = 'user', save = true) {
const msg = document.createElement('div');
msg.className = `chat-message ${type}`;
const bubble = document.createElement('div');
bubble.className = `chat-bubble ${type}`;
bubble.textContent = text;
if (type === 'admin') {
const avatar = document.createElement('img');
avatar.src = '/me.jpg';
avatar.alt = 'Admin';
avatar.className = 'chat-avatar';
msg.appendChild(avatar);
}
msg.appendChild(bubble);
messages.appendChild(msg);
scrollToBottom();
if (save) {
history.push({ text, type });
localStorage.setItem('chat_history_' + UID, JSON.stringify(history));
}
}
function showTyping() {
if (document.getElementById('typing-indicator')) return;
const typing = document.createElement('div');
typing.className = 'chat-message admin';
typing.id = 'typing-indicator';
const bubble = document.createElement('div');
bubble.className = 'chat-bubble admin';
bubble.textContent = '...typing';
typing.appendChild(document.createElement('span'));
typing.appendChild(bubble);
messages.appendChild(typing);
scrollToBottom();
}
function hideTyping() {
const el = document.getElementById('typing-indicator');
if (el) el.remove();
}
setInterval(() => {
const cleanUID = UID.replace('uid_', '');
fetch('/replies/all.json?ts=' + Date.now())
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!data || !data[cleanUID]) return;
const reply = data[cleanUID].reply;
if (reply && reply !== lastReplyShown) {
showTyping();
setTimeout(() => {
hideTyping();
addMessage(reply, 'admin');
lastReplyShown = reply;
localStorage.setItem('last_reply_' + UID, reply);
// ✅ Удаляем ответ с сервера
fetch(`/replies/clear.php?uid=${cleanUID}`);
}, 1500);
}
})
.catch(() => {});
}, 5000);
trigger.onclick = () => {
popup.classList.remove('hidden');
if (!greeted) {
addMessage("👋 Hi! Feel free to leave a message — we’ll get back to you soon.", 'admin');
localStorage.setItem('greeted_' + UID, '1');
greeted = true;
}
};
close.onclick = () => popup.classList.add('hidden');
sendBtn.onclick = () => {
const text = input.value.trim();
if (!text) return;
addMessage(text + ' ✅', 'user');
input.value = '';
const fullText = `💬 New message from site:\n\n👤 UID: ${UID}\n📝 ${text}\n\nReply:\n/reply_${UID} your message`;
fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: CHAT_ID, text: fullText })
});
};
window.leaveContact = function (type) {
const contact = prompt(`Enter your ${type}:`);
if (contact) {
addMessage(`${type.toUpperCase()}: ${contact}`, 'user');
const fullText = `📩 User left ${type}:\n\n👤 UID: ${UID}\n${type}: ${contact}`;
fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: CHAT_ID, text: fullText })
});
}
};
};
}