Как Smartcalls стал Voximplant Kit’ом – ребрендинг и киллер-фичи


    Мы долго готовили обновление Smartcalls – визуального редактора для исходящих звонков – и вот оно случилось. Сегодня под катом расскажем про UI/UX-изменения и залезем под капот деморежима, чтобы показать, как мы приручали JointJS.

    А что собственно поменялось?


    Из самого очевидного – новое имя и урл, а это значит, что Voximplant Kit доступен по соответствующей ссылке voximplant.com/kit. Модифицировали и страницу регистрации, теперь она такая:

    image

    Хотя концепция осталась прежней, существенно преобразился интерфейс продукта, став более user-friendly. Верхнее меню перекочевало налево, что сделало навигацию по блокам более логичной и удобной.

    image

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

    image

    Что касается интеграций: user-friendly интерфейс добрался и до настроек почты, а на вкладках Dialogflow, SIP, Global Variables появился поиск и сортировка файлов по ID и host'ам.

    image

    В общем, много всего нового и крутого! Подробнее об изменениях можно почитать в нашем блоге.

    Но самое главное – редактор


    Деморежим (спойлер: это и есть главная киллер-фича).


    Реал-тайм выполнение сценария с подсветкой задействованных блоков, а после выполнения – результат звонка (Flow и Log), благодаря чему отладка сценариев стала еще проще и быстрее.

    image

    Посмотреть видео работы деморежима можно здесь или протестировать самостоятельно после регистрации на Voximplant Kit.

    А о том, как это все реализовано, расскажем в следующем разделе. Новые фичи редактора:

    • undo/redo (1 на рисунке ниже);
    • горячие клавиши (2);
    • всплывающее меню, где можно выровнять блоки и линки между ними одним нажатием, изменить масштаб, работать с miniMap, развернуть сценарий на весь экран, а также расшарить его (скопировать или сохранить как png) (3);
    • контекстное меню по правому клику мыши;
    • копирование блоков – не только внутри одного сценария, но и между разными сценариями и даже(!) разными аккаунтами;
    • lock/unlock блока — залоченный блок двигать можно, но НЕЛЬЗЯ редактировать во избежание нежелательных изменений;
    • смена цветов – визуально можно выделить несколько «родственных» блоков;
    • поиск по именам и содержанию используемых блоков;
    • блок «Интерактивное меню» – возможность менять порты (варианты ответов) местами простым перетаскиванием.

    image

    Раскрываем карты


    Пришло время разобраться, как в коде реализована анимация блоков.


    Редактор вызывает метод нашего HTTP API – StartScenarios – чтобы запустить облачный сценарий. Облако Voximplant начинает выполнение сценария и отдает редактору media_access_url. С этого момента редактор дергает media_access_url каждую секунду, получая в ответ информацию о том, как сценарий «путешествует» по блокам – опираясь на эти данные, редактор подсвечивает нужные блоки и анимирует связи между ними.

    История путешествий (History) представляет собой объект JSON со следующими полями:

    • timestamp;
    • idSource – начальный блок;
    • idTarget – конечный блок;
    • port – порт (может быть несколько выходов из 1 блока).

    image

    С помощью этих кастомных и служебных переменных фронтенд понимает, из какого блока в какой сценарий переходит во время тестирования. Как он это понимает? Когда происходит визуальное конструирование (добавляется новый блок), ему сразу присваивается id, который потом используется в истории как idSource / idTarget.

    Чтобы реализовать данную функциональность, мы использовали библиотеку JointJS, но не обошлось и без самописного кода.

    Начнем с главного метода selectBlock(), он работает следующим образом: мы идем по массиву истории перемещений (idSource, idTarget) и как только находим начальную и конечную точки, ищем связь между ними:

    const link = this.editor.getTestLink(sourceCell, portId);

    Если связь между ними есть, то анимируем бегающий по линии связи шарик:
    if (link) this.setLinkAnimation(link);

    Метод selectBlock() вызывается после каждого обновления this.testHistory. Так как в this.testHistory могут прилететь сразу несколько пройденных блоков, мы рекурсивно вызываем selectBlock раз в 700 мс (это примерное время, затрачиваемое на анимацию перемещения от блока к блоку):

    setTimeout(this.selectBlock, 700);

    Весь код данного метода выглядит следующим образом. Обратите внимание на методы selectTestBlock и getTestLink, строки 7 и 10 – сейчас мы расскажем про них отдельно:



    Рисуем связь


    Метод getTestLink() помогает получить связь между блоками – он основан на getConnectedLinks(), встроенном методе JointJS, который принимает на вход блок и возвращает массив линков. В нашей реализации мы ищем в полученном массиве линк с портом, где свойство source имеет значение portId:

    link = this.graph.getConnectedLinks(cell, {outbound : true}).find(item => {
         return item.get('source').port === portId;

    Затем, если линк есть, то подсвечиваем его:

    return link ? (link.toFront() && link) : null;

    Код метода:

    getTestLink(sourceCell: Cell, portId: string): Link {
      let link = null;
      if (sourceCell && sourceCell.id) {
        let cell = null;
        if (sourceCell.type === 'ScenarioStart' || sourceCell.type === 'IncomingStart') {
          cell = this.getStartCell()
        } else {
          cell = this.graph.getCell(sourceCell.id);
        }
        link = this.graph.getConnectedLinks(cell, {outbound : true}).find(item => {
          return item.get('source').port === portId;
        });
      }
      return link ? (link.toFront() && link) : null;
    }
     

    Анимация бегающего шарика реализована полностью средствами JointJS (смотреть демо).

    Перемещаемся на текущий блок


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

    const center = cell.getBBox().center();

    Затем вызываем setTestCell() для окрашивания блока:

    editor.tester.setTestCell(cell);

    Наконец, зумимся к его центру с помощью самописной функции zoomToCell() (она самая интересная, но о ней в конце):

    editor.paperController.zoomToCell(center, 1, false);

    Код метода:

    selectTestBlock(id: string): Cell {
     const cell = (id === 'ScenarioStart') ? editor.tester.getStartCell() : editor.graph.getCell(id);
     if (cell) {
       const center = cell.getBBox().center();
       editor.tester.setTestCell(cell);
       editor.paperController.zoomToCell(center, 1, false);
     }
     return cell;
    }

    Метод для окрашивания: находим SVG-элемент нашего блока и добавляем CSS-класс .is-tested, чтобы блок стал цветным:

    setTestCell(cell: Cell): void {
     const view = cell.findView(this.paper);
     if (view) view.el.classList.add('is-tested');
    }

    Плавный зум


    И наконец zoomToCell()! У JointJS есть встроенный метод для перемещения холста по осям X и Y, сначала хотели взять именно его. Однако этот метод использует transform в качестве атрибута SVG-тега, он не поддерживает плавную анимацию в браузере Firefox + задействует исключительно CPU.

    Мы сделали небольшой хак – написали свою функцию zoomToCell(), которая, по сути, делает то же самое, но прокидывает transform как инлайновый CSS, это позволяет делать рендер с помощью GPU (потому что к процессу подключается WebGL). Таким образом решается проблема кроссбраузерности.

    Наша функция не только перемещает холст по X Y, но и позволяет одновременно производить масштабирование (зум) за счет использования transform matrix.

    Свойство will-change класса .animate-viewport сообщает браузеру, что элемент будет изменен и необходимо применить оптимизации, в том числе задействовать GPU, а свойство transition задает плавность перемещения холста к блоку:

    .animate-viewport {
     will-change: transform;
     transition: transform 0.5s ease-in-out;

    Весь код нашего метода ниже:

    public zoomToCell(center: g.Point, zoom: number, offset: boolean = true): void {
       this.updateGridSize();
       const currentMatrix = this.paper.layers.getAttribute('transform');
       // Получаем новую svg-матрицу, чтобы переместить холст в точку из аргумента center
       // и деструктурируем ее, чтобы установить в атрибут style
       const { a, b, c, d, e, f } = this.zoomMatrix(zoom, center, offset);
       // Для FireFox нужно установить исходную матрицу, иначе происходит короткий рывок холста в сторону
       this.paper.layers.style.transform = currentMatrix;
       // Без первого таймаута FF пропускает то, что мы установили исходную матрицу, и снова происходит рывок
       setTimeout(() => {
         // Добавляем CSS-селектор .animate-viewport, у которого задано св-во transition;
         // Устанавливаем в атрибут style новую матрицу и вычисляем длительность св-ва transition
         this.paper.layers.classList.add('animate-viewport');
         this.paper.layers.style.transform = `matrix(${ a }, ${ b }, ${ c }, ${ d }, ${ e }, ${ f })`;
         const duration = parseFloat(getComputedStyle(this.paper.layers)['transitionDuration']) * 1000;
         // После завершения анимации удаляем селектор и атрибут style;
         // для холста устанавливаем матрицу средствами joint
         setTimeout(() => {
           this.paper.layers.classList.remove('animate-viewport');
           this.paper.layers.style.transform = null;
           this.paper.matrix(newMatrix);
           this.paper.trigger('paper:zoom');
           this.updateGridSize();
           this.paper.trigger('paper:update');
         }, duration);
       }, 100);
     }

    Как оказалось, иногда даже самые продвинутые либы надо допиливать напильником :) Надеемся, вам понравилось копаться во внутренностях кита (как бы крипово это ни звучало). Желаем успешной разработки с Voximplant Kit и не только!
    Voximplant
    Облачная платформа голосовой и видеотелефонии

    Комментарии 6

      –2
      Киллер фича — наконец-то завезли сортировку?
      Вообще похоже, от амазоновского варианта немногих отличается. Я что-то пропустил?
        0
        Если прочитать статью, то можно увидеть, как прямым текстом написано:
        Демо-режим (спойлер: это и есть главная киллер-фича).


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

          Контакт центра Амазона называется Connect
          aws.amazon.com/connect
          А голосовой «бот» — Lex
          aws.amazon.com/lex
            0
            Люблю эти «вроде», может там еще у Amazon есть распознавание от Google, Yandex или Тинькова или синтез речи от них?
              0
              Ну тут в статье про это тоже не написано, не?
              Распознавание там есть и синтез тоже. Амазоновское. Неплохое, кстати, на уровне Гугла.
              Просто как бы интерфейс практически идентичный, вплоть до меню. Так, рамочки чуть подругому и цвета.
              Кто у кого передирает — не знаю, но факт.
                0
                Ну а тут есть и Амазоновское, и от Гугла и еще много от кого, выбор это же хорошо, не так ли?

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое