Делаем электронного консультанта из чата Post Hawk

d1e574b27da04de3baba05cf6bef36df.png

Привет, хабр!
Недавно вышла новая версия api Post Hawk и чат основанный на нём. Сегодня хочу показать как можно за минимальное количество времени трансформировать этот чат в электронного консультанта с простенькой панелью управления.
Итак, приступим.

Для самых нетерпеливых репозиторий с тем, что получится в конце и возможность «пощупать руками» — клиент, панель администрирования. После клонирования необходимо инициализировать подмодули:

git clone https://bitbucket.org/Slavenin/hawk_advisor
git submodule init && git submodule update

Наш консультант будет состоять из двух частей клиентской и административной.

Клиентская часть


Код клиентской части расположился в двух файлах: index.php и js/client.js. Первый не представляет большого интереса, в нём происходит подключение скриптов и установка основных свойств. В примере используется глобальный объект CHAT_CONTROL для хранения свойств текущего пользователя. Вы можете выбрать любой другой способ, какой вам больше нравится, например, через шаблонизацию. В качестве id пользователя используется md5 hash сессии, опять же, вы вольны использовать в качестве id, что угодно, главное, чтобы это удовлетворяло параметрам идентификатора: /^[a-zA-Z\d\_]{3,64}$/ (в будущем планируется уйти от этого ограничения, но пока так). Id менеджера (ключ CHAT_CONTROL.manager), который будет общаться с вашими клиентами также должен удовлетворять указанным выше параметрам.

Инициализация чата выглядит следующим образом:

код
$('#chat').hawkChat({
                userId: CHAT_CONTROL.userId, //id пользователя
                serverSideUrl: 'Chat.php', //адрес серверного скрипта
                groupName: CHAT_CONTROL.groupName, //группа куда будут добавлены все пользователи
                useTabs: false, //скрываем табы
                useUserList: false, //скрываем список пользователей
                inline: false, //делаем чат перетаскиваемым
                title: 'Задайте нам вопрос', //заголовок чата
                inMessageFormat: '<div class="chat-row triangle-right left" title="{time}"> \
                        <div class=""> \
                                <span class="chat-row-login">{from_login}</span>: \
                                <span class="chat-row-message">{message}</span> \
                        </div> \
                </div>', //меняем формат входящего сообщения
                outMessageFormat: '<div class="chat-row triangle-right right" title="{time}"> \
                        <div class=""> \
                                <span class="login">Вы</span>: \
                                <span class="message">{message}</span> \
                        </div> \
                </div>', //меняем формат исходящего сообщения
                openWithUser: [CHAT_CONTROL.manager], //открываем вкладку с менеджером
                onInMessage: function(msg, str) {
                        if(msg.event === 'hawk.chat_message')
                        {
                                var $body = $('.chat-mesasge-panel.active');
                                if($body.find('.mCSB_container').size())
                                {
                                        $body = $body.find('.mCSB_container');
                                }
                                $body.append(str);
                        }
                } //добавляем обработчик входящего сообщения из панели администрирования
        });



Полный набор возможных параметров находится на соответствующей странице документации.

Коротко о том, что происходит при инициализации чата: мы меняем формат входящих и исходящих сообщений, прячем список пользователей и вкладки, открываем чат с назначенным ранее менеджером, а также слушаем входящие сообщения, чтобы при поступлении сообщения для всех пользователей группы отобразить его. Делаем чат перетаскиваемым, для этого нам необходимо наличие jquery ui.

Делаем анимацию сворачивания/разворачивания чата:

код
var $body = $('.chat-body', '#chat');
        $('.chat-header', '#chat').dblclick(function () {
                var $container = $('.chat-container', '#chat');
                if($body.is(":visible"))
                {
                        $body.hide();
                        $container.stop().animate({
                                height: '0px'
                        }, 1000);
                }
                else
                {
                        var to = '+=0px';
                        $container.stop().animate({
                                height: '465px',
                                top: to
                        }, 1000, 'linear', function () {
                                $body.show();
                                //если чат уехал за экран возвращаем его на место
                                var offset = $container.offset();
                                if(offset.top < 0)
                                {
                                        $container.stop().animate({
                                                top: '+=' + offset.top|0 + 'px'
                                        })
                                }
                                $container.find('.chat-text-input').focus();
                        });
                }

        });



Сообщаем о себе вызовом функции sendPage() и ставим её вызов в интервал на каждые 15 секунд.
Вот и всё с клиентской частью.

Административная часть.


Для начала определимся с тем минимальным набором функционала которым должна располагать административная часть:

  1. Отправить сообщение любому пользователю
  2. Отправить сообщение всем пользователям
  3. Показывать статистику по текущим онлайн-пользователям


Для реализации первых двух пунктов будем использовать плагин чата, для третьего — highcharts и Яндекс.Карты.

Административная часть расположена в файлах admin.php и js/admin.js. Как и в случае с клиентской частью файл admin.php не содержит в себе каких-то интересных деталей, всё тот же объект для данных пользователя и немного html-разметки под графики. Понятно, что в таком виде как она сейчас — открытом — оставлять её нельзя, но так как каждый сайт использует собственную систему авторизации добавлять какой-либо код для её проверки я не стал.
Переходим к js-скрипту. Для хранения данных статистики используется объект STAT.

код
$('#chat').hawkChat({
                userId: CHAT_CONTROL.userId, //id пользователя
                serverSideUrl: 'Chat.php', //адрес серверного скрипта
                groupName: CHAT_CONTROL.groupName, //группа куда будут добавлены все пользователи
                onInMessage: function(msg, str) {
                        if(msg.event === 'hawk.ping')
                        {
                                return false;
                        }

                        msg.text.from_login = msg.text.from_login.substr(0, 10);

                        return -1;
                }//добавляем обработчик входящего сообщения для модификации логина пользователя
        });



Здесь стандартная инициализация чата. На входящие сообщения делаем обработчик дабы не пропустить данные из админ панели и укоротить логин пользователя. -1 — это сигнал чату перекомпилировать сообщения на основе изменённых данных. Так как сообщения с пингом рассылаются всем пользователям группы, то мы их игнорируем. В данной части код можно поменять создав две разные группы — одну для пользователей, вторую — для администраторов и посылая сообщения из одной в другую.
Далее мы инициализируем графики и подписываемся на события пинга и обновление списка пользователей. Новый список запрашивается чатом каждые 30 секунд.

код
//инициализируем графики
    initGraphs();

        //подписываемся на пинг от пользователя
        HAWK_API.bind_handler('ping', onUserPing);
        //подписываемся на обновление списка пользователей
        HAWK_API.bind_handler('get_by_group', onUserList);



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

код
function onUserPing(e, msg)
{
        //сохраняем или обновляем информацию о пользователе
        var info = msg.text.info;
        STAT.userInfo[msg.from] = info;

        //записываем текущую страницу пользователя
        if(!STAT.pageToUser.hasOwnProperty(info.page))
        {
                STAT.pageToUser[info.page] = [];
        }

        //и добавляем его в массив пользователей страницы
        if($.inArray(msg.from, STAT.pageToUser[info.page]) === -1)
        {
                STAT.pageToUser[info.page].push(msg.from);
        }
}

function onUserList(e, msg)
{
        var onlineUsers = [];
        msg.result.forEach(function (record) {
                for(var gname in record)
                {
                        record[gname].users.forEach(function (user) {
                                if (user.online)
                                {
                                        if($.inArray(user.user, onlineUsers) === -1)
                                        {
                                                onlineUsers.push(user.user);
                                        }
                                }
                                else if (!user.online)
                                {
                                        if (STAT.userInfo.hasOwnProperty(user.user))
                                        {
                                                delete STAT.userInfo[user.user];
                                        }
                                        //убираем оффлайн пользователей из группы
                                        HAWK_API.remove_user_from_group([gname], user.user);
                                }
                        });
                }
        });

        var pages = STAT.pageToUser;
        //перебираем циклом все имеющиеся страницы
        for(var page in pages)
        {
                //фильтруем пользователей на странице
                pages[page] = pages[page].filter(function (pUser) {
                        //если пользователя нет на странице, удаляем его
                        if($.inArray(pUser, onlineUsers) === -1 || STAT.userInfo[pUser].page !== page)
                                return false;

                        return true;
                });

                //если пользователей на странице не осталось удаляем страницу
                if(!pages[page].length)
                {
                        delete pages[page];
                }
        }

        $('#user_count').html(onlineUsers.length);

        //обновляем графики и карту
        updatePageGraph();
        updateBrowserGraph();
        updateMap();
}



Далее функции для обновления данных на графиках (какого-либо особого интереса они не представляют, поэтому подробно их рассматривать не буду):

код
function updatePageGraph()
{
        //если процесс обновления уже запущен возвращаемся
        if(STAT.graphPageUpdating)
        {
                return;
        }

        STAT.graphPageUpdating = true;

        //формируем серии для графика
        var series = [];
        var pages = STAT.pageToUser;
        for(var page in pages)
        {
                series.push([page, pages[page].length]);
        }

        //обновляем графики
        var chart = STAT.pageGraph.highcharts();
        chart.series[0].setData(series);
        chart.redraw();

        STAT.graphPageUpdating = false;
}

function updateBrowserGraph()
{
        //если процесс обновления уже запущен возвращаемся
        if(STAT.graphBrowserUpdating)
        {
                return;
        }

        STAT.graphBrowserUpdating = true;

        var browsers = {};
        var users = STAT.userInfo;

        //перебираем информацию пользователей
        for(var user in users)
        {
                var browser = users[user].browser;
                if(!browser)
                {
                        continue;
                }

                //формируем строку с названием браузера
                browser = browser.name.toString().substr(0, 10) + ' '
                                + ((browser.version) ? ' (' + browser.version + ')' : '(не определено)');
                //считаем количество браузеров
                if(!browsers.hasOwnProperty(browser))
                {
                        browsers[browser] = 0;
                }

                browsers[browser]++;
        }

        //формируем сирии для графика
        var series = [];
        for(var browser in browsers)
        {
                series.push([browser, browsers[browser]]);
        }

        //обновляем график
        var chart = STAT.browserGraph.highcharts();
        chart.series[0].setData(series);
        chart.redraw();

        STAT.graphBrowserUpdating = false;
}

function updateMap()
{
        if(!STAT.objectManager)
        {
                return;
        }
        //очищаем карту
        STAT.objectManager.removeAll();
        var users = STAT.userInfo;
        //формируем новый массив координат пользователей
        var points = [];
        for(var user in users)
        {
                if(users[user].coords && users[user].coords.length)
                {
                        points.push({
                                type: "Feature",
                                id: user,
                                geometry: {
                                        type: "Point",
                                        coordinates: users[user].coords
                                },
                                properties: {
                                        draggable: false
                                }
                        });

                }
        }

        //добавляем на карту
        STAT.objectManager.add(points)
}



и функции инициализации графиков и карт:

код
function initGraphs()
{
        //график пользователей
        STAT.pageGraph = $('#chart').highcharts({
        chart: {
            type: 'column'
        },
        title: {
            text: 'Пользователи на страницах'
        },
        xAxis: {
            type: 'category'
        },
        yAxis: {
            title: {
                text: 'Количество пользователей'
            }

        },
        legend: {
            enabled: false
        },
        plotOptions: {
            series: {
                borderWidth: 0,
                dataLabels: {
                    enabled: true,
                    format: '{point.y}'
                }
            }
        },

        tooltip: {
            headerFormat: '<span style="font-size:11px">{series.name}</span><br>',
            pointFormat: '<span style="color:{point.color}">{point.name}</span>: <b>{point.y}</b><br/>'
        },

        series: [{
                        name: "Страницы",
                        colorByPoint: true,
                        data: []
                }]
    });
        //график браузеров
        STAT.browserGraph = $('#browsers').highcharts({
                chart: {
            type: 'pie'
        },
        title: {
            text: 'Браузеры'
        },
        tooltip: {
            pointFormat: '{series.name} <b>{point.percentage:.1f}%</b>'
        },
        plotOptions: {
            pie: {
                allowPointSelect: true,
                cursor: 'pointer',
                dataLabels: {
                    enabled: true,
                    format: '<b>{point.name}</b> {point.percentage:.1f} %',
                    style: {
                        color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black'
                    }
                }
            }
        },
        series: [{
            name: "Браузеры",
            colorByPoint: true,
            data: []
        }]
        });
}
//подписываемся на инициализацию карты
ymaps.ready(initMap);

function initMap () {
        //создаём карту
    var myMap = new ymaps.Map("map", {
            center: [55.76, 37.64],
            zoom: 2,
                        controls: ['zoomControl', 'searchControl', 'typeSelector']
        }, {
            searchControlProvider: 'yandex#search'
        });

        //создаём менеджер объектов
        STAT.objectManager = new ymaps.ObjectManager({
                // Чтобы метки начали кластеризоваться, выставляем опцию.
                clusterize: true,
                // ObjectManager принимает те же опции, что и кластеризатор.
                gridSize: 32
        });

        myMap.geoObjects.add(STAT.objectManager);
}




Вот и всё с административной частью.

Для стилизации скроллов используется плагин jquery-custom-content-scroller он является опциональным. Если у вас на портале используется своя стилизация для скроллов вы можете переопределить соответствующую функцию.

Недостатком этой системы является невозможность просмотра статистики по всем пользователям сразу после загрузки страницы. Впрочем эта проблема решается несколькими способами, либо хранением статистики в чём-то более надёжном чем объект javascript, либо можно реализовать функционал опроса всех пользователей из списка с целью получения от них данных о текущем состоянии.

Вот и всё, что я хотел рассказать. Благодарю за внимание.

© Habrahabr.ru