All Components

Using with Google DialogFlow

The Chat provides integration options for connecting the component to chat bots which are built with Google DialogFlow.

The following example demonstrates how to connect the Chat to a sample DialogFlow Agent.

import { Component } from '@angular/core';

import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { switchMap } from 'rxjs/operators/switchMap';
import { map } from 'rxjs/operators/map';
import { windowCount } from 'rxjs/operators/windowCount';
import { scan } from 'rxjs/operators/scan';
import { take } from 'rxjs/operators/take';
import { tap } from 'rxjs/operators/tap';
import { from } from 'rxjs/observable/from';
import { merge } from 'rxjs/observable/merge';

import { Message, User, SendMessageEvent } from '@progress/kendo-angular-conversational-ui';

import { Agent } from './agent';

@Component({
  selector: 'my-app',
  template: `
    <kendo-chat
      [messages]="feed | async"
      [user]="user"
      (sendMessage)="sendMessage($event)"
    >
      <ng-template kendoChatAttachmentTemplate let-att>
        <ng-container [ngSwitch]="att.type">

          <quote-card *ngSwitchCase="'quote'"
            [quote]="att">
          </quote-card>

          <payment-plan-card *ngSwitchCase="'payment_plan'"
            [plan]="att">
          </payment-plan-card>

          <kendo-chat-hero-card *ngSwitchDefault
            imageUrl="{{ att.images[0]?.url }}"
            [title]="att.title"
            [subtitle]="att.subtitle"
            [actions]="att.buttons"
            (executeAction)="heroAction($event)"
          >
          </kendo-chat-hero-card>

        </ng-container>
      </ng-template>
    </kendo-chat>
  `
})
export class AppComponent {
  public feed: Observable<Message[]>;

  public readonly user: User = {
    id: 1
  };

  public readonly bot: User = {
    id: 0,
    name: 'Bobby McBot',
    avatarUrl: 'https://demos.telerik.com/kendo-ui/content/chat/avatar.png'
  };

  private agent: Agent = new Agent(this.bot);
  private local: Subject<Message> = new Subject<Message>();

  constructor() {
    // Merge local and remote messages into a single stream
    this.feed = merge(
      this.local,
      this.agent.responses
    ).pipe(
      // ... and emit an array of all messages
      scan((acc, x) => [...acc, x], [])
    );
  }

  public sendMessage(e: SendMessageEvent): void {
    this.send(e.message);
  }

  public heroAction(button: any) {
    if (button.type === 'postBack') {
      const message = {
        author: this.user,
        text: button.value
      };

      this.send(message);
    }
  }

  private send(message: Message) {
    this.local.next(message);
    this.local.next({
      author: this.bot,
      typing: true
    });
    this.agent.submit(message.text);
  }
}
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { from } from 'rxjs/observable/from';

import { ApiAiClient } from 'api-ai-javascript';
import { Message, User } from '@progress/kendo-angular-conversational-ui';

// See
// https://dialogflow.com/docs/reference/agent/message-objects
const enum MessageType {
  PlainText = 0,
  QuickReply = 2
}

const mapActions = (data: any) => (data || []).map(action => {
  if (action.type === 'postBack') {
    action.type = 'reply';
  }

  return action;
});

const mapReplies = (msg: any) => (msg.replies || []).map(reply =>
  ({ type: 'reply', value: reply })
);

export class Agent {
  public readonly responses: Subject<Message> = new Subject<Message>();
  private client: ApiAiClient;

  constructor(private user: User) {
    this.client = new ApiAiClient({
      accessToken: 'e96bbf18681e4c5bb2dce69deaec5ddb'
    });

    const req = this.client.eventRequest('Welcome');
    from(req).subscribe(data => this.onResponse(data));
  }

  public submit(question: string): void {
    const req = this.client.textRequest(question);
    from(req).subscribe(data => this.onResponse(data));
  }

  private onResponse(response: any): void {
    // See
    // https://dialogflow.com/docs/fulfillment
    const ff = response.result.fulfillment;
    if (!ff) {
      return;
    }

    const timestamp = new Date(response.timestamp);
    let messages: Message[] = [{
      author: this.user,
      timestamp
    }];

    if (ff.messages.length > 0) {
      // Extract plain text messages
      messages = ff.messages
        .filter(msg => msg.type === MessageType.PlainText)
        .map(msg => (
          {
            author: this.user,
            text: msg.speech,
            timestamp
          }
      ));
    }

    const lastMessage = messages[messages.length - 1];
    const suggestedActions = [];

    // Extract quick replies which are a type of message in DialogFlow V1 API
    ff.messages
      .filter(msg => msg.type === MessageType.QuickReply)
      .forEach(msg =>
        suggestedActions.push(...mapReplies(msg))
      );

    // Our webhook sends attachments and quick replies in "data".
    // See https://dialogflow.com/docs/fulfillment#response
    if (ff.data && ff.data.null) {
      // The webhook response is, oddly enough, stored in a "null" object.
      const payload = ff.data.null;
      lastMessage.attachments = payload.attachments;
      suggestedActions.push(...mapActions(payload.suggestedActions));
    }

    lastMessage.suggestedActions = suggestedActions;

    messages.forEach(msg => this.responses.next(msg));
  }
}

import { NgModule } from '@angular/core';
import { Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { ButtonModule } from '@progress/kendo-angular-buttons';
import { ChatModule } from '@progress/kendo-angular-conversational-ui';

import { AppComponent } from './app.component';
import { PaymentPlanCardComponent } from './payment-plan-card.component';
import { QuoteCardComponent } from './quote-card.component';

@NgModule({
  imports:      [
    BrowserModule,
    BrowserAnimationsModule,
    ButtonModule,
    ChatModule
  ],
  declarations: [
    AppComponent,
    PaymentPlanCardComponent,
    QuoteCardComponent
  ],
  bootstrap:    [ AppComponent ]
})

export class AppModule { }

import { Component, Input, HostBinding } from '@angular/core';
import { Attachment } from '@progress/kendo-angular-conversational-ui';

@Component({
  selector: 'payment-plan-card',
  template: `
    <table class="k-card-body table">
      <tr *ngFor="let row of plan.rows">
        <th>{{ row.text }}</th>
        <td>{{ row.value | currency:'USD':'symbol' }}</td>
      </tr>
    </table>
  `
})
export class PaymentPlanCardComponent {
  @Input()
  public plan: Attachment;

  @HostBinding('class.k-card')
  public cssClass = true;
}
import { Component, Input, HostBinding } from '@angular/core';
import { Attachment } from '@progress/kendo-angular-conversational-ui';

@Component({
  selector: 'quote-card',
  template: `
    <div class="k-card-body">
        <dl class="row">
          <dt class="col-sm-5">Type:</dt>
          <dd class="col-sm-7">{{ quote.coverage }}</dd>

          <dt class="col-sm-5">Car Model:</dt>
          <dd class="col-sm-7">{{ quote.make }} {{ quote.model }}</dd>

          <dt class="col-sm-5">Car Cost:</dt>
          <dd class="col-sm-7">{{ quote.worth | currency:'USD':'symbol' }}</dd>

          <dt class="col-sm-5">Start Date:</dt>
          <dd class="col-sm-7">{{ quote.startDate }}</dd>
        </dl>
        <div class="k-hr"></div>
        <dl class="row">
          <dt class="col-sm-5"><h4>Total:</h4><dt>
          <dd class="col-sm-7"><h4>{{ quote.premium | currency:'USD':'symbol' }}</h4></dd>
        </dl>
      </div>
    `
})
export class QuoteCardComponent {
  @Input()
  public quote: Attachment;

  @HostBinding('class.k-card')
  public cssClass = true;
}
'use strict';

/*
 * Unmodified source code for the DialogFlow Fulfillment Web Hook
 * See: https://dialogflow.com/docs/fulfillment
 */

const functions = require('firebase-functions');
const { WebhookClient } = require('dialogflow-fulfillment');
const { Card, Payload, Suggestion } = require('dialogflow-fulfillment');

process.env.DEBUG = 'dialogflow:debug';

const rates = {
  'full': 0.05,
  'coll_fire_theft': 0.04,
  'coll_only': 0.02,
  'young': 0.06
};

const rateNames = {
  'full': 'Full coverage',
  'coll_fire_theft': 'Collision, fire and theft',
  'coll_only': 'Collision only',
  'young': 'Young driver'
};

const rateDetails = {
  'full': [
    'Full Coverage insurance is the highest level of insurance available. This includes both collision and comprehensive insurance and it offers the most protection of your car, any third parties involved in an accident, as well as their property.',
    'It could also include compensation for medical and legal expenses as well as accidental damage.'
  ],
  'coll_fire_theft': [
    'Collision, fire, and theft insurance provides the same level of cover as ‘collision’ but it can also cover policyholders if their car is damaged due to fire or if it is stolen.',
    'It is very important to remember that this type of insurance doesn’t cover the costs of accidental damage like a tree falling on your car.'
  ],
  'coll_only': [
    'Collision coverage is the most basic level of insurance, and is the bare minimum required by law',
    'If an accident happens between two or more cars, then the insurance covers you for any damage you cause to another person’s car or any injury you caused to someone else, including passengers in your car.'
  ],
  'young': [
    'Congratulations! You’ve passed your test and you’re no longer a learner driver.',
    'Young drivers face expensive insurance premiums because they’re far more likely to be involved in a car accident than experienced drivers.',
    'If you are between 17 and 21 you can have a flexible short-term insurance that covers your own car, or a car owned by a parent.'
  ]
};

const renewalParameters = {
  "Worth": 43750,
  "StartDate": "2018-05-27",
  "Worth.original": "43750",
  "Make.original": "Audi",
  "Model": "A5 Sportback",
  "Model.original": "a5 sportback",
  "StartDate.original": "tomorrow",
  "Premium": 2187.50,
  "Make": "Audi",
  "Coverage": "full",
  "Coverage.original": "full coverage"
};

const attachQuote = data => ({
    type: 'quote',
    premium: data.Premium,
    coverage: rateNames[data.Coverage],
    make: data.Make,
    model: data.Model,
    worth: data.Worth,
    startDate: data.StartDate
});

function pow(p) {
  if (p) {
      return Math.pow(10, p);
  }

  return 1;
}

function round(value, precision) {
  const power = pow(precision);
  return Math.round(value * power) / power;
}

function welcome(agent) {  
  const payload = {
    type: 'message',
    attachments: [],
    suggestedActions: [{
        type: "postBack",
        title: "Get a Quote",
        value: "Get a Quote"
    }, {
        type: "postBack",
        title: "Get a Renewal",
        value: "Get a Renewal"
    }]
  };

  agent.setContext({ name: 'quoteinput-followup', lifespan: 5, parameters: {} });

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('welcome: ' + JSON.stringify(payload, null, 2));
}

function getQuote(agent) {
  const payload = {
    attachmentLayout: "carousel",
    attachments: [
      {
        title: "Full Coverage",
        subtitle: "5% of car cost",
        images: [
          {
            url: "https://demos.telerik.com/kendo-ui/content/chat/quote_full.jpeg"
          }
        ],
        buttons: [
          {
            type: "postBack",
            title: "View Details",
            value: "View details of Full Coverage"
          },
          {
            type: "postBack",
            title: "Get a Quote",
            value: "Get a quote for Full Coverage"
          }
        ]
      },
      {
        title: "Collision, fire and theft",
        subtitle: "4% of car cost",
        images: [
          {
            url: "https://demos.telerik.com/kendo-ui/content/chat/quote_collision.jpeg"
          }
        ],
        buttons: [
          {
            type: "postBack",
            title: "View Details",
            value: "View details of Collision, fire and theft"
          },
          {
            type: "postBack",
            title: "Get a Quote",
            value: "Get a quote for Collision, fire and theft"
          }
        ]
      },
      {
        title: "Collision only",
        subtitle: "2% of car cost",
        images: [
          {
            url: "https://demos.telerik.com/kendo-ui/content/chat/quote_collision_only.jpeg"
          }
        ],
        buttons: [
          {
            type: "postBack",
            title: "View Details",
            value: "View details of Collision only"
          },
          {
            type: "postBack",
            title: "Get a Quote",
            value: "Get a quote for Collision only"
          }
        ]
      },
      {
        title: "Young driver",
        subtitle: "6% of car cost",
        images: [
          {
            url: "https://demos.telerik.com/kendo-ui/content/chat/quote_young.jpeg"
          }
        ],
        buttons: [
          {
            type: "postBack",
            title: "View Details",
            value: "View details of Young driver"
          },
          {
            type: "postBack",
            title: "Get a Quote",
            value: "Get a quote for Young driver"
          }
        ]
      }
    ]
  }

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('getQuote: ' + JSON.stringify(payload, null, 2));
}

function quote(agent) {
  const context = agent.getContext('quoteinput-followup');
  const params = context.parameters;

  const worth = params.Worth;
  const coverage = params.Coverage;
  const make = params.Make[0];

  if (!rates.hasOwnProperty(coverage)) {
    agent.add(`I didn't get that, can you try again?`);
    return;
  }

  const rate = rates[coverage];
  params.Premium = worth * rate;

  agent.setContext({ name: 'quoteinput-followup', lifespan: 5, parameters: params })

  const payload = {
    type: 'message',
    attachments: [
      attachQuote(params)
    ],
    suggestedActions: [{
      type: 'postBack',
      title: 'Agree',
      value: 'Agree'
    }, {
      type: 'postBack',
      title: 'Cancel',
      value: 'Cancel'
    }, {
      type: 'postBack',
      title: 'Change Car Model',
      value: 'Change Car Model'
    }, {
      type: 'postBack',
      title: 'Change Insurance Start Date',
      value: 'Change Insurance Start Date'
    }, {
      type: 'postBack',
      title: 'Change Car Cost',
      value: 'Change Car Cost'
    }]
  };

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('quote: ' + JSON.stringify(payload, null, 2));
}

function quoteConfirm(agent) {
  const payload = {
    type: 'message',
    attachments: [],
    suggestedActions: [
      {
        type: "postBack",
        title: "One Payment",
        value: "One Payment"
      },      {
        type: "postBack",
        title: "Two Payments",
        value: "Two Payments"
      },      {
        type: "postBack",
        title: "Three Payments",
        value: "Three Payments"
      },      {
        type: "postBack",
        title: "Four Payments",
        value: "Four Payments"
      }
    ]
  };

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('quoteConfirm: ' + JSON.stringify(payload, null, 2));
}

function quoteCancel(agent) {
  const payload = {
    type: 'message',
    attachments: [],
    suggestedActions: [
      {
        type: "postBack",
        title: "Let's do it!",
        value: "View all products"
      },
      {
        type: "postBack",
        title: "No, thanks!",
        value: "No, thanks!"
      }
    ]
  };

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('quoteCancel: ' + JSON.stringify(payload, null, 2));
}

function quoteEnd(agent) {
  const context = agent.getContext('quoteinput-followup');

  agent.add('Congratulations! Your car insurance is confirmed!');
  agent.add('A confirmation message has been sent to your e-mail.');
  agent.add('Thank you for choosing Motor Insurance Company! Enjoy the ride!');

  console.log('processing quote: ', JSON.stringify(context, null, 2));
}

function quoteDetails(agent) {
  const context = agent.getContext('quotedetails');
  const params = context.parameters;
  const coverage = params.Coverage;

  if (!rateDetails.hasOwnProperty(coverage)) {
    agent.add(`I didn't get that, can you try again?`);
    return;
  }

  const details = rateDetails[coverage];
  details.forEach((msg) => agent.add(msg));

  agent.add(new Suggestion(`Get a quote for ${rateNames[coverage]}`));
  agent.add(new Suggestion({title: 'View all products', value: 'View all products'}));

  console.log('quoteDetails: sent details for ' + coverage);
}

function payments(agent) {
  const context = agent.getContext('quoteinput-followup');
  const params = context.parameters;
  const premium = params.Premium;
  const count = params.Payments;

  const rows = [];
  const value = round(premium / count, 2);
  for (let i = 0; i < count; i++) {
    rows.push({ text: `Payment #${ i + 1 }`, value });
  }

  const payload = {
    type: 'message',
    attachments: [{
      type: 'payment_plan',
      rows,
      premium
    }],
    suggestedActions: [{
      type: 'postBack',
      title: 'Confirm',
      value: 'Confirm'
    }, {
      type: 'postBack',
      title: 'Cancel',
      value: 'Cancel'
    }]
  };

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('payments: ' + JSON.stringify(payload, null, 2));
}

function resetPayments(agent) {
  agent.setFollowupEvent('ConfirmQuote');
  console.log('resetPayments: Triggering ConfirmQuote');
}

function renew(agent) {
  const data = renewalParameters;
  const payload = {
    type: 'message',
    attachments: [
      attachQuote(data)
    ],
    suggestedActions: [
      {
        type: 'postBack',
        title: 'Confirm',
        value: 'Agree'
      },
      {
        type: 'postBack',
        title: 'See other car insurance policies',
        value: 'See other car insurance policies'
      }
    ]
  };

  agent.setContext({
    name: 'quoteinput-followup',
    lifespan: 5,
    parameters: renewalParameters
  });

  agent.setContext({
    name: 'quoteinput-yes-followup',
    lifespan: 5,
    parameters: {}
  });

  agent.add(new Payload(agent.UNSPECIFIED, payload));
  console.log('renew: ' + JSON.stringify(payload, null, 2));
}

function confirmRenewal(agent) {
  agent.setFollowupEvent('ConfirmQuote');
  console.log('confirmRenewal: Triggering ConfirmQuote');
}

function renewOther(agent) {
  agent.setFollowupEvent('ListProducts');
  console.log('renewOther: triggering ListProducts')
}

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers, null, 2));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body, null, 2));

  const intentMap = new Map();
  intentMap.set('Default Welcome Intent', welcome);
  intentMap.set('Get a Quote', getQuote);
  intentMap.set('Quote Input', quote);
  intentMap.set('Quote Input - Correction - Car Model', quote);
  intentMap.set('Quote Input - Correction - Car Cost', quote);
  intentMap.set('Quote Input - Correction - Start Date', quote);
  intentMap.set('Quote Input - Confirm', quoteConfirm);
  intentMap.set('Quote Input - Cancel', quoteCancel);
  intentMap.set('Quote Input - Payments', payments);
  intentMap.set('Quote Input - Payments - Cancel', resetPayments);
  intentMap.set('Quote Input - Payments - Confirm', quoteEnd);
  intentMap.set('Quote Details', quoteDetails);
  intentMap.set('Get a Renewal', renew);
  intentMap.set('Get a Renewal - Confirm', confirmRenewal);
  intentMap.set('Get a Renewal - Other', renewOther);

  agent.handleRequest(intentMap);
});
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
In this article