From 49f502f84c8e2d2979481d71010908457fb0ce84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 20:18:43 +0900 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=20=D0=BA=20Web?= =?UTF-8?q?Socket=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- widget/src/widget.js | 69 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/widget/src/widget.js b/widget/src/widget.js index f47e08d..c899513 100644 --- a/widget/src/widget.js +++ b/widget/src/widget.js @@ -2,6 +2,7 @@ const currentScript = document.currentScript; const siteId = currentScript?.dataset?.siteId || 'unknown_site'; const apiUrl = currentScript?.dataset?.apiUrl || 'http://localhost:3000'; + const wsUrl = currentScript?.dataset?.wsUrl || apiUrl.replace(/^http/i, 'ws'); const visitorIdKey = `raspi_chat_visitor_${siteId}`; let visitorId = localStorage.getItem(visitorIdKey); @@ -17,7 +18,10 @@ .raspi-chat-panel.open{display:flex} .raspi-chat-header{padding:16px;background:#172033;color:#fff;font-weight:800} .raspi-chat-messages{flex:1;padding:14px;overflow:auto;background:#f4f6fb;color:#172033;font-size:14px} - .raspi-chat-message{margin:0 0 10px;padding:10px 12px;border-radius:14px;background:#fff} + .raspi-chat-message{margin:0 0 10px;padding:10px 12px;border-radius:14px;background:#fff;line-height:1.45} + .raspi-chat-message.operator{background:#dfe8ff;margin-left:18px} + .raspi-chat-message.visitor{background:#fff;margin-right:18px} + .raspi-chat-system{opacity:.7;font-size:12px;padding:4px 2px} .raspi-chat-form{display:flex;gap:8px;padding:12px;border-top:1px solid #e7ebf3} .raspi-chat-input{flex:1;border:1px solid #d7deea;border-radius:12px;padding:10px;font:14px system-ui} .raspi-chat-send{border:0;border-radius:12px;background:#4f6bff;color:#fff;padding:0 14px;font-weight:800;cursor:pointer} @@ -31,9 +35,9 @@ const panel = document.createElement('section'); panel.className = 'raspi-chat-panel'; panel.innerHTML = ` -
Напишите нам
+
StackWorks Support
-
Здравствуйте! Чем можем помочь?
+
Здравствуйте! Чем можем помочь?
@@ -47,24 +51,68 @@ const form = panel.querySelector('[data-role="form"]'); const input = panel.querySelector('[data-role="input"]'); + let socket = null; + let reconnectTimer = null; + button.addEventListener('click', () => { panel.classList.toggle('open'); }); - function addMessage(text) { + function addMessage(text, type = 'visitor') { const item = document.createElement('div'); - item.className = 'raspi-chat-message'; + item.className = `raspi-chat-message ${type}`; item.textContent = text; messages.appendChild(item); messages.scrollTop = messages.scrollHeight; } + function addSystem(text) { + const item = document.createElement('div'); + item.className = 'raspi-chat-system'; + item.textContent = text; + messages.appendChild(item); + messages.scrollTop = messages.scrollHeight; + } + + function connectWs() { + if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { + return; + } + + socket = new WebSocket(`${wsUrl}/ws?siteId=${encodeURIComponent(siteId)}&visitorId=${encodeURIComponent(visitorId)}`); + + socket.addEventListener('open', () => { + addSystem('Оператор подключен'); + }); + + socket.addEventListener('message', (event) => { + try { + const payload = JSON.parse(event.data); + + if (payload.type === 'operator_message') { + addMessage(payload.message?.body || 'Новое сообщение от оператора', 'operator'); + } + } catch { + // ignore invalid frames + } + }); + + socket.addEventListener('close', () => { + addSystem('Соединение потеряно, переподключение...'); + clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(connectWs, 3000); + }); + } + + connectWs(); + form.addEventListener('submit', async (event) => { event.preventDefault(); const message = input.value.trim(); if (!message) return; + input.value = ''; - addMessage(message); + addMessage(message, 'visitor'); try { const response = await fetch(`${apiUrl}/api/widget/message`, { @@ -72,10 +120,13 @@ headers: { 'content-type': 'application/json' }, body: JSON.stringify({ siteId, visitorId, message }) }); + const result = await response.json(); - if (!result.ok) addMessage(`Ошибка: ${result.error || 'message rejected'}`); - } catch (error) { - addMessage('Сообщение не отправилось. Проверьте соединение.'); + if (!result.ok) { + addSystem(`Ошибка: ${result.error || 'message rejected'}`); + } + } catch { + addSystem('Сообщение не отправилось. Проверьте соединение.'); } }); })();