├── runtime.txt ├── Procfile ├── ASRIntents.zip ├── static ├── .DS_Store ├── favicon.ico ├── images │ ├── .DS_Store │ └── twiliologo.svg ├── asr_dashboard.css ├── asr_dashboard.html └── asr_dashboard.js ├── asr_ivr_dashboard.png ├── requirements.txt ├── LICENSE ├── app.json ├── README.md └── main.py /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.3 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app 2 | -------------------------------------------------------------------------------- /ASRIntents.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ameerbadri/twilio-asr-realtime-dashboard/HEAD/ASRIntents.zip -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ameerbadri/twilio-asr-realtime-dashboard/HEAD/static/.DS_Store -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ameerbadri/twilio-asr-realtime-dashboard/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /asr_ivr_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ameerbadri/twilio-asr-realtime-dashboard/HEAD/asr_ivr_dashboard.png -------------------------------------------------------------------------------- /static/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ameerbadri/twilio-asr-realtime-dashboard/HEAD/static/images/.DS_Store -------------------------------------------------------------------------------- /static/asr_dashboard.css: -------------------------------------------------------------------------------- 1 | [v-cloak] .v-cloak--block { 2 | display: block; 3 | } 4 | [v-cloak] .v-cloak--inline { 5 | display: inline; 6 | } 7 | [v-cloak] .v-cloak--inlineBlock { 8 | display: inline-block; 9 | } 10 | [v-cloak] .v-cloak--hidden { 11 | display: none; 12 | } 13 | [v-cloak] .v-cloak--invisible { 14 | visibility: hidden; 15 | } 16 | .v-cloak--block, 17 | .v-cloak--inline, 18 | .v-cloak--inlineBlock { 19 | display: none; 20 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==1.5.3 2 | certifi==2017.11.5 3 | chardet==3.0.4 4 | click==6.7 5 | flask>=0.12.3 6 | gunicorn==19.7.1 7 | idna==2.6 8 | isort==4.2.15 9 | itsdangerous==0.24 10 | Jinja2>=2.10.1 11 | lazy-object-proxy==1.3.1 12 | MarkupSafe==1.0 13 | mccabe==0.6.1 14 | PyJWT==1.5.3 15 | pylint==1.7.4 16 | PySocks==1.6.7 17 | pytz==2017.3 18 | requests==2.18.4 19 | six==1.11.0 20 | twilio==6.9.1 21 | urllib3==1.24.2 22 | Werkzeug==0.15.3 23 | wrapt==1.10.11 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ameer Badri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Twilio ASR IVR Dashboard", 3 | "description": "Twilio ASR and Intent IVR Dashboard", 4 | "repository": "https://github.com/ameerbadri/twilio-asr-realtime-dashboard", 5 | "logo": "http://presos.jamesward.com/practicing_continuous_delivery/assets/python.png", 6 | "keywords": ["python", "twilio", "ivr", "asr", "dashboard"], 7 | "env": { 8 | "TWILIO_ACCOUNT_SID": { 9 | "description": "Your Twilio Account SID", 10 | "value": "your_twilio_account_sid" 11 | }, 12 | "TWILIO_AUTH_TOKEN": { 13 | "description": "Your Twilio Auth Token", 14 | "value": "your_twilio_auth_token" 15 | }, 16 | "TWILIO_API_KEY": { 17 | "description": "Your Twilio API Key", 18 | "value": "your_twilio_api_key" 19 | }, 20 | "TWILIO_API_SECRET": { 21 | "description": "Your Twilio API Secret", 22 | "value": "twilio_api_secret" 23 | }, 24 | "TWILIO_SYNC_SERVICE_ID": { 25 | "description": "Your Twilio Sync Sercice ID", 26 | "value": "your_twilio_sync_service_id" 27 | }, 28 | "APIAPI_CLIENT_ACCESS_KEY": { 29 | "description": "Your API.AI Bot Client Access Key", 30 | "value": "your_apiai_client_access_key" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /static/images/twiliologo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 16 | 18 | 20 | 22 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio ASR and Intent Realtime Dashboard 2 | 3 | This demo is about the power of using Twilio realtime Automated Speech Recoginition (ASR) and Intent analysis system (Dialogflow) in an IVR. Because Twilio makes it so easy to collect customer speech and related intent, one of the business benefits is direct access to what your customers are calling for and saying so the calls can be routed accurately. 4 | 5 | Let's say your organisation want to implement a Speech or Chat bot in your customer journey. The first question that arises is how should the bot be designed, what intents will it recognise so the calls are routed correctly with the right context to the customer care agent. There are two options to go about - one is to use your own judgement/guess work to design the intents the bot will recognise. Or, you can directly listen to your customers and create the best experience. That's exactly what some of our most brand concious customers have done. They integrated this code into their production contact centre. Once they collected thousands of customer speech phrases, they've started refining their bot for most accurate intent based routing. 6 | 7 | Lets get started... 8 | 9 | ## Realtime ASR and Intent Dashboard 10 | 11 | ![](asr_ivr_dashboard.png) 12 | 13 | You'll need following accounts: 14 | 1) Twilio (https://www.twilio.com) 15 | 2) Dialogflow (https://dialogflow.com) 16 | 3) Heroku (if you want a one-click install) (https://www.heroku.com) 17 | 18 | ## Technologies 19 | 1) Twilio Speech Recoginition (https://www.twilio.com/speech-recognition) 20 | 2) Twilio Sync (https://www.twilio.com/sync) 21 | 3) Server side app using Python and Twilio REST APIs (Sync) 22 | 4) Dialogflow REST API 23 | 5) Dashboard app components: 24 | 5.1) Javascript framework Vue.js (https://vuejs.org) 25 | 5.2) UI framework Semantic-ui (https://semantic-ui.com) 26 | 5.3) Twilio Sync JS SDK (https://www.twilio.com/docs/api/sync/quickstart-js) 27 | 28 | ## Setup 29 | 30 | ### Configuring Dialogflow 31 | 1) Upload the ASRIntents.zip file into the Dialogflow console (https://dialogflow.com/docs/intents#upload_intents) 32 | 2) Obtain the developer access key from the dialogflow console (https://dialogflow.com/docs/reference/agent/#obtaining_access_tokens) 33 | 3) You'll need this key when you configure during Heroku app install 34 | 35 | ### Obtaining Credentials and Sync Service ID 36 | 1) Log into your Twilio console to get: 37 | 1.1) Account SID and Auth Token from console dashboard 38 | 1.2) API Key and Secret (https://www.twilio.com/console/runtime/api-keys) 39 | 1.3) Create a Sync Service (https://www.twilio.com/console/sync/services) 40 | 2) You'll need the Twilio Credentials and Sync Service ID during the Heroku app install 41 | 42 | ### Create a Sync Map (ASRBotEvents) 43 | Sync Map object is used to store call details along with ASR and Intents. This map is subscribed by the frontend dashboard. 44 | Run the following CURL command: 45 | curl -X POST https://sync.twilio.com/v1/Services//Maps \ 46 | -d 'UniqueName=ASRBotEvents' \ 47 | -u 'YOUR_TWILIO_ACCOUNT_SID:YOUR_TWILIO_AUTH_TOKEN' 48 | 49 | 50 | ### One Click Heroku Deploy of Web App 51 | This will install the wep application and all the dependencies on Heroku (login required). As part of the installation, the Heroku app will walk you through configuration of environment variables. Please click on the following button to deploy the application. 52 | 53 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ameerbadri/twilio-asr-realtime-dashboard) 54 | 55 | ### Configuring Twilio inbound phone number 56 | 1) Log into your Twilio console and purchase a phone number 57 | 2) Assign the URL https:///start?language=en-GB to the phone number 58 | 59 | ### You're all set 60 | Now, Navigate to https:// 61 | 62 | As the phone calls come into your Twilio phone number, the user ASR and Intent will be displayed in the dashboard. 63 | 64 | I look forward to your feedback. 65 | Ameer 66 | https://www.linkedin.com/in/ameer/ -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, Response, jsonify, send_from_directory 3 | # from flask_cors import CORS, cross_origin 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | import json 7 | from urllib.parse import urlencode, quote_plus 8 | from datetime import datetime 9 | from twilio.rest import Client 10 | from twilio.jwt.access_token import AccessToken 11 | from twilio.jwt.access_token.grants import SyncGrant 12 | from twilio.twiml.voice_response import VoiceResponse, Gather 13 | 14 | app = Flask(__name__) 15 | 16 | # Twilio Settings 17 | twilio_account_sid = os.environ["TWILIO_ACCOUNT_SID"] 18 | twilio_auth_token = os.environ["TWILIO_AUTH_TOKEN"] 19 | twilio_api_key = os.environ["TWILIO_API_KEY"] 20 | twilio_api_secret = os.environ["TWILIO_API_SECRET"] 21 | twilio_sync_service_id = os.environ["TWILIO_SYNC_SERVICE_ID"] 22 | apiai_client_access_key = os.environ["APIAPI_CLIENT_ACCESS_KEY"] 23 | 24 | # Create Client to access Twilio resources 25 | client = Client(twilio_account_sid, twilio_auth_token) 26 | 27 | def dialogflow_intent(sessionId, input_speech_text): 28 | apiai_url = "https://api.api.ai/v1/query" 29 | apiai_querystring = {"v": "20150910"} 30 | apiai_language = "en" 31 | # Initialize API.AI Bot 32 | headers = { 33 | 'authorization': "Bearer " + apiai_client_access_key, 34 | 'content-type': "application/json" 35 | } 36 | payload = {'query': input_speech_text, 37 | 'lang': "en", 38 | 'sessionId': sessionId 39 | } 40 | response = requests.request("POST", url=apiai_url, data=json.dumps(payload), headers=headers, params=apiai_querystring) 41 | output = json.loads(response.text) 42 | print (json.dumps(output, indent=2)) 43 | try: 44 | intent = output['result']['metadata']['intentName'] 45 | print ('intentName found') 46 | except: 47 | print('intentName not found') 48 | try: 49 | intent = output['result']['action'] 50 | print('intent Action found') 51 | except: 52 | print('intent Action not found') 53 | intent = "Unknown" 54 | try: 55 | score = str(output['result']['score']) 56 | except: 57 | score = "0.0" 58 | return intent, score 59 | 60 | @app.route('/asr_callback', methods=['POST']) 61 | def asr_callback(): 62 | request_dict = {} 63 | request_dict = request.form.to_dict() 64 | request_dict['initial_question'] = request.values.get('initial_question', '') 65 | language = request.values.get('language', 'en-GB') 66 | request_dict['CallDate'] = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") 67 | item_key = request_dict['CallDate'] + ':' + request_dict['CallSid'] 68 | intent, score = dialogflow_intent(request_dict['CallSid'], request_dict['SpeechResult']) 69 | request_dict['Intent'] = intent 70 | request_dict['IntentScore'] = score 71 | callback_data = json.dumps(request_dict) 72 | # print(callback_data) 73 | new_data = {'Key': item_key, 74 | 'Data': callback_data} 75 | print(new_data) 76 | sync_map = 'ASRBotEvents' 77 | url = 'https://sync.twilio.com/v1/Services/' + twilio_sync_service_id + '/Maps/' + sync_map + '/Items' 78 | response = requests.request("POST", url, data=new_data, auth=HTTPBasicAuth(twilio_account_sid, twilio_auth_token)) 79 | print(response.text) 80 | 81 | # Generate return twiml 82 | resp = VoiceResponse() 83 | resp.say('You said, ' + request_dict['SpeechResult'] + ' , With an intent of: ' + request_dict['Intent'] + ' , Thanks for calling.', language=language, voice='woman') 84 | print (resp) 85 | return str(resp) 86 | 87 | 88 | @app.route('/start', methods=['POST']) 89 | def start(): 90 | request_dict = {} 91 | request_dict = request.form.to_dict() 92 | language = request.values.get('language', 'en-GB') 93 | initial_question = ' Welcome to Customer Care. You can ask questions related to order delivery or store hours and direction. How may I help you? ' 94 | asr_hints = 'I need order status, order status, status, track orders, order delivery, payments, customer service, help' 95 | # Generate initial twiml 96 | values = {'initial_question': initial_question, 97 | 'language': language 98 | } 99 | qs = urlencode(values, quote_via=quote_plus) 100 | callback_url = '/asr_callback?' + qs 101 | resp = VoiceResponse() 102 | gather = Gather(input='speech', hints=asr_hints, language=language, speech_timeout='auto', action=callback_url) 103 | gather.say(initial_question, language=language, voice='woman') 104 | resp.append(gather) 105 | print(resp) 106 | return str(resp) 107 | 108 | @app.route('/') 109 | def index(): 110 | return send_from_directory('static', 'asr_dashboard.html') 111 | 112 | @app.route('/') 113 | def send_js(path): 114 | print (path) 115 | return send_from_directory('static', path) 116 | 117 | @app.route('/token') 118 | def token(): 119 | # get the userid from the incoming request 120 | identity = request.values.get('identity', None) 121 | # Create access token with credentials 122 | token = AccessToken(twilio_account_sid, twilio_api_key, twilio_api_secret, identity=identity) 123 | # Create a Sync grant and add to token 124 | sync_grant = SyncGrant(service_sid=twilio_sync_service_id) 125 | token.add_grant(sync_grant) 126 | # Return token info as JSON 127 | return jsonify(identity=identity, token=token.to_jwt().decode('utf-8')) 128 | 129 | if __name__ == '__main__': 130 | app.run(host='0.0.0.0', debug=True, threaded=True) -------------------------------------------------------------------------------- /static/asr_dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Twilio ASR Dashboard 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Real-Time Speech Recoginition Dashboard

31 | 32 | 33 | 34 | 35 | 36 | 46 | 49 | 50 | 51 |
37 |
38 |
39 | {{totalIVRInteractions}} 40 |
41 |
42 | Most Recent IVR Interactions 43 |
44 |
45 |
47 |
48 | />
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 71 | 74 | 77 | 80 | 83 | 84 | 85 |
Date CreatedFromASRASR ConfidenceIntent (Dialogflow)Intent Score
66 | {{call.CallDate}} 67 | 69 | {{call.From}} 70 | 72 |

{{call.SpeechResult}}

73 |
75 | {{call.ASRConfidence}}% 76 | 78 | {{call.Intent}} 79 | 81 | {{call.IntentScore}} 82 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /static/asr_dashboard.js: -------------------------------------------------------------------------------- 1 | var asrdashboard = new Vue({ 2 | el: '#asrdashboard', 3 | data: { 4 | headerMessage: 'Real-Time Speech Recoginition Dashboard', 5 | loggedUser: "ameer@twilio.com", 6 | userAuthenticated: false, 7 | syncMode: false, 8 | syncStatus: "Disconnected", 9 | callList: [], 10 | maxCallList: 100, 11 | Intents: {"Default": 0} 12 | }, 13 | methods: { 14 | syncRetrieveCallMap: function(data) { 15 | var self = this; 16 | var call = {}; 17 | //var localcallList = []; 18 | //console.log(data[0]); 19 | for (let i = 0 ; i < data.length ; i++ ) { 20 | call = {}; 21 | call['CallSid'] = data[i].value['CallSid']; 22 | call['CallDate'] = data[i].value['CallDate'];; 23 | call['From'] = data[i].value['From']; 24 | call['CallStatus'] = data[i].value['CallStatus']; 25 | call['SpeechResult'] = data[i].value['SpeechResult']; 26 | call['ASRConfidence'] = Math.round((parseFloat(data[i].value['Confidence']) * 100.00)); 27 | call['Intent'] = data[i].value['Intent']; 28 | call['IntentScore'] = Math.round((parseFloat(data[i].value['IntentScore']) * 100.00)); 29 | //localcallList.push(call); 30 | self.callList.push(call); 31 | } 32 | self.callList = _.orderBy(self.callList, ['CallDate'], ['desc']); 33 | }, 34 | syncCallMap: function(data) { 35 | var self = this; 36 | var call = {}; 37 | //console.log(data); 38 | call = {}; 39 | call['CallSid'] = data.value['CallSid']; 40 | call['CallDate'] = data.value['CallDate'];; 41 | call['From'] = data.value['From']; 42 | call['CallStatus'] = data.value['CallStatus']; 43 | call['SpeechResult'] = data.value['SpeechResult']; 44 | call['ASRConfidence'] = Math.round((parseFloat(data.value['Confidence']) * 100.00)); 45 | call['Intent'] = data.value['Intent']; 46 | call['IntentScore'] = Math.round((parseFloat(data.value['IntentScore']) * 100.00)); 47 | self.callList.push(call); 48 | self.callList = _.orderBy(self.callList, ['CallDate'], ['desc']); 49 | var currentLength = self.callList.length; 50 | // Keep the callList array length of maxCallList 51 | if (currentLength > self.maxCallList) 52 | { 53 | delta = currentLength - self.maxCallList; 54 | self.callList.splice(self.maxCallList, delta); 55 | } 56 | } 57 | }, 58 | computed: { 59 | reverseCallList: function() { 60 | // Use lodash provided sort function 61 | return _.orderBy(this.callList, ['CallDate'], ['desc']); 62 | }, 63 | totalIVRInteractions: function () { 64 | return this.callList.length; 65 | }, 66 | callIntentData: function () { 67 | var self = this; 68 | var Intents = {}; 69 | var allIntents = []; 70 | //console.log(self.callList.length) 71 | if (self.callList.length > 0) { 72 | for (var i = 0; i < self.callList.length; i++) { 73 | //console.log(self.callList[i]); 74 | //Intent[self.callList[i]["Intent"]] = Intent[self.callList[i]["Intent"]]; 75 | if ( Intents[self.callList[i]["Intent"]] >= 0 ) 76 | { 77 | Intents[self.callList[i]["Intent"]] += 1 ; 78 | } 79 | else 80 | { 81 | Intents[self.callList[i]["Intent"]] = 1; 82 | } 83 | 84 | } 85 | //console.log(Intents); 86 | 87 | for ( intent in Intents ) 88 | { 89 | allIntents.push( [intent, Intents[intent]]); 90 | } 91 | 92 | //self.Intents = Intent; 93 | } 94 | 95 | console.log("[DEBUG]::" + allIntents); 96 | return allIntents; 97 | }, 98 | callIntentTrendsData: function () { 99 | var self = this; 100 | var intentTrends = []; 101 | var intentTrend = {}; 102 | //console.log(self.callList.length) 103 | if (self.callList.length > 0) { 104 | for (var i = 0; i < self.callList.length; i++) { 105 | console.log(self.callList[i]); 106 | 107 | //Intent[self.callList[i]["Intent"]] = Intent[self.callList[i]["Intent"]]; 108 | let intentName = self.callList[i]["Intent"] ; 109 | let intentDate = self.callList[i]["CallDate"]; 110 | let intentDateFormatted = new Date(intentDate.substring(0,10)); 111 | let currIntent = {}; 112 | 113 | if ( ! intentTrend[intentName] ) 114 | { 115 | currIntent["name"] = intentName ; 116 | currIntent["data"] = {} ; 117 | currIntent["data"][intentDateFormatted] = 1 ; 118 | intentTrend[intentName] = currIntent ; 119 | } 120 | else 121 | { 122 | currIntent = intentTrend[intentName]; 123 | if ( ! currIntent["data"][intentDateFormatted] ) 124 | { 125 | currIntent["data"][intentDateFormatted] = 1 ; 126 | } 127 | else 128 | { 129 | currIntent["data"][intentDateFormatted] += 1 ; 130 | } 131 | 132 | intentTrend[intentName] = currIntent ; 133 | } 134 | } 135 | //console.log("TREND Data" + JSON.stringify(intentTrend)); 136 | 137 | for ( thisIntentTrend in intentTrend ) 138 | { 139 | intentTrends.push( intentTrend[thisIntentTrend] ); 140 | } 141 | //self.Intents = Intent; 142 | } 143 | console.log("[TREND DATA ARRAY]::" + intentTrends); 144 | return intentTrends; 145 | 146 | 147 | } 148 | }, 149 | }) 150 | // Twilio Sync setup 151 | //Our interface to the Sync service 152 | var syncClient; 153 | //We're going to use a Sync Map for this demo 154 | var syncMapName; 155 | var userid = asrdashboard.$data.loggedUser; 156 | var ts = Math.round((new Date()).getTime() / 1000); 157 | var tokenUserid = userid + ts; 158 | asrdashboard.$data.syncEndpoint = tokenUserid; 159 | $.getJSON('/token' + '?identity=' + tokenUserid, function (tokenResponse) { 160 | //Initialize the Sync client 161 | var syncClient = new Twilio.Sync.Client(tokenResponse.token, { logLevel: 'info' }); 162 | asrdashboard.$data.syncStatus = userid + ' Connected'; 163 | //Get current Map and then subsribe to add and update events 164 | syncMapName = 'ASRBotEvents'; 165 | 166 | syncClient.map(syncMapName).then(function(map) { 167 | function processPage(page) { 168 | //page.items.forEach(message => {console.log(message);}); 169 | asrdashboard.syncRetrieveCallMap(page.items); 170 | if (page.hasNextPage) { 171 | page.nextPage().then(processPage); 172 | } else { 173 | console.log("DEBUG:: All Map Items Retrieved."); 174 | } 175 | } 176 | // retrieve 100 items per page 177 | map.getItems({pageSize: 100, order: 'desc'}).then(function(page) { 178 | processPage(page); 179 | }); 180 | console.log("Dashboard Ready!"); 181 | map.on('itemAdded', function(item) { 182 | console.log('New Key: ', item.key); 183 | console.log('New ASRBotEvents Data:', item); 184 | asrdashboard.syncCallMap(item); 185 | }); 186 | }); 187 | }); --------------------------------------------------------------------------------