Чат с анализом настроения с помощью Next.js

Пишем чат с анализом настроения с помощью Next.js

Первые шаги

Настройка сервера

Настроим простой сервер, использующий Next.js. Также загрузим необходимые middleware (промежуточные функции обработки запроса) и настроим Pusher, используя учётные данные, добавленные ранее в переменные окружения.

Создайте файл server.js в корневом каталоге приложения и добавьте в него следующий фрагмент кода для настройки сервера:

/* server.js */
  
   const cors = require('cors');
   const next = require('next');
   const Pusher = require('pusher');
   const express = require('express');
   const bodyParser = require('body-parser');
   const dotenv = require('dotenv').config();
   const Sentiment = require('sentiment');
  
   const dev = process.env.NODE_ENV !== 'production';
   const port = process.env.PORT || 3000;
  
   const app = next({ dev });
   const handler = app.getRequestHandler();
   const sentiment = new Sentiment();
  
   // Убедитесь, что ваши учётные данные правильно установлены в файле .env
   // Используем определённые переменные
   const pusher = new Pusher({
     appId: process.env.PUSHER_APP_ID,
     key: process.env.PUSHER_APP_KEY,
     secret: process.env.PUSHER_APP_SECRET,
     cluster: process.env.PUSHER_APP_CLUSTER,
     encrypted: true
   });
  
   app.prepare()
     .then(() => {
    
       const server = express();
      
       server.use(cors());
       server.use(bodyParser.json());
       server.use(bodyParser.urlencoded({ extended: true }));
      
       server.get('*', (req, res) => {
         return handler(req, res);
       });
      
       server.listen(port, err => {
         if (err) throw err;
         console.log(`> Ready on http://localhost:${port}`);
       });
      
     })
     .catch(ex => {
       console.error(ex.stack);
       process.exit(1);
     });

Изменение скриптов npm

Измените блок scripts в файле package.json так, чтобы файл выглядел следующим образом:

/* package.json */
  
   "scripts": {
     "dev": "node server.js",
     "build": "next build",
     "start": "NODE_ENV=production node server.js"
   }

Всё необходимое для создания компонентов приложения готово. Если сейчас выполнить команду npm run dev в терминале, то он запустит сервер на порте 3000, если, конечно, он доступен. Однако в браузере пока ничего не произойдёт, потому что на главной странице не создано ни одного компонента. Поэтому далее создадим компоненты приложения.

Создание index-страницы

Для работы Next.js нужно, чтобы компоненты страницы приложения находились в каталоге pages, поэтому добавим его в корневой каталог приложения и уже внутри него создадим новый файл index.js для главной страницы приложения.

Прежде чем поместить контент на главную страницу, добавим компонент Layout, который можно будет использовать на страницах приложения в качестве шаблона. Идём дальше и создаём каталог components в корне приложения. Помещаем новый файл Layout.js внутри только что созданного каталога со следующим содержимым:

/* components/Layout.js */
  
   import React, { Fragment } from 'react';
   import Head from 'next/head';
   const Layout = props => (
     <Fragment>
       <Head>
         <meta charSet="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
         <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous" />
         <title>props.pageTitle </title>
       </Head>
       {props.children}
     </Fragment>
   );
  
   export default Layout;

Здесь мы используем компонент next/head для добавления метаданных в заголовки всех страниц. Также добавляем ссылку на файл Bootstrap CDN, чтобы придать стиль приложению. Кроме всего прочего, заголовок страницы делаем динамическим и отображаем содержимое страницы используя {props.children}.

Далее добавляем следующее содержимое в файл index.js, который был создан ранее:

/* pages/index.js */
    
    import React, { Component } from 'react';
    import Layout from '../components/Layout';
    
    class IndexPage extends Component {
    
      state = { user: null }
      
      handleKeyUp = evt => {
        if (evt.keyCode === 13) {
          const user =  evt.target.value;
          this.setState({ user });
        }
      }
      
      render() {
        const { user } = this.state;
        
        const nameInputStyles = {
          background: 'transparent',
          color: '#999',
          border: 0,
          borderBottom: '1px solid #666',
          borderRadius: 0,
          fontSize: '3rem',
          fontWeight: 500,
          boxShadow: 'none !important'
        };
        
        return (
          <Layout pageTitle="Realtime Chat">
          
            <main className="container-fluid position-absolute h-100 bg-dark">
            
              <div className="row position-absolute w-100 h-100">
              
                <section className="col-md-8 d-flex flex-row flex-wrap align-items-center align-content-center px-5">
                  <div className="px-5 mx-5">
                  
                    <span className="d-block w-100 h1 text-light" style={{marginTop: -50}}>
                      {
                        user
                          ? (<span>
                              <span style={{color: '#999'}}>Hello!</span> {user}
                            </span>)
                          : `What is your name?`
                      }
                    </span>
                    
                    { !user && <input type="text" className="form-control mt-3 px-3 py-2" onKeyUp={this.handleKeyUp} autoComplete="off" style={nameInputStyles} /> }
                    
                  </div>
                </section>
                
                <section className="col-md-4 position-relative d-flex flex-wrap h-100 align-items-start align-content-between bg-white px-0"></section>
                
              </div>
              
            </main>
            
          </Layout>
        );
      }
      
    }
    
    export default () => (
      <IndexPage />
    );

Здесь для главной страницы приложения создаётся компонент IndexPage. Состояние компонента инициализируется с помощью пустого свойства user. Свойство user предназначено для хранения имени текущего активного пользователя.

Также здесь добавляется поле ввода для получения имени пользователя, если в данный момент оно не задано. Как только поле заполнено, имя сохраняется в свойстве user по нажатию enter или return.

Если сейчас запустить приложение в браузере, можно увидеть следующее:

Создание компонента Chat

Добавим компонент Chat. Для этого создаём новый файл Chat.js в каталоге components и добавляем следующее содержимое:

/* components/Chat.js */
  
   import React, { Component, Fragment } from 'react';
   import axios from 'axios';
   import Pusher from 'pusher-js';
  
   class Chat extends Component {
  
     state = { chats: [] }
    
     componentDidMount() {
    
       this.pusher = new Pusher(process.env.PUSHER_APP_KEY, {
         cluster: process.env.PUSHER_APP_CLUSTER,
         encrypted: true
       });
      
       this.channel = this.pusher.subscribe('chat-room');
      
       this.channel.bind('new-message', ({ chat = null }) => {
         const { chats } = this.state;
         chat && chats.push(chat);
         this.setState({ chats });
       });
      
       this.pusher.connection.bind('connected', () => {
         axios.post('/messages')
           .then(response => {
             const chats = response.data.messages;
             this.setState({ chats });
           });
       });
      
     }
    
     componentWillUnmount() {
       this.pusher.disconnect();
     }
    
   }
  
   export default Chat;

Давайте разберёмся, что было сделано выше:

  1. Сначала инициализируется состояние компонента, чтобы оно вмещало пустой массив chats. Этот массив будет заполняться сообщениями по мере их поступления. Как только компонент установлен, внутри метода componentDidMount() устанавливается соединение с Pusher.
  2. Присоединяемся к Pusher-каналу с именем chat-room. Затем подписываемся на событие new-message, которое запускается при поступлении нового сообщения в чат. После просто заполняем chats, добавляя новый чат.
  3. В том же методе componentDidMount() подписываемся на событие connected, чтобы извлечь все сообщения чата из истории с помощью HTTP-запроса POST/messages и библиотеки axios, как только пользователь присоединился. После этого заполняем chats сообщениями чата, полученными в ответ на запрос.
  4. Компонент Chat ещё не завершён. Всё ещё необходимо добавить метод render(). Чтобы это сделать, добавьте следующий код в класс компонента Chat:
    /* components/Chat.js */

    handleKeyUp = evt => {
      const value = evt.target.value;

      if (evt.keyCode === 13 && !evt.shiftKey) {
        const { activeUser: user } = this.props;
        const chat = { user, message: value, timestamp: +new Date };

        evt.target.value = '';
        axios.post('/message', chat);
      }
    }

    render() {
      return (this.props.activeUser && <Fragment>

        <div className="border-bottom border-gray w-100 d-flex align-items-center bg-white" style={{ height: 90 }}>
          <h2 className="text-dark mb-0 mx-4 px-2">{this.props.activeUser}</h2>
        </div>

        <div className="border-top border-gray w-100 px-4 d-flex align-items-center bg-light" style={{ minHeight: 90 }}>
          <textarea className="form-control px-3 py-2" onKeyUp={this.handleKeyUp} placeholder="Enter a chat message" style={{ resize: 'none' }}></textarea>
        </div>

      </Fragment> )
    } 

Как видно из метода render(), свойство activeUser требуется для идентификации текущего активного пользователя. Также присутствует элемент <textarea>, который необходим для ввода сообщения. К этому элементу добавлен обработчик событий onKeyUp для отправки сообщения в чат при нажатии клавиш enter или return.

В обработчике handleKeyUp() создаётся объект chat, содержащий user (текущий активный пользователь), message (само сообщение) и timestamp (время отправки сообщения). После нужно очистить поле <textarea> и выполнить HTTP-запрос POST/message, передавая объект chat.

Теперь необходимо добавить компонент Chat на главную страницу. Сначала добавьте следующую строку к операторам import в файле pages/index.js.

/* pages/index.js */
  
   // Другие установки import здесь
   import Chat from '../components/Chat';

Затем установите метод render() компонента IndexPage. Вставьте компонент Chat в пустой элемент <section>. Он должен выглядеть следующим образом:

    /* pages/index.js */

    <section className="col-md-4 position-relative d-flex flex-wrap h-100 align-items-start align-content-between bg-white px-0">
      { user && <Chat activeUser={user} /> }
    </section>

Теперь можно перезагрузить приложение в браузере, чтобы увидеть изменения.

Определение маршрутов чата

На данный момент при попытках отправить сообщение в чат ничего не происходит. Пока что нельзя увидеть ни сообщения, ни историю чата. Это потому, что ещё не реализованы два маршрута, к которым мы делаем запросы.

Создадим маршруты /message и /messages. Далее изменим файл server.js и добавим следующий код непосредственно перед вызовом server.listen() внутри функции then().

/* server.js */
  
   // server.get('*') находится здесь
  
   const chatHistory = { messages: [] };
  
   server.post('/message', (req, res, next) => {
     const { user = null, message = '', timestamp = +new Date } = req.body;
     const sentimentScore = sentiment.analyze(message).score;
    
     const chat = { user, message, timestamp, sentiment: sentimentScore };
    
     chatHistory.messages.push(chat);
     pusher.trigger('chat-room', 'new-message', { chat });
   });
  
   server.post('/messages', (req, res, next) => {
     res.json({ ...chatHistory, status: 'success' });
   });
  
   // server.listen() находится здесь

Здесь создаётся своего рода хранилище для истории чата, чтобы хранить сообщения в массиве. Это полезно для новых пользователей, которые присоединяются к чату для просмотра предыдущих сообщений. Всякий раз, когда клиент отправляет POST-запрос к /messages при подключении, он получает все сообщения в истории чата при ответе.

В запросе POST/message приходит выборка данных из req.body с помощью инструмента body-parser, который мы добавили ранее. Затем используется модуль sentiment, чтобы вычислить общую оценку настроения в сообщении. Далее изменяем объект chat, добавив в него свойство sentiment, содержащее оценку настроения.

Наконец, добавляем чат к истории чата в messages, а затем запускаем событие new-message в канале chat-room, передавая объект chat в событие.

Осталось всего несколько шагов, и приложение будет готово. Если перезагрузить приложение в браузере сейчас и попытаться отправить сообщение в чат, оно не будет отображено. Это не потому, что приложение не работает, оно работает отлично. Просто ещё не происходит вывод сообщений чата на экран.

Отображение сообщений чата

Создайте новый файл ChatMessage.js в каталоге components и добавьте в него следующее содержимое:

    /* components/ChatMessage.js */

    import React, { Component } from 'react';

    class ChatMessage extends Component {

      render() {
        const { position = 'left', message } = this.props;
        const isRight = position.toLowerCase() === 'right';

        const align = isRight ? 'text-right' : 'text-left';
        const justify = isRight ? 'justify-content-end' : 'justify-content-start';

        const messageBoxStyles = {
          maxWidth: '70%',
          flexGrow: 0
        };

        const messageStyles = {
          fontWeight: 500,
          lineHeight: 1.4,
          whiteSpace: 'pre-wrap'
        };

        return <div className={`w-100 my-1 d-flex ${justify}`}>
          <div className="bg-light rounded border border-gray p-2" style={messageBoxStyles}>
            <span className={`d-block text-secondary ${align}`} style={messageStyles}>
              {message}
            </span>
          </div>
        </div>
      }

    }

    export default ChatMessage;

Компонент ChatMessage — простой компонент, требующий 2 свойства: message — сообщение чата, и position  — положение сообщения справа или слева. Это полезно для размещения сообщений текущего пользователя на одной стороне экрана, а сообщений других пользователей — на другой.

Внесём следующие изменения в компонент Chat для отображения сообщений. Для этого изменим файл components/Chat.js.

Сначала добавим следующие константы перед определением класса компонента Chat. Каждая константа представляет собой массив кодировок, обозначающих конкретный смайл Эмоджи. Также нужно убедиться, что компонент ChatMessage импортирован.

/* components/Chat.js */
  
   // Модуль включается здесь
   import ChatMessage from './ChatMessage';
  
   const SAD_EMOJI = [55357, 56864];
   const HAPPY_EMOJI = [55357, 56832];
   const NEUTRAL_EMOJI = [55357, 56848];
  
   // Класс компонента Chat здесь

Далее следует добавить следующий фрагмент кода между контейнером <div> заголовка чата и контейнером <div> окна сообщений, которое было создано ранее в компоненте Chat.

/* components/Chat.js */

    {/** ЗАГОЛОВОК ЧАТА ЗДЕСЬ **/}

    <div className="px-4 pb-4 w-100 d-flex flex-row flex-wrap align-items-start align-content-start position-relative" style={{ height: 'calc(100% - 180px)', overflowY: 'scroll' }}>

      {this.state.chats.map((chat, index) => {

        const previous = Math.max(0, index - 1);
        const previousChat = this.state.chats[previous];
        const position = chat.user === this.props.activeUser ? "right" : "left";

        const isFirst = previous === index;
        const inSequence = chat.user === previousChat.user;
        const hasDelay = Math.ceil((chat.timestamp - previousChat.timestamp) / (1000 * 60)) > 1;

        const mood = chat.sentiment > 0 ? HAPPY_EMOJI : (chat.sentiment === 0 ? NEUTRAL_EMOJI : SAD_EMOJI);

        return (
          <Fragment key={index}>

            { (isFirst || !inSequence || hasDelay) && (
              <div className={`d-block w-100 font-weight-bold text-dark mt-4 pb-1 px-1 text-${position}`} style={{ fontSize: '0.9rem' }}>
                <span className="d-block" style={{ fontSize: '1.6rem' }}>
                  {String.fromCodePoint(...mood)}
                </span>
                <span>chat.user </span>
              </div>
            ) }

            <ChatMessage message={chat.message} position={position} />

          </Fragment>
        );

      })}

    </div>

    {/** ОКНО СООБЩЕНИЯ ЧАТА ЗДЕСЬ **/}

Попробуем разобраться, что делает этот фрагмент кода. Сначала выполняется проход по каждому объекту chat в массиве chats. Затем выполняется проверка, совпадает ли отправитель сообщения с текущим пользователем, и определяется позиция сообщения в чате. Сообщения активного пользователя появляются справа.

Здесь также выполняется оценка sentiment, чтобы установить настроение пользователя (весёлое, грустное или нейтральное) с использованием ранее определённых констант.

Имя пользователя отображается перед сообщением в чате на основе одного из следующих условий:

  1. isFirst — текущее сообщение является первым в списке.
  2. !inSequence — текущее сообщение следует непосредственно за сообщением другого пользователя.
  3. hasDelay — текущее сообщение имеет задержку более 1 минуты от предыдущего сообщения того же пользователя.
  4. Также обратите внимание, как используется метод , чтобы получить смайлики из кодировок, которые были определены в константах ранее.

Наконец, приложение чата завершено. Теперь можно проверить, как оно работает в браузере. Вот несколько скриншотов, показывающих чат между пользователями 9lad, Steve и Bob.

9lad

Steve

Bob

Заключение

Это руководство помогает создать очень простое приложение чата с эмоциональным анализом с помощью модулей (), и . Несмотря на то, что здесь затрагиваются лишь основы, есть множество инструментов, которые помогут вам создать более продвинутое приложение чата. Исходный код этого руководства можно найти на .

Также вы можете изучить документацию для каждого инструмента, который был использован в этом проекте, чтобы узнать больше о других способах реализации таких приложений.

Вы можете пропустить чтение записи и оставить комментарий. Размещение ссылок запрещено.

Оставить комментарий

Вы должны быть авторизованы, чтобы разместить комментарий.