Showing posts with label JET. Show all posts
Showing posts with label JET. Show all posts

Wednesday, May 30, 2018

Oracle Bot Cloud (IBCS): Voice based coversation

Problem Description: In this blog we are trying to enhance our web chatbot ui created in previous blog and add voice features.

Idea here is to send text message to Bot and receive only text messages but before sending convert user voice (speech) to text and again when we receive and message from bot server play it as audio.

There are few options available to convert voice and text. In this blog we are going to use
1. Speech to Text:  webkitSpeechRecognition api of browser
2. Text to Speech: speechSynthesis api of browser

Lets take our previous chatbot UI created in blog http://sanjeev-technology.blogspot.com/2018/05/oracle-bot-cloud-ibcs-custom-ui.html and enhance it for voice support.

Now we can follow below steps
1. Add a button to initiate voice chatting
<oj-button id="start_button" on-oj-action="[[startVoiceChat]]">Start Voice Chat</oj-button>
Now add corresponding code in appcontroller.js
self.startVoiceChat = function(event){
        listenUserVoice();
      }

NOTE: I am using oracle jet so I use oj-button. Point is we have to call listenUserVoice method on button click.

2. Add speak method in app.js. This method will provide audio to text message. It will also callback once meassage reading is finished.

function speak(text, onendcallback){
    var msg = new SpeechSynthesisUtterance();
     msg.text = text;
     if(onendcallback){
        msg.onend = onendcallback;
     }
     window.speechSynthesis.speak(msg);

     console.log(JSON.stringify(msg));;
  }

3. Add listenUserVoice method also in app.js. This method reconizes user voice and converts it to text. It also calls bots sendmessage or triggerpostback depending on situation.

function listenUserVoice(){
    var recognition = new webkitSpeechRecognition();
            recognition.continuous = false;
            recognition.interimResults = true;

            recognition.onstart = function() {
                recognizing = true;
            };

            recognition.onend = function() {
                recognizing = false;
            };

            recognition.onresult = function(event) {
                console.log("recognition-onresult" + event);
              console.log(event);

              var interim_transcript = '';
              if (typeof(event.results) == 'undefined') {
                recognition.onend = null;
                recognition.stop();
                upgrade();
                return;
              }
              for (var i = event.resultIndex; i < event.results.length; ++i) {
                if (event.results[i].isFinal) {
                  final_transcript += event.results[i][0].transcript;
                } else {
                  interim_transcript += event.results[i][0].transcript;
                }
              }
              

              if(final_transcript){

                console.log("User said: " + final_transcript);
                var totalMsg = Bots.getConversation().messages.length;
                if(Bots.getConversation().messages[totalMsg-1] && Bots.getConversation().messages[totalMsg-1].actions){
        
                var actions = Bots.getConversation().messages[totalMsg-1].actions.filter(function(action){
                    return action.text === final_transcript; //Improve it by performing case insensitive matching
                })
                if(actions && actions[0]){
                    Bots.triggerPostback(actions[0]._id).then(function() {
                                console.log("postback");
                            });
                }
                
                }
                else{
                   Bots.sendMessage(final_transcript).then(function() {
                            console.log("normal message");
                        }); 
                }

                
                if(final_transcript){
                  recognizing = false;
                }
                

            }
            }

            if (recognizing) {
              recognition.stop();
              return;
            }
            final_transcript = '';
            recognition.start();

        }
  
4. Now modify displayServerMessage method to call listenUserVoice method so that system automatically starts taking user message after providing any information.

function displayServerMessage(message) {
         console.log(message);
            var conversationElement = document.getElementById('conversation');
            var messageElement = document.createElement('li');
            var text = 'Server says "' + message.text + '"';
            messageElement.innerText = text;

            if(message.actions && message.actions.length > 0){
                var wrapperElement = document.createElement('div');
                for(var i = 0; i < message.actions.length; i++){
                    var action = message.actions[i];
                    var btnElement = createButtonElement(action);
                    wrapperElement.appendChild(btnElement);
                }
                messageElement.appendChild(wrapperElement);
                
            }
            conversationElement.appendChild(messageElement);


            speak(text, listenUserVoice);
        }

Thats all.

Oracle Bot Cloud (IBCS): Custom UI

Problem Description: In this blog I would be going over various options to build a web UI for IBCS chat bot.

By default IBCS provides two sdks to build web UI for Chatbot. We can download those sdks from
http://www.oracle.com/technetwork/topics/cloud/downloads/mobile-suite-3636471.html under heading Bots Client SDK


We can follow link https://docs.oracle.com/en/cloud/paas/mobile-suite/use-chatbot/bot-channels.html#GUID-A0A40E26-54BA-4EDD-A4C5-95D498D6CF61 to find out how to use these SDKs.


Widget is very cool and rich but still at times you want to build your own custom UI instead of widget. To do that you can follow
https://docs.oracle.com/en/cloud/paas/mobile-suite/use-chatbot/bot-channels.html#GUID-78F6DD7E-5085-476B-AD03-1318D9107D39

In this blog I am trying to enhance that code to handle postback requests. Also this blog will help me in my next blog to achieve voice based conversation.

1. Add below code in html
HTML:
 <div id="no-display" style="display:none;"></div>
       <p>User ID: <span id="user-id"></span></p>
       <ul id="conversation"></ul>
    <input type="text" id="text-input" placeholder="text">

<script src="js/app.js"></script>


2. Add app.js inside js directory. It can have following code
!function(e,t,n,r){
    function s(){try{var e;if((e="string"==typeof this.response?JSON.parse(this.response):this.response).url){var n=t.getElementsByTagName("script")[0],r=t.createElement("script");r.async=!0,r.src=e.url,n.parentNode.insertBefore(r,n)}}catch(e){}}var o,p,a,i=[],c=[];e[n]={init:function(){o=arguments;var e={then:function(t){return c.push({type:"t",next:t}),e},catch:function(t){return c.push({type:"c",next:t}),e}};return e},on:function(){i.push(arguments)},render:function(){p=arguments},destroy:function(){a=arguments}},e.__onWebMessengerHostReady__=function(t){if(delete e.__onWebMessengerHostReady__,e[n]=t,o)for(var r=t.init.apply(t,o),s=0;s<c.length;s++){var u=c[s];r="t"===u.type?r.then(u.next):r.catch(u.next)}p&&t.render.apply(t,p),a&&t.destroy.apply(t,a);for(s=0;s<i.length;s++)t.on.apply(t,i[s])};var u=new XMLHttpRequest;u.addEventListener("load",s),u.open("GET",r+"/loader.json",!0),u.responseType="json",u.send()
}(window,document,"Bots", "<Your-Bot-sdk-url>");

var appId = '<Your app id>';

Bots.init({
        appId: appId, embedded: true
    }).then(function (res){
        console.log("init complete");

    });

Bots.render(document.getElementById('no-display'));


var inputElement = document.getElementById('text-input');
  inputElement.onkeyup = function(e) {
  if (e.key === 'Enter') {
    var totalMsg = Bots.getConversation().messages.length;
    if(Bots.getConversation().messages[totalMsg-1] && Bots.getConversation().messages[totalMsg-1].actions){
        
        var actions = Bots.getConversation().messages[totalMsg-1].actions.filter(function(action){
            return action.text === inputElement.value; //Improve it by performing case insensitive matching
        })
        if(actions){
            Bots.triggerPostback(actions[0]._id).then(function() {
                        inputElement.value = '';
                    });
        }
        
    }
    else{
       Bots.sendMessage(inputElement.value).then(function() {
                inputElement.value = '';
            }); 
    }
        
    }
   
  }


function displayUserMessage(message) {
    console.log(message);
            var conversationElement = document.getElementById('conversation');
            var messageElement = document.createElement('li');
            messageElement.innerText = message.name + ' says "' + message.text + '"';
            conversationElement.appendChild(messageElement);
        }

        function createButtonElement(action) {
            var btnElement = document.createElement('button');
            var btnTitle = document.createTextNode(action.text);
            btnElement.appendChild(btnTitle);
            btnElement.onclick = function(e){Bots.triggerPostback(action._id);};
            return btnElement;
        }

    function displayServerMessage(message) {
         console.log(message);
            var conversationElement = document.getElementById('conversation');
            var messageElement = document.createElement('li');
            var text = 'Server says "' + message.text + '"';
            messageElement.innerText = text;

            if(message.actions && message.actions.length > 0){
                var wrapperElement = document.createElement('div');
                for(var i = 0; i < message.actions.length; i++){
                    var action = message.actions[i];
                    var btnElement = createButtonElement(action);
                    wrapperElement.appendChild(btnElement);
                }
                messageElement.appendChild(wrapperElement);
                isPostBackRequired = true;
                lastPostBackServerMsg = message;
            }
            conversationElement.appendChild(messageElement);


        }
  // display new messages
  Bots.on('message:sent', displayUserMessage);

  Bots.on('message:received', displayServerMessage);



NOTE:
1. Bot-sdk-url is url of your sdk directory. If you copy bot-sdk inside js directory as bot-client-sdk-js and server is running on port 8000, your bot-sdk-url would be http://localhost:8000/js/bots-client-sdk-js
2. app-id mentioned in above code is given on channels page of IBCS (once you register a web channel)

3. Input element in which user types message is enhanced to handle postBack message of user.
4. displayUserMessage function adds user typed message in list
5. displayServerMessage function adds server message in list. It also create appropriate buttons if server wants user to select one value.

UI is very crude but it gives you complete control to decorate it.

Now we have a very basic UI ready. My idea is to enhance it further and add voice feature to it in my Next blog.

Thats all in this blog.

Monday, February 19, 2018

Oracle JET: Bookmark a secured pages

Problem Description: Oracle JET allows us to create SinglePage application (SPA). It changes url when we navigate between pages. What if application is secured application and pages require login to visit. In such case if we book mark a page and later try to launch it, application should verify if user is already logged in, If not redirect user to login page and once logged in user should be shown same page, which he has requested. In this blog we are trying to achieve it.

Also if user has directly started from login page then if login is successful we need to take him to default page mentioned in router configuration.

Solution:
 To implement this we need to achieve following flow

Here are the steps to achieve above flow

1. User can either launch login page or a secured page (say Page-X). If user launches login page, we can directly show him login page, but if he launches Page-X, we need to verify if user is already logged in.To verify if user has already logged in depends on server side logic.
   Here in this blog I assume there is a REST service available, which returns profile of logged in user (myprofile service). we will call this service, without passing any Authorization header. In such case browser will pass SESSIONID from cookie if user is already logged in. If not logged in then we will get 401 from server. If we get error from server, we can redirect user to login page, but we will add current url in login page url additionally, so that we can use it later to redirect user back to Page-X. Here is the common code checkIfLoginDone function written in appcontroller.js

     function ControllerViewModel() {
        $.ajaxSetup({
          xhrFields: {
            withCredentials: true
          }
        }); 

        var self = this;
        self.baseURL='http://localhost:7101';
        self.serviceContextRoot = '/myapp';
        self.serviceInitialURL = self.baseURL + self.serviceContextRoot + '/resources';

        self.router = oj.Router.rootInstance;
        self.checkIfLoginDone = function checkIfLoginDone(){
          console.log('starting login check');
          
          var promise = $.ajax({
             type: "GET",
             url: self.serviceInitialURL+ "/v1/myprofile",
             contentType: "application/json; charset=utf-8",
             crossDomain: true,
             dataType: "json"});
           
            promise.then(function(response) {
                  
                 

              }, function(error) {
                 // error handler
                 var currenturl = window.location.pathname + window.location.search ;
                window.location.href = '/?root=login&origurl='+currenturl;
              });

            return promise;
         
          
        }

In above code if we get error from server, we will be redirecting user to login page and add url of secured page as a parameter origurl. Login page url will appear like
http://<host>:<port>/?root=login&origurl=<url-path-of-secured-page>

[Assuming that login will be router state for login page]

2. To perform login check we can it checkIfLoginDone from all secured pages's ViewModel as
define(['ojs/ojcore', 'knockout', 'jquery', 'appController'],
 function(oj, ko, $, app) {
      self.handleActivated = function(info) {
        // Implement if needed
        return app.checkIfLoginDone();
      };

3. Create a login page: For this you can follow below steps
      a. Create login.html in js/views directory. Content could be
         

      <div class="oj-hybrid-padding">
        <h1>Login Content Area</h1>
          <div id="sampleDemo" style="" class="demo-container">
            <div id="componentDemoContent" style="width: 1px; min-width: 100%;">
              <div id="form-container" class="oj-form-layout">
                <div class="oj-form">
                  <div class="oj-flex">  
                    <div class="oj-flex-item">
                      <oj-label show-required for="username">User Name</oj-label>
                      <oj-input-text id="username" required value="{{username}}"></oj-input-text>
                    </div>
                  </div>
                  <div class="oj-flex">  
                    <div class="oj-flex-item">
                      <oj-label for="password" show-required>Passward</oj-label>
                      <oj-input-password id="password" required value="{{password}}"></oj-input-password>
                    </div>
                  </div>
                </div>
                <oj-button id='submit' on-click='[[doLogin]]'>Login</oj-button>
              </div>              
            </div>
    </div>
  </div>
   
In above page, we have created two fields username/password and a button Login.
Username and password are bound to ViewModel and Login button click calls doLogin method of ViewModel

    b. Create login.js as a ViewModel in js/viewModels directory. Its code would be

define(['ojs/ojcore', 'knockout', 'jquery', 'appController','ojs/ojknockout','ojs/ojlabel','ojs/ojinputtext', 'ojs/ojcheckboxset'],
 function(oj, ko, $, app) {
  
    function LoginViewModel() {
      var self = this;
      function param(name) {
            return (location.search.split(name + '=')[1] || '').split('&')[0];
        }
      self.username = ko.observable("");
      self.password = ko.observable("");
      self.doLogin = function doLogin(event){
        $("body").css("cursor", "wait");
       //do server login here
       var string = self.username() + ':' + self.password();
      var encodedString = 'Basic ' + btoa(string);
             
      var promise = $.ajax({
              type: "POST",
              url: app.serviceInitialURL + '/v1/auth/login',
              contentType: "application/json; charset=utf-8",
              headers: {
                   "Content-Type": "text/plain",
                    "Authorization": encodedString
              },
              crossDomain: true,
              dataType: "json"});
         
      promise.then(function(response){
              var origurl = param('origurl');
              if(origurl){
                window.location.href = origurl;
              }
              else{
                oj.Router.rootInstance.go('dashboard');
              }

              $("body").css("cursor", "default");
         }, function(response){
             //write logic here to show error message to end user.
         }) ;   

      }
      // Header Config
      self.headerConfig = {'viewName': 'header', 'viewModelFactory': app.getHeaderModel()};

      
     

    return new LoginViewModel();
  }
);

Important piece of above code is 
    i. username/password created as ko observable.
   ii. username password is used to create base 64 encoded authorization string
  iii. server side login using $.ajax POST request
   iv. If login is success then verify if there is any url parameter as origurl present. Navigate to the location whereever origurl points to. If not specified then get default page from router and navigate there. 

c. register login page in router configuration

self.router.configure({
          'login': {label: 'Login'},
         'dashboard': {label: 'Dashboard', isDefault: true},
         'incidents': {label: 'Incidents'},
         'customers': {label: 'Customers'},
         'profile': {label: 'Profile'},
         'about': {label: 'About'}
        });

4. Finally we need a logout button. We can keep it in header.html
<div class="oj-flex-bar-end">
    <oj-button id='button1' on-click="[[logout]]">Logout</oj-button>
</div>

logout implementation is written in appcontroller.js by changing getHeaderModel method.
 self.logout = function(event){
           $.ajax({
                 type: "GET",
                 url: self.serviceInitialURL+ "/v1/auth/logout",
                 contentType: "application/json; charset=utf-8",
                 crossDomain: true,
                 dataType: "json",
                 success: function (data, status, jqXHR) {
                        oj.Router.rootInstance.go('login');
                 },

                 error: function (jqXHR, status) {
                     // error handler
                     
                 }
              });

      }


 self.getHeaderModel = function() {
          var headerFactory = {
            createViewModel: function(params, valueAccessor) {
              var model =  {
                pageTitle: self.router.currentState().label,
                handleBindingsApplied: function(info) {
                  // Adjust content padding after header bindings have been applied
                  self.adjustContentPadding();
                },
                toggleDrawer: self.toggleDrawer,
                logout: self.logout
              };
              return Promise.resolve(model);
            }
          }
          return headerFactory;
        }

Above logout method calls server side logout and then redirect user to login page.



Saturday, January 13, 2018

Oracle JET: Caching data

Problem Description: Oracle JET is a client side technology and mostly it uses REST services to fetch data from server. Some of data is used on multiple pages and it does not change very frequently so its a good approach to query such data once and then cache it. Next time when you need it refer from cache.


Solution:

In Oracle JET we have index.html, which serves as main (or base) page. All other pages (module) appears on it. Some modules might be static and some are attached dynamically using router. To some up at run time, things are like below image.


Based on URL different modules keep on changing but index.html (base page) is as it is. It implies view model associated with it (also called root viewmodel) is also available all the time.

Another thing that we should know is 'LifeCycle of ViewModel instances'. In general code for viewmodels are like below

define(['ojs/ojcore', 'knockout', 'jquery', 'appController'],
 function(oj, ko, $, app) {
    
    function PageXViewModel() {
      console.log("Instantiating PageXViewModel");
      var self = this;
 
    }

    return new PageXViewModel();
  }
);

If we see ViewModels are created using RequireJS define function. RequireJS is an AMD framework. It helps creating JS objects only when needed and once object is available, it associate it with a key. Object remains in memory always and you can access it with that key. All ViewModels are created like that. Which means ViewModel instace would get created when first time you access module associated with that ViewModel. Next time when you try to access ViewModel, you will not get a fresh instance, you will get same old instance instead.
In above example  PageXViewModel instance would get created only once, when we access PageX first time. After that if we go to PageY and come back to PageX, same instance of PageXViewModel will be reused. To confirm that you can see browser console log and find how many times "Instantiating PageXViewModel" is getting printed. You will find its only once, when PageX is loaded first time.

Effectively we can say if  page is showing a module (PageX), there are two instances available PageXViewModel and RootViewModel. We can use them to cache our data. 

With that in mind, in this blog we can see various strategy to cache our data.

There are different kinds of data that we show on pages
1. Transactional data: which keeps on changing and user expects it to be latest always. For example table showing employee data. We should not try to cache such data as user expectation is to see latest data always. As we know our ViewModel instances are getting reused so how should we make sure that every time, we go to a page data is re-fetched from server. ViewModel has a function called handleActivated. This is called by framework always when a view is loaded. We can use this function to fetch data from server. For example

 self.handleActivated = function(info) {

         var promise = $.ajax({
                 type: 'GET',
                 url: 'http://myservice-url',
                 contentType: "application/json; charset=utf-8",
                 crossDomain: true,
                 dataType: "json"
              })
 
  promise.then(function(data) {
            self.data = ko.observableArray(data);

          });
 
         return promise;

        
        
     };
In above code we are setting data property of ViewModel using a REST output and this code runs every time we load page, so data will always show new values from server whenever page is loaded.

2. Setup data: which rarely changes. For example data appearing in various drop-downs (lookup).  This kind of data is ideal for caching. In this example let say we have a lookup PRIORITY and it shows values as High (H), Medium (M), Low (L). We want to show it as drop-down on customer and profile pages.

  To cache that I have two step code
  a. Add below method in appcontroller.js

function ControllerViewModel() {
        var self = this;

        //initiate global cache with empty objects
        self.globals = {lookups: []};


        //core private method responsible for fetching lookup data using rest-service
        var fetchLookupData = function fetchLookupData(lookupType){
            
            var encodedString = 'Basic ' + btoa('username:password');
            var promise = $.ajax({
                 type: 'GET',
                 url: 'my-rest-url,
                 contentType: "application/json; charset=utf-8",
                 headers: {"Authorization": encodedString},
                 crossDomain: true,
                 dataType: "json"
              });

            promise.then(function(response) {
                  self.globals.lookups.push ( {"lookupType": lookupType, 
                                              "lookupValues": response.lookupValues});
                 

              }, function(error) {
                  //alertFactory.handleServiceErrors(error);
                  console.log(error); //Handle errors in a better way
              });

            return promise;
        }

        //method to put a logic to get lookup data from cache (global) first and if not present then from rest-service
        self.initLookpCache = function initLookpCache(lookupType){
          
            var lookups = self.globals.lookups.filter(function (lookup){
              return lookup.lookupType === lookupType;
            })
            if(lookups.length > 0){
              console.log("Lookup present in cache");
              return lookups[0];
              
            }
            else{
              console.log("Reading lookup from service");
              return fetchLookupData(lookupType);
            }
          
        }
      
         //Remaining standard code of appcontroller.js is not shown.

Now on page, which requires cached data we can add below lines. Add it in customer.js and profile.js both

self.handleActivated = function(info) {
        // Implement if needed
        console.log('running handleActivated from CustomerViewModel');
        
        var promise = app.initLookpCache('MY_LOOKUP_TYPE');
        Promise.resolve(promise).then(function(lookupTypeValues) {
            self.linkTypes = ko.observableArray(lookupTypeValues.lookupValues);
        });


        return promise;

      };


We have actually put a call to Root-ViewModel's initLookupCache
It can return either Promise or lookupvalue. Promise is returned if values are getting fetched from REST service call. LookupValues are returned directly if values are coming from cache.

Promise.resolve make sure that
        if input is promise object then wait for it to get completed and once completed then only run function specified in then.
        if input is not promise but direct values then directly runs the function and pass values as input.

If you run customer and profile pages, you will find below log
running handleActivated from CustomerViewModel
Reading lookup from service

Instantiating ProfileViewModel
running handleActivated from ProfileViewModel
Lookup present in cache

You can see lookup values are coming from cache second time. Good part is its incremental cache, as and when you need lookup data first time, it will get cached. You are not loading all lookup data upfront.

Few other tips

  a. If you want to cache a data which is required very frequently (almost on all pages) you can cache such data from appcontroller.js itself.


  c. If you want to cache a data, which is required only in very very few pages you can cache them in their respective page's ViewModel itself. For that instead of calling appcontroller's method you can have local method and populate ViewModel (self.data) variable. NOTE: Even these page's viewmodel are not getting destroyed so anything cached there is also present throughout. For local caching your code of handleActivated could be be something like
self.data = ko.observable();

self.handleActivated = function(info) {
   if(!self.data()){
      //Make reset service call and populate self.data
   }
}




Thats all.

Monday, May 8, 2017

Oracle JET: Hybrid android app using NetBeans

This blog is continuation of my previous blog http://sanjeev-technology.blogspot.in/2017/05/oracle-jet-first-hybrid-android-mobile.html

In that blog I showed how we can use npm, yo and grunt commands to develop a new web/mobile application in JET.
In this blog my idea is simply open the project in NetBeans and start development in NetBeans. NetBeans provide auto-completion + templates for new project etc for Oracle JET. As we have already created project using commands so we will not be creating a new project but directly open it in NetBeans and start using it.

1. Open project: To open project in NetBeans you can navigate as File > Open Project
      Good News: NetBeans automatically identifies our project created using yoman (yo) as a project.
     

We get various source directories (src, src-hybrid, src-web). We also have a separate web directory.


2. Make changes: Let us make some changes in source and see its
Few things to notice:

  • We can directly run index.html present in web directory.
  • Once its running Chrome browser shows a bar " NetBeans Connector" is debugging this browser. Don't cancel it. It enables browser log to appear in NetBeans console. 
  • Any changes done in web directory (Site Root) are picked immediately (without restart). 
  • Any changes done in src directories are also picked immediately (without restart). Actually these changes are pushed automatically to web directory immediately and application picks from web after that.
  • Ideally we should be changing src directory and should not code in web directory in this kind of setup. web directory is kind of classes directory and NetBeans will use it to run application. 

  

3. Run and Quick test: For this demo I just make changes in index.html inside src directory. I will change quickly change <h2 class="demo-profile-name">James</h2> to <h2 class="demo-profile-name">Sanjeev</h2>. I just need to save it. I find that changes are automatically pushed to web directory and even my browser is refreshed automatically to show new changes.




4. Release using Grunt from NetBeans

To run grunt commands for project, I can right click on project and select Grunt Tasks. There are few tasks already listed. To release I need to use serve:release task, which is not listed, so I can go to advance and create my own task.



Under grunt tab log you will see location of apk file.


Thats all. Now you can move and install this apk in android phone.