How Smartcalls became Voximplant Kit - rebranding and killer features


For a long time, we were preparing an update to Smartcalls, a visual editor for outgoing calls, and now it happened. Today, under the cut, we’ll talk about UI / UX changes and climb under the hood of the demo mode to show how we tamed JointJS .

What has actually changed?


From the most obvious - a new name and URL, which means that the Voximplant Kit is available at the appropriate link voximplant.com/kit . We also modified the registration page , now it is like this:

image

Although the concept has remained the same, the product interface has significantly changed, becoming more user-friendly. The top menu migrated to the left, which made navigating the blocks more logical and convenient.

image

In addition, grouping and sorting of scripts and audio recordings, search by numbers, as well as campaign cards with brief information about them, including new indicators - the average duration of a successful call and the total amount spent, are now available.

image

As for the integration: the user-friendly interface got to the mail settings, and on the Dialogflow, SIP, Global Variables tabs, a search and sorting of files by ID and host appeared.

image

In general, a lot of new and cool! Read more about the changes in our blog .

But the most important thing is the editor


Demo mode (spoiler: this is the main killer feature).


Real-time execution of a script with highlighting of the involved blocks, and after execution - the result of a call (Flow and Log), which makes debugging scripts even easier and faster.

image

You can watch the video of the demo mode here or test it yourself after registering with the Voximplant Kit .

And how this is all implemented, we will tell in the next section. New features of the editor:

  • undo / redo (1 in the image below);
  • hot keys (2);
  • pop-up menu where you can align the blocks and links between them with one click, change the scale, work with miniMap, expand the script to full screen, and also share it (copy or save as png) (3);
  • right-click context menu;
  • copying blocks - not only within the same script, but also between different scripts and even (!) different accounts;
  • lock / unlock block - a locked block can be moved, but it is NOT possible to edit to avoid unwanted changes;
  • color change - visually you can select several "related" blocks;
  • search by names and contents of used blocks;
  • “Interactive menu” block - the ability to swap ports (answer options) in places by simply dragging and dropping.

image

We reveal the cards


It's time to figure out how block animation is implemented in the code.


The editor calls our HTTP API method - StartScenarios - to run the cloud script. The Voximplant cloud starts the script and gives it to the media_access_url editor. From this moment, the editor pulls media_access_url every second, receiving information on how the script “travels” through the blocks in response - based on this data, the editor highlights the necessary blocks and animates the connections between them.

History is a JSON object with the following fields:

  • timestamp;
  • idSource - initial block;
  • idTarget - final block;
  • port - port (there may be several outputs from 1 block).

image

With the help of these custom and service variables, the front-end understands which block it passes to during which testing. How does he understand this? When visual construction occurs (a new block is added), it is immediately assigned an id, which is then used in history as idSource / idTarget.

To implement this functionality, we used the JointJS library, but there were some self-written code.

Let's start with the main selectBlock () method, it works as follows: we go through the array of movement history (idSource, idTarget) and as soon as we find the start and end points, look for the connection between them:

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

If there is a connection between them, then we animate a ball running along the communication line:
if (link) this.setLinkAnimation(link);

The selectBlock () method is called after each update of this.testHistory. Since several blocks passed by can arrive in this.testHistory at once, we recursively call selectBlock once every 700 ms (this is the approximate time spent on animating the movement from block to block):

setTimeout(this.selectBlock, 700);

All code for this method is as follows. Pay attention to the selectTestBlock and getTestLink methods, lines 7 and 10 - now we will talk about them separately:

selectBlock ( ) : void {
if ( this . historyIndex < this . testHistory . length ) {
const i = this . historyIndex ;
const targetCellId = this.testHistory[i].idTarget;
const sourceCellId = this.testHistory[i].idSource;
const portId = this.testHistory[i].port;
const targetCell = this.editor.selectTestBlock(targetCellId);
const sourceCell = this.editor.getCell(sourceCellId);
if (sourceCell && targetCell) {
const link = this.editor.getTestLink(sourceCell, portId);
if (link) this.setLinkAnimation(link);
}
this.historyIndex += 1;
setTimeout ( this . selectBlock , 700 ) ;
}
}
view raw selectBlock.js hosted with ❤ by GitHub


Draw a connection


The getTestLink () method helps to get the connection between the blocks - it is based on getConnectedLinks (), a built-in JointJS method that takes a block input and returns an array of links. In our implementation, we are looking in the resulting array for a link with a port, where the source property has the value portId:

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

Then, if there is a link, then highlight it:

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

Method Code:

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;
}
 

Animation of a running ball is implemented completely by means of JointJS ( see demo ).

We move to the current block


We call the selectTestBlock () method when it is necessary to select the final block and move the canvas to it. Here we get the coordinates of the center of the block:

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

Then call setTestCell () to color the block:

editor.tester.setTestCell(cell);

Finally, we zoomed to its center using the self-written zoomToCell () function (it is the most interesting, but about it at the end):

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

Method Code:

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;
}

Method for coloring: find the SVG element of our block and add the CSS class .is-tested to make the block color:

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

Smooth zoom


Finally, zoomToCell ()! JointJS has a built-in method for moving the canvas along the X and Y axes, at first they wanted to take it. However, this method uses transform as an attribute of the SVG tag, it does not support smooth animation in the Firefox + browser and only uses the CPU.

We made a small hack - we wrote our function zoomToCell (), which, in essence, does the same thing, but casts transform as inline CSS, this allows us to render using the GPU (because WebGL is connected to the process). Thus, the problem of cross-browser compatibility is solved.

Our function not only moves the canvas in XY, but also allows us to simultaneously scale (zoom) through the use of transform matrix.

The will-change property of the .animate-viewport class tells the browser that the element will be changed and optimizations must be applied, including the use of the GPU, and the transition property sets the smoothness of moving the canvas to the block:

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

All our method code is below:

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);
 }

As it turned out, sometimes even the most advanced ones need to be finished with a file :) We hope you enjoyed digging into the inside of the whale (no matter how creepy it sounds). We wish you successful development with the Voximplant Kit and more!

All Articles