Let's get started on our agent UI. Assuming you've followed the conventions so far in this tutorial, the UI we create will be accessible using your web browser at:
http://localhost:8080/agents?WorkerSid=WK01234012340123401234
(substitute your Alice's WorkerSid)
We pass the WorkerSid in the URL to avoid implementing complex user management in our demo. In reality, you are likely to store a user's WorkerSid in your database alongside other User attributes.
Let's add on our to our server.rb
file to add an endpoint to generate a page based on a template.
_101require 'rubygems'_101require 'twilio-ruby'_101require 'sinatra'_101require 'json'_101_101set :port, 8080_101_101# Get your Account Sid and Auth Token from twilio.com/user/account_101account_sid = '{{ account_sid }}'_101auth_token = '{{ auth_token }}'_101workspace_sid = '{{ workspace_sid }}'_101workflow_sid = '{{ workflow_sid }}'_101_101@client = Twilio::REST::Client.new(account_sid, auth_token)_101_101post '/assignment_callback' do_101 # Respond to assignment callbacks with accept instruction_101 content_type :json_101 # from must be a verified phone number from your twilio account_101 {_101 "instruction" => "dequeue",_101 "from" => "+15556667777",_101 "post_work_activity_sid" => "WA0123401234..."_101 }.to_json_101end_101_101get '/create-task' do_101 # Create a task_101 task = @client.taskrouter.workspaces(workspace_sid)_101 .tasks_101 .create(_101 attributes: {_101 'selected_language' => 'es'_101 }.to_json,_101 workflow_sid: workflow_sid_101 )_101 task.attributes_101end_101_101get '/accept_reservation' do_101 # Accept a Reservation_101 task_sid = params[:task_sid]_101 reservation_sid = params[:reservation_sid]_101_101 reservation = @client.taskrouter.workspaces(workspace_sid)_101 .tasks(task_sid)_101 .reservations(reservation_sid)_101 .update(reservation_status: 'accepted')_101 reservation.worker_name_101end_101_101get '/incoming_call' do_101 Twilio::TwiML::VoiceResponse.new do |r|_101 r.gather(action: '/enqueue_call', method: 'POST', timeout: 5, num_digits: 1) do |gather|_101 gather.say('Para Español oprime el uno.', language: 'es')_101 gather.say('For English, please hold or press two.', language: 'en')_101 end_101 end.to_s_101end_101_101post '/enqueue_call' do_101 digit_pressed = params[:Digits]_101 if digit_pressed == 1_101 language = "es"_101 else_101 language = "en"_101 end_101_101 attributes = '{"selected_language":"'+language+'"}'_101_101 Twilio::TwiML::Response.new do |r|_101 r.Enqueue workflowSid: workflow_sid do |e|_101 e.Task attributes_101 end_101 end.text_101end_101_101get '/agents' do_101 worker_sid = params['WorkerSid']_101_101 capability = Twilio::JWT::TaskRouterCapability.new(_101 account_sid, auth_token,_101 workspace_sid, worker_sid_101 )_101_101 allow_activity_updates = Twilio::JWT::TaskRouterCapability::Policy.new(_101 Twilio::JWT::TaskRouterCapability::TaskRouterUtils_101 .all_activities(workspace_sid), 'POST', true_101 )_101 capability.add_policy(allow_activity_updates)_101_101 allow_reservation_updates = Twilio::JWT::TaskRouterCapability::Policy.new(_101 Twilio::JWT::TaskRouterCapability::TaskRouterUtils_101 .all_reservations(workspace_sid, worker_sid), 'POST', true_101 )_101 capability.add_policy(allow_reservation_updates)_101_101 worker_token = capability.to_s_101_101 erb :agent, :locals => {:worker_token => worker_token}_101end
Now create a folder called views. Inside that folder, create a erb template file that will be rendered when the URL is requested:
_141<!DOCTYPE html>_141<html>_141<head>_141 <title>Customer Care - Voice Agent Screen</title>_141 <link rel="stylesheet" href="//media.twiliocdn.com/taskrouter/quickstart/agent.css"/>_141 <script src="https://sdk.twilio.com/js/taskrouter/v1.21/taskrouter.min.js" integrity="sha384-5fq+0qjayReAreRyHy38VpD3Gr9R2OYIzonwIkoGI4M9dhfKW6RWeRnZjfwSrpN8" crossorigin="anonymous"></script>_141 <script type="text/javascript">_141 /* Subscribe to a subset of the available TaskRouter.js events for a worker */_141 function registerTaskRouterCallbacks() {_141 worker.on('ready', function(worker) {_141 agentActivityChanged(worker.activityName);_141 logger("Successfully registered as: " + worker.friendlyName)_141 logger("Current activity is: " + worker.activityName);_141 });_141_141 worker.on('activity.update', function(worker) {_141 agentActivityChanged(worker.activityName);_141 logger("Worker activity changed to: " + worker.activityName);_141 });_141_141 worker.on("reservation.created", function(reservation) {_141 logger("-----");_141 logger("You have been reserved to handle a call!");_141 logger("Call from: " + reservation.task.attributes.from);_141 logger("Selected language: " + reservation.task.attributes.selected_language);_141 logger("-----");_141 });_141_141 worker.on("reservation.accepted", function(reservation) {_141 logger("Reservation " + reservation.sid + " accepted!");_141 });_141_141 worker.on("reservation.rejected", function(reservation) {_141 logger("Reservation " + reservation.sid + " rejected!");_141 });_141_141 worker.on("reservation.timeout", function(reservation) {_141 logger("Reservation " + reservation.sid + " timed out!");_141 });_141_141 worker.on("reservation.canceled", function(reservation) {_141 logger("Reservation " + reservation.sid + " canceled!");_141 });_141 }_141_141 /* Hook up the agent Activity buttons to TaskRouter.js */_141_141 function bindAgentActivityButtons() {_141 // Fetch the full list of available Activities from TaskRouter. Store each_141 // ActivitySid against the matching Friendly Name_141 var activitySids = {};_141 worker.activities.fetch(function(error, activityList) {_141 var activities = activityList.data;_141 var i = activities.length;_141 while (i--) {_141 activitySids[activities[i].friendlyName] = activities[i].sid;_141 }_141 });_141_141 /* For each button of class 'change-activity' in our Agent UI, look up the_141 ActivitySid corresponding to the Friendly Name in the button's next-activity_141 data attribute. Use Worker.js to transition the agent to that ActivitySid_141 when the button is clicked.*/_141 var elements = document.getElementsByClassName('change-activity');_141 var i = elements.length;_141 while (i--) {_141 elements[i].onclick = function() {_141 var nextActivity = this.dataset.nextActivity;_141 var nextActivitySid = activitySids[nextActivity];_141 worker.update({"ActivitySid":nextActivitySid});_141 }_141 }_141 }_141_141 /* Update the UI to reflect a change in Activity */_141_141 function agentActivityChanged(activity) {_141 hideAgentActivities();_141 showAgentActivity(activity);_141 }_141_141 function hideAgentActivities() {_141 var elements = document.getElementsByClassName('agent-activity');_141 var i = elements.length;_141 while (i--) {_141 elements[i].style.display = 'none';_141 }_141 }_141_141 function showAgentActivity(activity) {_141 activity = activity.toLowerCase();_141 var elements = document.getElementsByClassName(('agent-activity ' + activity));_141 elements.item(0).style.display = 'block';_141 }_141_141 /* Other stuff */_141_141 function logger(message) {_141 var log = document.getElementById('log');_141 log.value += "\n> " + message;_141 log.scrollTop = log.scrollHeight;_141 }_141_141 window.onload = function() {_141 // Initialize TaskRouter.js on page load using window.workerToken -_141 // a Twilio Capability token that was set from rendering the template with agents endpoint_141 logger("Initializing...");_141 window.worker = new Twilio.TaskRouter.Worker("{{ worker_token }}");_141_141 registerTaskRouterCallbacks();_141 bindAgentActivityButtons();_141 };_141 </script>_141</head>_141<body>_141<div class="content">_141 <section class="agent-activity offline">_141 <p class="activity">Offline</p>_141 <button class="change-activity" data-next-activity="Idle">Go Available</button>_141 </section>_141 <section class="agent-activity idle">_141 <p class="activity"><span>Available</span></p>_141 <button class="change-activity" data-next-activity="Offline">Go Offline</button>_141 </section>_141 <section class="agent-activity reserved">_141 <p class="activity">Reserved</p>_141 </section>_141 <section class="agent-activity busy">_141 <p class="activity">Busy</p>_141 </section>_141 <section class="agent-activity wrapup">_141 <p class="activity">Wrap-Up</p>_141 <button class="change-activity" data-next-activity="Idle">Go Available</button>_141 <button class="change-activity" data-next-activity="Offline">Go Offline</button>_141 </section>_141 <section class="log">_141 <textarea id="log" readonly="true"></textarea>_141 </section>_141</div>_141</body>_141</html>
You'll notice that we included two external files:
And that's it! Open http://localhost:8080/agents?WorkerSid=WK012340123401234
in your browser and you should see the screen below. If you make the same phone call as we made in Part 3, you should see Alice's Activity transition on screen as she is reserved and assigned to handle the Task.
If you see "Initializing..." and no progress, make sure that you have included the correct WorkerSid in the "WorkerSid" request parameter of the URL.
For more details, refer to the TaskRouter JavaScript SDK documentation.