diff --git a/README.md b/README.md index 6d6ef16d..f5bcf541 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ A simple web page that allows users to communicate with the [Azure Health Bot](h 1.Deploy the website: -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FHealthBotContainerSample%2Fmaster%2Fazuredeploy.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FHealthBotContainerSample%2Flive_agent_handoff%2Fazuredeploy.json) 2.Set the following environment variables: -`APP_SECRET` +`AUTH_JWT_SECRET` `WEBCHAT_SECRET` @@ -51,5 +51,19 @@ Pass your preferred geographic endpoint URI by setting the environment variable: **Note:** If you are deploying the code sample using the "Deploy to Azure" option, you should add the above secrets to the application settings for your App Service. -## Agent webchat -If the agent webchat sample is also required, [switch to the live agent handoff branch](https://github.com/Microsoft/HealthBotContainerSample/tree/live_agent_handoff) +## Live agent handoff sample + +The live agent handoff sample is wrapper around the standard webchat that is generally used by end users. This sample is intended for testing the handoff scenario that is built-in to your Health Bot instance. + +To access the sample you should follow the deployment instructions and request the `/agent.html` path from your browser. This will load a dummy login page that illustrates the agent experience (you can provide any values to access the agent portal). Within the agent portal you can issue agent commands to interact with end users that are talking with your bot. + +The wrapper adds to the server.js file an agent flagging function: `function isAgentAuthenticated(req)` which will serve the agent webchat if a `true` value is returned. You should implement custom logic in this function that returns a `true` value once your agent has been authenticated. + +**IMPORTANT:** +The sample login page is for testing and demonstration purposes only. You MUST authenticate agent access in a production deployment of the agent webchat. The agent webchat provides access to sensitive end user information. + +## Customizing the webchat + +You can send programmed messages to the agent webchat by invoking the `function talk(message)`. In the sample we have added example buttons with issue some of the built-in agent commands. + +[Learn more about agent webchat functionality](https://docs.microsoft.com/en-us/HealthBot/handoff) diff --git a/azuredeploy.json b/azuredeploy.json index 38b2c5c5..3fa7836d 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -47,10 +47,10 @@ "description": "Location for all resources." } }, - "appSecret": { + "authJwtSecret": { "type": "securestring", "metadata":{ - "description": "Healthbot application secret." + "description": "Healthbot end user authentication JWT secret." } }, "webchatSecret": { @@ -68,7 +68,7 @@ }, "branch": { "type": "string", - "defaultValue": "master", + "defaultValue": "live_agent_handoff", "metadata": { "description": "The branch of the GitHub repository to use." } @@ -109,8 +109,8 @@ "alwaysOn": "[variables('alwaysOn')]", "appSettings": [ { - "name": "APP_SECRET", - "value": "[parameters('appSecret')]" + "name": "AUTH_JWT_SECRET", + "value": "[parameters('authJwtSecret')]" }, { "name": "WEBCHAT_SECRET", @@ -170,8 +170,8 @@ "siteConfig": { "appSettings": [ { - "name": "APP_SECRET", - "value": "[parameters('appSecret')]" + "name": "AUTH_JWT_SECRET", + "value": "[parameters('authJwtSecret')]" }, { "name": "WEBCHAT_SECRET", diff --git a/public/agent.html b/public/agent.html new file mode 100644 index 00000000..eac639b4 --- /dev/null +++ b/public/agent.html @@ -0,0 +1,48 @@ + + + + + + Health Bot + + + + + + +

Agent Webchat

+

Login page

+ +
+ +
+ +

+ + +
+ +

+ +
+ + + + + + +
+
+ + + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/public/agent.js b/public/agent.js new file mode 100644 index 00000000..7dac50ad --- /dev/null +++ b/public/agent.js @@ -0,0 +1,35 @@ +var logon_form = document.getElementById('logon-form'); +var logon_title = document.getElementById('logon-title'); + +var user_id = document.getElementById('user-id'); +var user_name = document.getElementById('user-name'); + +logon_form.onsubmit = e => { + e.preventDefault(); + logon_form.style.display = 'none'; + logon_title.style.display = 'none'; + + document.querySelector(".agent-buttons").classList.toggle("hidden"); + document.querySelector(".invisible").classList.toggle("invisible"); + + chatRequested({ + userId: user_id.value, + userName: user_name.value, + agent: true + }); +}; + +function talk(message) { + var input = document.querySelectorAll('[data-id]')[0]; + var lastValue = input.value; + input.value = message; + var event = new CustomEvent('input', { bubbles: true }); + event.simulated = true; + var tracker = input._valueTracker; + if (tracker) { + tracker.setValue(lastValue); + } + input.dispatchEvent(event); + var sendButton = document.querySelectorAll(".css-115fwte")[1]; + sendButton.click(); +} diff --git a/public/index.js b/public/index.js index 40575d3a..4cbfc9a6 100644 --- a/public/index.js +++ b/public/index.js @@ -1,6 +1,6 @@ const defaultLocale = 'en-US'; -function requestChatBot(loc) { +function requestChatBot(info, loc) { const params = new URLSearchParams(location.search); const oReq = new XMLHttpRequest(); oReq.addEventListener("load", initBotConversation); @@ -9,12 +9,22 @@ function requestChatBot(loc) { if (loc) { path += "&lat=" + loc.lat + "&long=" + loc.long; } - if (params.has('userId')) { - path += "&userId=" + params.get('userId'); + + const userId = (info && info.userId) || (params.has('userId') ? params.get('userId') : undefined); + if (userId) { + path += "&userId=" + userId; + } + + const userName = (info && info.userName) || (params.has('userName') ? params.get('userName') : undefined); + if (userName) { + path += "&userName=" + userName; } - if (params.has('userName')) { - path += "&userName=" + params.get('userName'); + + if (info && info.agent) { + path += "&agent=true"; } + + oReq.open("POST", path); oReq.send(); } @@ -31,17 +41,17 @@ function extractLocale(localeParam) { } } -function chatRequested() { +function chatRequested(info) { const params = new URLSearchParams(location.search); if (params.has('shareLocation')) { - getUserLocation(requestChatBot); + getUserLocation(info, requestChatBot); } else { - requestChatBot(); + requestChatBot(info); } } -function getUserLocation(callback) { +function getUserLocation(info, callback) { navigator.geolocation.getCurrentPosition( function(position) { var latitude = position.coords.latitude; @@ -50,12 +60,12 @@ function getUserLocation(callback) { lat: latitude, long: longitude } - callback(location); + callback(info, location); }, function(error) { // user declined to share location console.log("location error:" + error.message); - callback(); + callback(info); }); } @@ -134,7 +144,7 @@ function initBotConversation() { else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') { if (action.payload && action.payload.activity && action.payload.activity.type === "event" && action.payload.activity.name === "ShareLocationEvent") { // share - getUserLocation(function (location) { + getUserLocation(null, function (location) { store.dispatch({ type: 'WEB_CHAT/SEND_POST_BACK', payload: { value: JSON.stringify(location) } diff --git a/public/stylesheets/agent.css b/public/stylesheets/agent.css new file mode 100644 index 00000000..1d184c4f --- /dev/null +++ b/public/stylesheets/agent.css @@ -0,0 +1,103 @@ + + body{ + font-family: calibri; + } + + + h1 { + background-color: grey; + color: white; + border: solid gray 1px; + font-weight: 500; + font-size: 20px; + + margin-top: 0; + margin-bottom: 0; + + line-height:30px; + padding-left: 5px; + + position: relative; + top:0; + left: 0; + z-index: 100000; + } + + #webchat-container { + height: calc(100% - 32px); + } + + #webchat { + height: calc(100vh - 36px); + } + + #logon-title{ + font-weight: 700; + + margin-top: 20px; + margin-left: 20px; + } + + input { + width: 300px; + height: 20px; + margin-left: 20px; + } + + button { + width: 130px; + height: 50px; + background-color: grey; + + color: white; + font-size: 16px; + border: none; + outline: none !important; + margin-bottom: 20px; + } + +#submit-btn { + margin-left: 20px; +} + + .hidden{ + display: none; + } + + label { + margin-left: 20px; + } + + #botContainer{ + height: 100%; + } + + + #logon-form{ + margin-top: 10px; + } + + div.wc-header{ + visibility: hidden; + } + + .agent-buttons { + width: 130px; + padding: 10px; + } + + table tr td { + padding: 0; + margin: 0; + + + } + + table { + border: none; + outline: none; + } + + .invisible { + visibility: hidden + } \ No newline at end of file diff --git a/secrets.png b/secrets.png index e4d0fb39..ac0a8f04 100644 Binary files a/secrets.png and b/secrets.png differ diff --git a/server.js b/server.js index 95e18a62..1d68516a 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,7 @@ const fetch = require('node-fetch'); const cookieParser = require('cookie-parser'); const WEBCHAT_SECRET = process.env.WEBCHAT_SECRET; const DIRECTLINE_ENDPOINT_URI = process.env.DIRECTLINE_ENDPOINT_URI; -const APP_SECRET = process.env.APP_SECRET; +const AUTH_JWT_SECRET = process.env.AUTH_JWT_SECRET const directLineTokenEp = `https://${DIRECTLINE_ENDPOINT_URI || "directline.botframework.com"}/v3/directline/tokens/generate`; // Initialize the web app instance, @@ -33,6 +33,11 @@ function isUserAuthenticated(){ return true; } +function isAgentAuthenticated(req) { + // add here the logic to verify the agent is authenticated + return Boolean(req.query.agent); +} + const appConfig = { isHealthy : false, options : { @@ -102,8 +107,11 @@ app.post('/chatBot', async function(req, res) { if (req.query.lat && req.query.long) { response['location'] = {lat: req.query.lat, long: req.query.long}; } + if (isAgentAuthenticated(req)) { + response['isAgent'] = true; + } response['directLineURI'] = DIRECTLINE_ENDPOINT_URI; - const jwtToken = jwt.sign(response, APP_SECRET); + const jwtToken = jwt.sign(response, AUTH_JWT_SECRET); res.send(jwtToken); } catch (err) {