Реалізація Real-Time чату підтримки на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Real-Time чату підтримки на сайті
Середня
~5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Real-Time чату поддержки на сайті

Чат поддержки дозволяє користувачам спілкуватися з операторами у реальному часі. Складається з клієнтського віджету, інтерфейсу оператора та серверної логіки маршрутизації.

Архітектура

Користувач (віджет)    Оператор (admin UI)
       │                      │
       └─── WebSocket ──► Чат-сервер ◄───────────┘
                             │
                         PostgreSQL (історія)
                         Redis (активні сесії)

Серверна логіка

import { Server } from 'socket.io';

// Черга невідповіданих чатів
const waitingQueue: Map<string, ChatSession> = new Map();
const activeSessions: Map<string, ChatSession> = new Map();

io.on('connection', async (socket) => {
  const { role } = socket.data;  // 'user' або 'operator'

  if (role === 'user') {
    handleUserConnection(socket);
  } else if (role === 'operator') {
    handleOperatorConnection(socket);
  }
});

async function handleUserConnection(socket: Socket) {
  const userId = socket.data.userId;

  // Створити або відновити сесію
  let session = await sessionRepo.findActiveByUser(userId);
  if (!session) {
    session = await sessionRepo.create({
      userId,
      status: 'waiting',
      startedAt: new Date()
    });
  }

  socket.join(`session:${session.id}`);

  if (session.status === 'waiting') {
    waitingQueue.set(session.id, session);
    socket.emit('queue:position', {
      position: waitingQueue.size,
      estimatedWait: waitingQueue.size * 2  // хвил
    });
    io.to('operators').emit('queue:updated', { count: waitingQueue.size });
  }

  socket.on('message:send', async ({ content }) => {
    const message = await messageRepo.create({
      sessionId: session.id,
      senderId: userId,
      senderRole: 'user',
      content,
      sentAt: new Date()
    });

    io.to(`session:${session.id}`).emit('message:new', message);
  });

  socket.on('disconnect', () => {
    io.to(`session:${session.id}`).emit('user:offline', { userId });
  });
}

async function handleOperatorConnection(socket: Socket) {
  socket.join('operators');

  // Оператор приймає чат з черги
  socket.on('session:accept', async ({ sessionId }) => {
    const session = waitingQueue.get(sessionId);
    if (!session) return;

    waitingQueue.delete(sessionId);
    await sessionRepo.assign(sessionId, socket.data.userId);

    socket.join(`session:${sessionId}`);

    io.to(`session:${sessionId}`).emit('operator:joined', {
      operatorId: socket.data.userId,
      operatorName: socket.data.user.name
    });

    io.to('operators').emit('queue:updated', { count: waitingQueue.size });

    // Відправити історію сообщень оператору
    const history = await messageRepo.findBySession(sessionId);
    socket.emit('session:history', history);
  });

  socket.on('message:send', async ({ sessionId, content }) => {
    const message = await messageRepo.create({
      sessionId,
      senderId: socket.data.userId,
      senderRole: 'operator',
      content
    });

    io.to(`session:${sessionId}`).emit('message:new', message);
  });

  // Закрити чат
  socket.on('session:close', async ({ sessionId, resolution }) => {
    await sessionRepo.close(sessionId, resolution);
    io.to(`session:${sessionId}`).emit('session:closed', { resolution });
  });
}

Клієнтський віджет (React)

function SupportWidget() {
  const [isOpen, setIsOpen] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);
  const [status, setStatus] = useState<'connecting' | 'waiting' | 'active' | 'closed'>('connecting');
  const [input, setInput] = useState('');
  const socketRef = useRef<Socket>();

  useEffect(() => {
    const socket = io('/support', {
      auth: { token: getGuestToken() }
    });

    socket.on('queue:position', ({ position }) => {
      setStatus('waiting');
      setMessages([{ system: true, text: `Ви в черзі. Позиція: ${position}` }]);
    });

    socket.on('operator:joined', ({ operatorName }) => {
      setStatus('active');
      setMessages(prev => [...prev, {
        system: true, text: `${operatorName} приєднався`
      }]);
    });

    socket.on('message:new', (msg) => {
      setMessages(prev => [...prev, msg]);
    });

    socketRef.current = socket;
    return () => { socket.disconnect(); };
  }, []);

  const sendMessage = () => {
    if (!input.trim()) return;
    socketRef.current?.emit('message:send', { content: input });
    setInput('');
  };

  return (
    <div className={`chat-widget ${isOpen ? 'open' : ''}`}>
      <button className="chat-toggle" onClick={() => setIsOpen(!isOpen)}>
        💬 Поддержка
      </button>

      {isOpen && (
        <div className="chat-window">
          <MessageList messages={messages} />
          {status === 'active' && (
            <div className="chat-input">
              <input value={input} onChange={e => setInput(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && sendMessage()} />
              <button onClick={sendMessage}>Відправити</button>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Повідомлення оператору

// Якщо оператор не в браузері — повідомлення через Telegram/email
async function notifyOperatorsNewChat(session: ChatSession) {
  const onlineOperators = await redis.smembers('online:operators');

  if (onlineOperators.length === 0) {
    await telegramBot.sendMessage(OPERATORS_CHAT_ID,
      `🆕 Новий запит в поддержку\nКористувач: ${session.userId}\nПершого сообщення: ${session.firstMessage}`
    );
  }
}

Часові рамки

Базовий чат з чергою та історією: 2–3 тижні. Повний: віджет + оператор UI + повідомлення + рейтинг: 4–6 тижнів.