/* Minification failed. Returning unminified contents.
(2984,19-20): run-time warning JS1195: Expected expression: )
(2984,43-44): run-time warning JS1195: Expected expression: >
(2984,132-133): run-time warning JS1002: Syntax error: }
(2984,148-149): run-time warning JS1195: Expected expression: )
(2984,296-297): run-time warning JS1002: Syntax error: }
(2984,359-360): run-time warning JS1197: Too many errors. The file might not be a JavaScript file: >
(2984,181-199): run-time warning JS1018: 'return' statement outside of function: return globalThis;
(2984,203-245): run-time warning JS1018: 'return' statement outside of function: return this||new Function("return this")()
(2984,282-295): run-time warning JS1018: 'return' statement outside of function: return window
 */
/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width",!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoundingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.duration=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)),
void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r});
;
/* Laura Doktorova https://github.com/olado/doT */
(function(){function o(){var a={"&":"&#38;","<":"&#60;",">":"&#62;",'"':"&#34;","'":"&#39;","/":"&#47;"},b=/&(?!#?\w+;)|<|>|"|'|\//g;return function(){return this?this.replace(b,function(c){return a[c]||c}):this}}function p(a,b,c){return(typeof b==="string"?b:b.toString()).replace(a.define||i,function(l,e,f,g){if(e.indexOf("def.")===0)e=e.substring(4);if(!(e in c))if(f===":"){a.defineParams&&g.replace(a.defineParams,function(n,h,d){c[e]={arg:h,text:d}});e in c||(c[e]=g)}else(new Function("def","def['"+
e+"']="+g))(c);return""}).replace(a.use||i,function(l,e){if(a.useParams)e=e.replace(a.useParams,function(g,n,h,d){if(c[h]&&c[h].arg&&d){g=(h+":"+d).replace(/'|\\/g,"_");c.__exp=c.__exp||{};c.__exp[g]=c[h].text.replace(RegExp("(^|[^\\w$])"+c[h].arg+"([^\\w$])","g"),"$1"+d+"$2");return n+"def.__exp['"+g+"']"}});var f=(new Function("def","return "+e))(c);return f?p(a,f,c):f})}function m(a){return a.replace(/\\('|\\)/g,"$1").replace(/[\r\t\n]/g," ")}var j={version:"1.0.1",templateSettings:{evaluate:/\{\{([\s\S]+?(\}?)+)\}\}/g,
interpolate:/\{\{=([\s\S]+?)\}\}/g,encode:/\{\{!([\s\S]+?)\}\}/g,use:/\{\{#([\s\S]+?)\}\}/g,useParams:/(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,define:/\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,defineParams:/^\s*([\w$]+):([\s\S]+)/,conditional:/\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,iterate:/\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,varname:"it",strip:true,append:true,selfcontained:false},template:undefined,
compile:undefined},q;if(typeof module!=="undefined"&&module.exports)module.exports=j;else if(typeof define==="function"&&define.amd)define(function(){return j});else{q=function(){return this||(0,eval)("this")}();q.doT=j}String.prototype.encodeHTML=o();var r={append:{start:"'+(",end:")+'",endencode:"||'').toString().encodeHTML()+'"},split:{start:"';out+=(",end:");out+='",endencode:"||'').toString().encodeHTML();out+='"}},i=/$^/;j.template=function(a,b,c){b=b||j.templateSettings;var l=b.append?r.append:
r.split,e,f=0,g;a=b.use||b.define?p(b,a,c||{}):a;a=("var out='"+(b.strip?a.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ").replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""):a).replace(/'|\\/g,"\\$&").replace(b.interpolate||i,function(h,d){return l.start+m(d)+l.end}).replace(b.encode||i,function(h,d){e=true;return l.start+m(d)+l.endencode}).replace(b.conditional||i,function(h,d,k){return d?k?"';}else if("+m(k)+"){out+='":"';}else{out+='":k?"';if("+m(k)+"){out+='":"';}out+='"}).replace(b.iterate||i,function(h,
d,k,s){if(!d)return"';} } out+='";f+=1;g=s||"i"+f;d=m(d);return"';var arr"+f+"="+d+";if(arr"+f+"){var "+k+","+g+"=-1,l"+f+"=arr"+f+".length-1;while("+g+"<l"+f+"){"+k+"=arr"+f+"["+g+"+=1];out+='"}).replace(b.evaluate||i,function(h,d){return"';"+m(d)+"out+='"})+"';return out;").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\r/g,"\\r").replace(/(\s|;|\}|^|\{)out\+='';/g,"$1").replace(/\+''/g,"").replace(/(\s|;|\}|^|\{)out\+=''\+/g,"$1out+=");if(e&&b.selfcontained)a="String.prototype.encodeHTML=("+
o.toString()+"());"+a;try{return new Function(b.varname,a)}catch(n){typeof console!=="undefined"&&console.log("Could not create a template function: "+a);throw n;}};j.compile=function(a,b){return j.template(a,null,b)}})();
;
//     Underscore.js 1.8.3
//     http://underscorejs.org
//     (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
//     Underscore may be freely distributed under the MIT license.
(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._u=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])<u?i=a+1:o=a}return i},m.indexOf=r(1,m.findIndex,m.sortedIndex),m.lastIndexOf=r(-1,m.findLastIndex),m.range=function(n,t,r){null==t&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e<arguments.length;)i.push(arguments[e++]);return E(n,r,this,this,i)};return r},m.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
//# sourceMappingURL=underscore-min.map;
/* jquery.signalR.core.js */
/*global window:false */
/*!
 * ASP.NET SignalR JavaScript Library v2.2.2
 * http://signalr.net/
 *
 * Copyright (c) .NET Foundation. All rights reserved.
 * Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 *
 */

/// <reference path="Scripts/jquery-1.6.4.js" />
/// <reference path="jquery.signalR.version.js" />
(function ($, window, undefined) {

    var resources = {
        nojQuery: "jQuery was not found. Please ensure jQuery is referenced before the SignalR client JavaScript file.",
        noTransportOnInit: "No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization.",
        errorOnNegotiate: "Error during negotiation request.",
        stoppedWhileLoading: "The connection was stopped during page load.",
        stoppedWhileNegotiating: "The connection was stopped during the negotiate request.",
        errorParsingNegotiateResponse: "Error parsing negotiate response.",
        errorDuringStartRequest: "Error during start request. Stopping the connection.",
        stoppedDuringStartRequest: "The connection was stopped during the start request.",
        errorParsingStartResponse: "Error parsing start response: '{0}'. Stopping the connection.",
        invalidStartResponse: "Invalid start response: '{0}'. Stopping the connection.",
        protocolIncompatible: "You are using a version of the client that isn't compatible with the server. Client version {0}, server version {1}.",
        sendFailed: "Send failed.",
        parseFailed: "Failed at parsing response: {0}",
        longPollFailed: "Long polling request failed.",
        eventSourceFailedToConnect: "EventSource failed to connect.",
        eventSourceError: "Error raised by EventSource",
        webSocketClosed: "WebSocket closed.",
        pingServerFailedInvalidResponse: "Invalid ping response when pinging server: '{0}'.",
        pingServerFailed: "Failed to ping server.",
        pingServerFailedStatusCode: "Failed to ping server.  Server responded with status code {0}, stopping the connection.",
        pingServerFailedParse: "Failed to parse ping server response, stopping the connection.",
        noConnectionTransport: "Connection is in an invalid state, there is no transport active.",
        webSocketsInvalidState: "The Web Socket transport is in an invalid state, transitioning into reconnecting.",
        reconnectTimeout: "Couldn't reconnect within the configured timeout of {0} ms, disconnecting.",
        reconnectWindowTimeout: "The client has been inactive since {0} and it has exceeded the inactivity timeout of {1} ms. Stopping the connection."
    };

    if (typeof ($) !== "function") {
        // no jQuery!
        throw new Error(resources.nojQuery);
    }

    var signalR,
        _connection,
        _pageLoaded = (window.document.readyState === "complete"),
        _pageWindow = $(window),
        _negotiateAbortText = "__Negotiate Aborted__",
        events = {
            onStart: "onStart",
            onStarting: "onStarting",
            onReceived: "onReceived",
            onError: "onError",
            onConnectionSlow: "onConnectionSlow",
            onReconnecting: "onReconnecting",
            onReconnect: "onReconnect",
            onStateChanged: "onStateChanged",
            onDisconnect: "onDisconnect"
        },
        ajaxDefaults = {
            processData: true,
            timeout: null,
            async: true,
            global: false,
            cache: false
        },
        log = function (msg, logging) {
            if (logging === false) {
                return;
            }
            var m;
            if (typeof (window.console) === "undefined") {
                return;
            }
            m = "[" + new Date().toTimeString() + "] SignalR: " + msg;
            if (window.console.debug) {
                window.console.debug(m);
            } else if (window.console.log) {
                window.console.log(m);
            }
        },

        changeState = function (connection, expectedState, newState) {
            if (expectedState === connection.state) {
                connection.state = newState;

                $(connection).triggerHandler(events.onStateChanged, [{ oldState: expectedState, newState: newState }]);
                return true;
            }

            return false;
        },

        isDisconnecting = function (connection) {
            return connection.state === signalR.connectionState.disconnected;
        },

        supportsKeepAlive = function (connection) {
            return connection._.keepAliveData.activated &&
                   connection.transport.supportsKeepAlive(connection);
        },

        configureStopReconnectingTimeout = function (connection) {
            var stopReconnectingTimeout,
                onReconnectTimeout;

            // Check if this connection has already been configured to stop reconnecting after a specified timeout.
            // Without this check if a connection is stopped then started events will be bound multiple times.
            if (!connection._.configuredStopReconnectingTimeout) {
                onReconnectTimeout = function (connection) {
                    var message = signalR._.format(signalR.resources.reconnectTimeout, connection.disconnectTimeout);
                    connection.log(message);
                    $(connection).triggerHandler(events.onError, [signalR._.error(message, /* source */ "TimeoutException")]);
                    connection.stop(/* async */ false, /* notifyServer */ false);
                };

                connection.reconnecting(function () {
                    var connection = this;

                    // Guard against state changing in a previous user defined even handler
                    if (connection.state === signalR.connectionState.reconnecting) {
                        stopReconnectingTimeout = window.setTimeout(function () { onReconnectTimeout(connection); }, connection.disconnectTimeout);
                    }
                });

                connection.stateChanged(function (data) {
                    if (data.oldState === signalR.connectionState.reconnecting) {
                        // Clear the pending reconnect timeout check
                        window.clearTimeout(stopReconnectingTimeout);
                    }
                });

                connection._.configuredStopReconnectingTimeout = true;
            }
        };

    signalR = function (url, qs, logging) {
        /// <summary>Creates a new SignalR connection for the given url</summary>
        /// <param name="url" type="String">The URL of the long polling endpoint</param>
        /// <param name="qs" type="Object">
        ///     [Optional] Custom querystring parameters to add to the connection URL.
        ///     If an object, every non-function member will be added to the querystring.
        ///     If a string, it's added to the QS as specified.
        /// </param>
        /// <param name="logging" type="Boolean">
        ///     [Optional] A flag indicating whether connection logging is enabled to the browser
        ///     console/log. Defaults to false.
        /// </param>

        return new signalR.fn.init(url, qs, logging);
    };

    signalR._ = {
        defaultContentType: "application/x-www-form-urlencoded; charset=UTF-8",

        ieVersion: (function () {
            var version,
                matches;

            if (window.navigator.appName === 'Microsoft Internet Explorer') {
                // Check if the user agent has the pattern "MSIE (one or more numbers).(one or more numbers)";
                matches = /MSIE ([0-9]+\.[0-9]+)/.exec(window.navigator.userAgent);

                if (matches) {
                    version = window.parseFloat(matches[1]);
                }
            }

            // undefined value means not IE
            return version;
        })(),

        error: function (message, source, context) {
            var e = new Error(message);
            e.source = source;

            if (typeof context !== "undefined") {
                e.context = context;
            }

            return e;
        },

        transportError: function (message, transport, source, context) {
            var e = this.error(message, source, context);
            e.transport = transport ? transport.name : undefined;
            return e;
        },

        format: function () {
            /// <summary>Usage: format("Hi {0}, you are {1}!", "Foo", 100) </summary>
            var s = arguments[0];
            for (var i = 0; i < arguments.length - 1; i++) {
                s = s.replace("{" + i + "}", arguments[i + 1]);
            }
            return s;
        },

        firefoxMajorVersion: function (userAgent) {
            // Firefox user agents: http://useragentstring.com/pages/Firefox/
            var matches = userAgent.match(/Firefox\/(\d+)/);
            if (!matches || !matches.length || matches.length < 2) {
                return 0;
            }
            return parseInt(matches[1], 10 /* radix */);
        },

        configurePingInterval: function (connection) {
            var config = connection._.config,
                onFail = function (error) {
                    $(connection).triggerHandler(events.onError, [error]);
                };

            if (config && !connection._.pingIntervalId && config.pingInterval) {
                connection._.pingIntervalId = window.setInterval(function () {
                    signalR.transports._logic.pingServer(connection).fail(onFail);
                }, config.pingInterval);
            }
        }
    };

    signalR.events = events;

    signalR.resources = resources;

    signalR.ajaxDefaults = ajaxDefaults;

    signalR.changeState = changeState;

    signalR.isDisconnecting = isDisconnecting;

    signalR.connectionState = {
        connecting: 0,
        connected: 1,
        reconnecting: 2,
        disconnected: 4
    };

    signalR.hub = {
        start: function () {
            // This will get replaced with the real hub connection start method when hubs is referenced correctly
            throw new Error("SignalR: Error loading hubs. Ensure your hubs reference is correct, e.g. <script src='/signalr/js'></script>.");
        }
    };

    // .on() was added in version 1.7.0, .load() was removed in version 3.0.0 so we fallback to .load() if .on() does
    // not exist to not break existing applications
    if (typeof _pageWindow.on == "function") {
        _pageWindow.on("load", function () { _pageLoaded = true; });
    }
    else {
        _pageWindow.load(function () { _pageLoaded = true; });
    }

    function validateTransport(requestedTransport, connection) {
        /// <summary>Validates the requested transport by cross checking it with the pre-defined signalR.transports</summary>
        /// <param name="requestedTransport" type="Object">The designated transports that the user has specified.</param>
        /// <param name="connection" type="signalR">The connection that will be using the requested transports.  Used for logging purposes.</param>
        /// <returns type="Object" />

        if ($.isArray(requestedTransport)) {
            // Go through transport array and remove an "invalid" tranports
            for (var i = requestedTransport.length - 1; i >= 0; i--) {
                var transport = requestedTransport[i];
                if ($.type(transport) !== "string" || !signalR.transports[transport]) {
                    connection.log("Invalid transport: " + transport + ", removing it from the transports list.");
                    requestedTransport.splice(i, 1);
                }
            }

            // Verify we still have transports left, if we dont then we have invalid transports
            if (requestedTransport.length === 0) {
                connection.log("No transports remain within the specified transport array.");
                requestedTransport = null;
            }
        } else if (!signalR.transports[requestedTransport] && requestedTransport !== "auto") {
            connection.log("Invalid transport: " + requestedTransport.toString() + ".");
            requestedTransport = null;
        } else if (requestedTransport === "auto" && signalR._.ieVersion <= 8) {
            // If we're doing an auto transport and we're IE8 then force longPolling, #1764
            return ["longPolling"];

        }

        return requestedTransport;
    }

    function getDefaultPort(protocol) {
        if (protocol === "http:") {
            return 80;
        } else if (protocol === "https:") {
            return 443;
        }
    }

    function addDefaultPort(protocol, url) {
        // Remove ports  from url.  We have to check if there's a / or end of line
        // following the port in order to avoid removing ports such as 8080.
        if (url.match(/:\d+$/)) {
            return url;
        } else {
            return url + ":" + getDefaultPort(protocol);
        }
    }

    function ConnectingMessageBuffer(connection, drainCallback) {
        var that = this,
            buffer = [];

        that.tryBuffer = function (message) {
            if (connection.state === $.signalR.connectionState.connecting) {
                buffer.push(message);

                return true;
            }

            return false;
        };

        that.drain = function () {
            // Ensure that the connection is connected when we drain (do not want to drain while a connection is not active)
            if (connection.state === $.signalR.connectionState.connected) {
                while (buffer.length > 0) {
                    drainCallback(buffer.shift());
                }
            }
        };

        that.clear = function () {
            buffer = [];
        };
    }

    signalR.fn = signalR.prototype = {
        init: function (url, qs, logging) {
            var $connection = $(this);

            this.url = url;
            this.qs = qs;
            this.lastError = null;
            this._ = {
                keepAliveData: {},
                connectingMessageBuffer: new ConnectingMessageBuffer(this, function (message) {
                    $connection.triggerHandler(events.onReceived, [message]);
                }),
                lastMessageAt: new Date().getTime(),
                lastActiveAt: new Date().getTime(),
                beatInterval: 5000, // Default value, will only be overridden if keep alive is enabled,
                beatHandle: null,
                totalTransportConnectTimeout: 0 // This will be the sum of the TransportConnectTimeout sent in response to negotiate and connection.transportConnectTimeout
            };
            if (typeof (logging) === "boolean") {
                this.logging = logging;
            }
        },

        _parseResponse: function (response) {
            var that = this;

            if (!response) {
                return response;
            } else if (typeof response === "string") {
                return that.json.parse(response);
            } else {
                return response;
            }
        },

        _originalJson: window.JSON,

        json: window.JSON,

        isCrossDomain: function (url, against) {
            /// <summary>Checks if url is cross domain</summary>
            /// <param name="url" type="String">The base URL</param>
            /// <param name="against" type="Object">
            ///     An optional argument to compare the URL against, if not specified it will be set to window.location.
            ///     If specified it must contain a protocol and a host property.
            /// </param>
            var link;

            url = $.trim(url);

            against = against || window.location;

            if (url.indexOf("http") !== 0) {
                return false;
            }

            // Create an anchor tag.
            link = window.document.createElement("a");
            link.href = url;

            // When checking for cross domain we have to special case port 80 because the window.location will remove the
            return link.protocol + addDefaultPort(link.protocol, link.host) !== against.protocol + addDefaultPort(against.protocol, against.host);
        },

        ajaxDataType: "text",

        contentType: "application/json; charset=UTF-8",

        logging: false,

        state: signalR.connectionState.disconnected,

        clientProtocol: "1.5",

        reconnectDelay: 2000,

        transportConnectTimeout: 0,

        disconnectTimeout: 30000, // This should be set by the server in response to the negotiate request (30s default)

        reconnectWindow: 30000, // This should be set by the server in response to the negotiate request

        keepAliveWarnAt: 2 / 3, // Warn user of slow connection if we breach the X% mark of the keep alive timeout

        start: function (options, callback) {
            /// <summary>Starts the connection</summary>
            /// <param name="options" type="Object">Options map</param>
            /// <param name="callback" type="Function">A callback function to execute when the connection has started</param>
            var connection = this,
                config = {
                    pingInterval: 300000,
                    waitForPageLoad: true,
                    transport: "auto",
                    jsonp: false
                },
                initialize,
                deferred = connection._deferral || $.Deferred(), // Check to see if there is a pre-existing deferral that's being built on, if so we want to keep using it
                parser = window.document.createElement("a");

            connection.lastError = null;

            // Persist the deferral so that if start is called multiple times the same deferral is used.
            connection._deferral = deferred;

            if (!connection.json) {
                // no JSON!
                throw new Error("SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8.");
            }

            if ($.type(options) === "function") {
                // Support calling with single callback parameter
                callback = options;
            } else if ($.type(options) === "object") {
                $.extend(config, options);
                if ($.type(config.callback) === "function") {
                    callback = config.callback;
                }
            }

            config.transport = validateTransport(config.transport, connection);

            // If the transport is invalid throw an error and abort start
            if (!config.transport) {
                throw new Error("SignalR: Invalid transport(s) specified, aborting start.");
            }

            connection._.config = config;

            // Check to see if start is being called prior to page load
            // If waitForPageLoad is true we then want to re-direct function call to the window load event
            if (!_pageLoaded && config.waitForPageLoad === true) {
                connection._.deferredStartHandler = function () {
                    connection.start(options, callback);
                };
                _pageWindow.bind("load", connection._.deferredStartHandler);

                return deferred.promise();
            }

            // If we're already connecting just return the same deferral as the original connection start
            if (connection.state === signalR.connectionState.connecting) {
                return deferred.promise();
            } else if (changeState(connection,
                            signalR.connectionState.disconnected,
                            signalR.connectionState.connecting) === false) {
                // We're not connecting so try and transition into connecting.
                // If we fail to transition then we're either in connected or reconnecting.

                deferred.resolve(connection);
                return deferred.promise();
            }

            configureStopReconnectingTimeout(connection);

            // Resolve the full url
            parser.href = connection.url;
            if (!parser.protocol || parser.protocol === ":") {
                connection.protocol = window.document.location.protocol;
                connection.host = parser.host || window.document.location.host;
            } else {
                connection.protocol = parser.protocol;
                connection.host = parser.host;
            }

            connection.baseUrl = connection.protocol + "//" + connection.host;

            // Set the websocket protocol
            connection.wsProtocol = connection.protocol === "https:" ? "wss://" : "ws://";

            // If jsonp with no/auto transport is specified, then set the transport to long polling
            // since that is the only transport for which jsonp really makes sense.
            // Some developers might actually choose to specify jsonp for same origin requests
            // as demonstrated by Issue #623.
            if (config.transport === "auto" && config.jsonp === true) {
                config.transport = "longPolling";
            }

            // If the url is protocol relative, prepend the current windows protocol to the url.
            if (connection.url.indexOf("//") === 0) {
                connection.url = window.location.protocol + connection.url;
                connection.log("Protocol relative URL detected, normalizing it to '" + connection.url + "'.");
            }

            if (this.isCrossDomain(connection.url)) {
                connection.log("Auto detected cross domain url.");

                if (config.transport === "auto") {
                    // TODO: Support XDM with foreverFrame
                    config.transport = ["webSockets", "serverSentEvents", "longPolling"];
                }

                if (typeof (config.withCredentials) === "undefined") {
                    config.withCredentials = true;
                }

                // Determine if jsonp is the only choice for negotiation, ajaxSend and ajaxAbort.
                // i.e. if the browser doesn't supports CORS
                // If it is, ignore any preference to the contrary, and switch to jsonp.
                if (!config.jsonp) {
                    config.jsonp = !$.support.cors;

                    if (config.jsonp) {
                        connection.log("Using jsonp because this browser doesn't support CORS.");
                    }
                }

                connection.contentType = signalR._.defaultContentType;
            }

            connection.withCredentials = config.withCredentials;

            connection.ajaxDataType = config.jsonp ? "jsonp" : "text";

            $(connection).bind(events.onStart, function (e, data) {
                if ($.type(callback) === "function") {
                    callback.call(connection);
                }
                deferred.resolve(connection);
            });

            connection._.initHandler = signalR.transports._logic.initHandler(connection);

            initialize = function (transports, index) {
                var noTransportError = signalR._.error(resources.noTransportOnInit);

                index = index || 0;
                if (index >= transports.length) {
                    if (index === 0) {
                        connection.log("No transports supported by the server were selected.");
                    } else if (index === 1) {
                        connection.log("No fallback transports were selected.");
                    } else {
                        connection.log("Fallback transports exhausted.");
                    }

                    // No transport initialized successfully
                    $(connection).triggerHandler(events.onError, [noTransportError]);
                    deferred.reject(noTransportError);
                    // Stop the connection if it has connected and move it into the disconnected state
                    connection.stop();
                    return;
                }

                // The connection was aborted
                if (connection.state === signalR.connectionState.disconnected) {
                    return;
                }

                var transportName = transports[index],
                    transport = signalR.transports[transportName],
                    onFallback = function () {
                        initialize(transports, index + 1);
                    };

                connection.transport = transport;

                try {
                    connection._.initHandler.start(transport, function () { // success
                        // Firefox 11+ doesn't allow sync XHR withCredentials: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#withCredentials
                        var isFirefox11OrGreater = signalR._.firefoxMajorVersion(window.navigator.userAgent) >= 11,
                            asyncAbort = !!connection.withCredentials && isFirefox11OrGreater;

                        connection.log("The start request succeeded. Transitioning to the connected state.");

                        if (supportsKeepAlive(connection)) {
                            signalR.transports._logic.monitorKeepAlive(connection);
                        }

                        signalR.transports._logic.startHeartbeat(connection);

                        // Used to ensure low activity clients maintain their authentication.
                        // Must be configured once a transport has been decided to perform valid ping requests.
                        signalR._.configurePingInterval(connection);

                        if (!changeState(connection,
                                            signalR.connectionState.connecting,
                                            signalR.connectionState.connected)) {
                            connection.log("WARNING! The connection was not in the connecting state.");
                        }

                        // Drain any incoming buffered messages (messages that came in prior to connect)
                        connection._.connectingMessageBuffer.drain();

                        $(connection).triggerHandler(events.onStart);

                        // wire the stop handler for when the user leaves the page
                        _pageWindow.bind("unload", function () {
                            connection.log("Window unloading, stopping the connection.");

                            connection.stop(asyncAbort);
                        });

                        if (isFirefox11OrGreater) {
                            // Firefox does not fire cross-domain XHRs in the normal unload handler on tab close.
                            // #2400
                            _pageWindow.bind("beforeunload", function () {
                                // If connection.stop() runs runs in beforeunload and fails, it will also fail
                                // in unload unless connection.stop() runs after a timeout.
                                window.setTimeout(function () {
                                    connection.stop(asyncAbort);
                                }, 0);
                            });
                        }
                    }, onFallback);
                }
                catch (error) {
                    connection.log(transport.name + " transport threw '" + error.message + "' when attempting to start.");
                    onFallback();
                }
            };

            var url = connection.url + "/negotiate",
                onFailed = function (error, connection) {
                    var err = signalR._.error(resources.errorOnNegotiate, error, connection._.negotiateRequest);

                    $(connection).triggerHandler(events.onError, err);
                    deferred.reject(err);
                    // Stop the connection if negotiate failed
                    connection.stop();
                };

            $(connection).triggerHandler(events.onStarting);

            url = signalR.transports._logic.prepareQueryString(connection, url);

            connection.log("Negotiating with '" + url + "'.");

            // Save the ajax negotiate request object so we can abort it if stop is called while the request is in flight.
            connection._.negotiateRequest = signalR.transports._logic.ajax(connection, {
                url: url,
                error: function (error, statusText) {
                    // We don't want to cause any errors if we're aborting our own negotiate request.
                    if (statusText !== _negotiateAbortText) {
                        onFailed(error, connection);
                    } else {
                        // This rejection will noop if the deferred has already been resolved or rejected.
                        deferred.reject(signalR._.error(resources.stoppedWhileNegotiating, null /* error */, connection._.negotiateRequest));
                    }
                },
                success: function (result) {
                    var res,
                        keepAliveData,
                        protocolError,
                        transports = [],
                        supportedTransports = [];

                    try {
                        res = connection._parseResponse(result);
                    } catch (error) {
                        onFailed(signalR._.error(resources.errorParsingNegotiateResponse, error), connection);
                        return;
                    }

                    keepAliveData = connection._.keepAliveData;
                    connection.appRelativeUrl = res.Url;
                    connection.id = res.ConnectionId;
                    connection.token = res.ConnectionToken;
                    connection.webSocketServerUrl = res.WebSocketServerUrl;

                    // The long poll timeout is the ConnectionTimeout plus 10 seconds
                    connection._.pollTimeout = res.ConnectionTimeout * 1000 + 10000; // in ms

                    // Once the server has labeled the PersistentConnection as Disconnected, we should stop attempting to reconnect
                    // after res.DisconnectTimeout seconds.
                    connection.disconnectTimeout = res.DisconnectTimeout * 1000; // in ms

                    // Add the TransportConnectTimeout from the response to the transportConnectTimeout from the client to calculate the total timeout
                    connection._.totalTransportConnectTimeout = connection.transportConnectTimeout + res.TransportConnectTimeout * 1000;

                    // If we have a keep alive
                    if (res.KeepAliveTimeout) {
                        // Register the keep alive data as activated
                        keepAliveData.activated = true;

                        // Timeout to designate when to force the connection into reconnecting converted to milliseconds
                        keepAliveData.timeout = res.KeepAliveTimeout * 1000;

                        // Timeout to designate when to warn the developer that the connection may be dead or is not responding.
                        keepAliveData.timeoutWarning = keepAliveData.timeout * connection.keepAliveWarnAt;

                        // Instantiate the frequency in which we check the keep alive.  It must be short in order to not miss/pick up any changes
                        connection._.beatInterval = (keepAliveData.timeout - keepAliveData.timeoutWarning) / 3;
                    } else {
                        keepAliveData.activated = false;
                    }

                    connection.reconnectWindow = connection.disconnectTimeout + (keepAliveData.timeout || 0);

                    if (!res.ProtocolVersion || res.ProtocolVersion !== connection.clientProtocol) {
                        protocolError = signalR._.error(signalR._.format(resources.protocolIncompatible, connection.clientProtocol, res.ProtocolVersion));
                        $(connection).triggerHandler(events.onError, [protocolError]);
                        deferred.reject(protocolError);

                        return;
                    }

                    $.each(signalR.transports, function (key) {
                        if ((key.indexOf("_") === 0) || (key === "webSockets" && !res.TryWebSockets)) {
                            return true;
                        }
                        supportedTransports.push(key);
                    });

                    if ($.isArray(config.transport)) {
                        $.each(config.transport, function (_, transport) {
                            if ($.inArray(transport, supportedTransports) >= 0) {
                                transports.push(transport);
                            }
                        });
                    } else if (config.transport === "auto") {
                        transports = supportedTransports;
                    } else if ($.inArray(config.transport, supportedTransports) >= 0) {
                        transports.push(config.transport);
                    }

                    initialize(transports);
                }
            });

            return deferred.promise();
        },

        starting: function (callback) {
            /// <summary>Adds a callback that will be invoked before anything is sent over the connection</summary>
            /// <param name="callback" type="Function">A callback function to execute before the connection is fully instantiated.</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onStarting, function (e, data) {
                callback.call(connection);
            });
            return connection;
        },

        send: function (data) {
            /// <summary>Sends data over the connection</summary>
            /// <param name="data" type="String">The data to send over the connection</param>
            /// <returns type="signalR" />
            var connection = this;

            if (connection.state === signalR.connectionState.disconnected) {
                // Connection hasn't been started yet
                throw new Error("SignalR: Connection must be started before data can be sent. Call .start() before .send()");
            }

            if (connection.state === signalR.connectionState.connecting) {
                // Connection hasn't been started yet
                throw new Error("SignalR: Connection has not been fully initialized. Use .start().done() or .start().fail() to run logic after the connection has started.");
            }

            connection.transport.send(connection, data);
            // REVIEW: Should we return deferred here?
            return connection;
        },

        received: function (callback) {
            /// <summary>Adds a callback that will be invoked after anything is received over the connection</summary>
            /// <param name="callback" type="Function">A callback function to execute when any data is received on the connection</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onReceived, function (e, data) {
                callback.call(connection, data);
            });
            return connection;
        },

        stateChanged: function (callback) {
            /// <summary>Adds a callback that will be invoked when the connection state changes</summary>
            /// <param name="callback" type="Function">A callback function to execute when the connection state changes</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onStateChanged, function (e, data) {
                callback.call(connection, data);
            });
            return connection;
        },

        error: function (callback) {
            /// <summary>Adds a callback that will be invoked after an error occurs with the connection</summary>
            /// <param name="callback" type="Function">A callback function to execute when an error occurs on the connection</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onError, function (e, errorData, sendData) {
                connection.lastError = errorData;
                // In practice 'errorData' is the SignalR built error object.
                // In practice 'sendData' is undefined for all error events except those triggered by
                // 'ajaxSend' and 'webSockets.send'.'sendData' is the original send payload.
                callback.call(connection, errorData, sendData);
            });
            return connection;
        },

        disconnected: function (callback) {
            /// <summary>Adds a callback that will be invoked when the client disconnects</summary>
            /// <param name="callback" type="Function">A callback function to execute when the connection is broken</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onDisconnect, function (e, data) {
                callback.call(connection);
            });
            return connection;
        },

        connectionSlow: function (callback) {
            /// <summary>Adds a callback that will be invoked when the client detects a slow connection</summary>
            /// <param name="callback" type="Function">A callback function to execute when the connection is slow</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onConnectionSlow, function (e, data) {
                callback.call(connection);
            });

            return connection;
        },

        reconnecting: function (callback) {
            /// <summary>Adds a callback that will be invoked when the underlying transport begins reconnecting</summary>
            /// <param name="callback" type="Function">A callback function to execute when the connection enters a reconnecting state</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onReconnecting, function (e, data) {
                callback.call(connection);
            });
            return connection;
        },

        reconnected: function (callback) {
            /// <summary>Adds a callback that will be invoked when the underlying transport reconnects</summary>
            /// <param name="callback" type="Function">A callback function to execute when the connection is restored</param>
            /// <returns type="signalR" />
            var connection = this;
            $(connection).bind(events.onReconnect, function (e, data) {
                callback.call(connection);
            });
            return connection;
        },

        stop: function (async, notifyServer) {
            /// <summary>Stops listening</summary>
            /// <param name="async" type="Boolean">Whether or not to asynchronously abort the connection</param>
            /// <param name="notifyServer" type="Boolean">Whether we want to notify the server that we are aborting the connection</param>
            /// <returns type="signalR" />
            var connection = this,
                // Save deferral because this is always cleaned up
                deferral = connection._deferral;

            // Verify that we've bound a load event.
            if (connection._.deferredStartHandler) {
                // Unbind the event.
                _pageWindow.unbind("load", connection._.deferredStartHandler);
            }

            // Always clean up private non-timeout based state.
            delete connection._.config;
            delete connection._.deferredStartHandler;

            // This needs to be checked despite the connection state because a connection start can be deferred until page load.
            // If we've deferred the start due to a page load we need to unbind the "onLoad" -> start event.
            if (!_pageLoaded && (!connection._.config || connection._.config.waitForPageLoad === true)) {
                connection.log("Stopping connection prior to negotiate.");

                // If we have a deferral we should reject it
                if (deferral) {
                    deferral.reject(signalR._.error(resources.stoppedWhileLoading));
                }

                // Short-circuit because the start has not been fully started.
                return;
            }

            if (connection.state === signalR.connectionState.disconnected) {
                return;
            }

            connection.log("Stopping connection.");

            // Clear this no matter what
            window.clearTimeout(connection._.beatHandle);
            window.clearInterval(connection._.pingIntervalId);

            if (connection.transport) {
                connection.transport.stop(connection);

                if (notifyServer !== false) {
                    connection.transport.abort(connection, async);
                }

                if (supportsKeepAlive(connection)) {
                    signalR.transports._logic.stopMonitoringKeepAlive(connection);
                }

                connection.transport = null;
            }

            if (connection._.negotiateRequest) {
                // If the negotiation request has already completed this will noop.
                try {
                    connection._.negotiateRequest.abort(_negotiateAbortText);
                } catch (e) { }
                delete connection._.negotiateRequest;
            }

            // Ensure that initHandler.stop() is called before connection._deferral is deleted
            if (connection._.initHandler) {
                connection._.initHandler.stop();
            }

            delete connection._deferral;
            delete connection.messageId;
            delete connection.groupsToken;
            delete connection.id;
            delete connection._.pingIntervalId;
            delete connection._.lastMessageAt;
            delete connection._.lastActiveAt;

            // Clear out our message buffer
            connection._.connectingMessageBuffer.clear();
            
            // Clean up this event
            $(connection).unbind(events.onStart);

            // Trigger the disconnect event
            changeState(connection, connection.state, signalR.connectionState.disconnected);
            $(connection).triggerHandler(events.onDisconnect);

            return connection;
        },

        log: function (msg) {
            log(msg, this.logging);
        }
    };

    signalR.fn.init.prototype = signalR.fn;

    signalR.noConflict = function () {
        /// <summary>Reinstates the original value of $.connection and returns the signalR object for manual assignment</summary>
        /// <returns type="signalR" />
        if ($.connection === signalR) {
            $.connection = _connection;
        }
        return signalR;
    };

    if ($.connection) {
        _connection = $.connection;
    }

    $.connection = $.signalR = signalR;

}(window.jQuery, window));
/* jquery.signalR.transports.common.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

/*global window:false */
/// <reference path="jquery.signalR.core.js" />

(function ($, window, undefined) {

    var signalR = $.signalR,
        events = $.signalR.events,
        changeState = $.signalR.changeState,
        startAbortText = "__Start Aborted__",
        transportLogic;

    signalR.transports = {};

    function beat(connection) {
        if (connection._.keepAliveData.monitoring) {
            checkIfAlive(connection);
        }

        // Ensure that we successfully marked active before continuing the heartbeat.
        if (transportLogic.markActive(connection)) {
            connection._.beatHandle = window.setTimeout(function () {
                beat(connection);
            }, connection._.beatInterval);
        }
    }

    function checkIfAlive(connection) {
        var keepAliveData = connection._.keepAliveData,
            timeElapsed;

        // Only check if we're connected
        if (connection.state === signalR.connectionState.connected) {
            timeElapsed = new Date().getTime() - connection._.lastMessageAt;

            // Check if the keep alive has completely timed out
            if (timeElapsed >= keepAliveData.timeout) {
                connection.log("Keep alive timed out.  Notifying transport that connection has been lost.");

                // Notify transport that the connection has been lost
                connection.transport.lostConnection(connection);
            } else if (timeElapsed >= keepAliveData.timeoutWarning) {
                // This is to assure that the user only gets a single warning
                if (!keepAliveData.userNotified) {
                    connection.log("Keep alive has been missed, connection may be dead/slow.");
                    $(connection).triggerHandler(events.onConnectionSlow);
                    keepAliveData.userNotified = true;
                }
            } else {
                keepAliveData.userNotified = false;
            }
        }
    }

    function getAjaxUrl(connection, path) {
        var url = connection.url + path;

        if (connection.transport) {
            url += "?transport=" + connection.transport.name;
        }

        return transportLogic.prepareQueryString(connection, url);
    }

    function InitHandler(connection) {
        this.connection = connection;

        this.startRequested = false;
        this.startCompleted = false;
        this.connectionStopped = false;
    }

    InitHandler.prototype = {
        start: function (transport, onSuccess, onFallback) {
            var that = this,
                connection = that.connection,
                failCalled = false;

            if (that.startRequested || that.connectionStopped) {
                connection.log("WARNING! " + transport.name + " transport cannot be started. Initialization ongoing or completed.");
                return;
            }

            connection.log(transport.name + " transport starting.");

            transport.start(connection, function () {
                if (!failCalled) {
                    that.initReceived(transport, onSuccess);
                }
            }, function (error) {
                // Don't allow the same transport to cause onFallback to be called twice
                if (!failCalled) {
                    failCalled = true;
                    that.transportFailed(transport, error, onFallback);
                }

                // Returns true if the transport should stop;
                // false if it should attempt to reconnect
                return !that.startCompleted || that.connectionStopped;
            });

            that.transportTimeoutHandle = window.setTimeout(function () {
                if (!failCalled) {
                    failCalled = true;
                    connection.log(transport.name + " transport timed out when trying to connect.");
                    that.transportFailed(transport, undefined, onFallback);
                }
            }, connection._.totalTransportConnectTimeout);
        },

        stop: function () {
            this.connectionStopped = true;
            window.clearTimeout(this.transportTimeoutHandle);
            signalR.transports._logic.tryAbortStartRequest(this.connection);
        },

        initReceived: function (transport, onSuccess) {
            var that = this,
                connection = that.connection;

            if (that.startRequested) {
                connection.log("WARNING! The client received multiple init messages.");
                return;
            }

            if (that.connectionStopped) {
                return;
            }

            that.startRequested = true;
            window.clearTimeout(that.transportTimeoutHandle);

            connection.log(transport.name + " transport connected. Initiating start request.");
            signalR.transports._logic.ajaxStart(connection, function () {
                that.startCompleted = true;
                onSuccess();
            });
        },

        transportFailed: function (transport, error, onFallback) {
            var connection = this.connection,
                deferred = connection._deferral,
                wrappedError;

            if (this.connectionStopped) {
                return;
            }

            window.clearTimeout(this.transportTimeoutHandle);

            if (!this.startRequested) {
                transport.stop(connection);

                connection.log(transport.name + " transport failed to connect. Attempting to fall back.");
                onFallback();
            } else if (!this.startCompleted) {
                // Do not attempt to fall back if a start request is ongoing during a transport failure.
                // Instead, trigger an error and stop the connection.
                wrappedError = signalR._.error(signalR.resources.errorDuringStartRequest, error);

                connection.log(transport.name + " transport failed during the start request. Stopping the connection.");
                $(connection).triggerHandler(events.onError, [wrappedError]);
                if (deferred) {
                    deferred.reject(wrappedError);
                }

                connection.stop();
            } else {
                // The start request has completed, but the connection has not stopped.
                // No need to do anything here. The transport should attempt its normal reconnect logic.
            }
        }
    };

    transportLogic = signalR.transports._logic = {
        ajax: function (connection, options) {
            return $.ajax(
                $.extend(/*deep copy*/ true, {}, $.signalR.ajaxDefaults, {
                    type: "GET",
                    data: {},
                    xhrFields: { withCredentials: connection.withCredentials },
                    contentType: connection.contentType,
                    dataType: connection.ajaxDataType
                }, options));
        },

        pingServer: function (connection) {
            /// <summary>Pings the server</summary>
            /// <param name="connection" type="signalr">Connection associated with the server ping</param>
            /// <returns type="signalR" />
            var url,
                xhr,
                deferral = $.Deferred();

            if (connection.transport) {
                url = connection.url + "/ping";

                url = transportLogic.addQs(url, connection.qs);

                xhr = transportLogic.ajax(connection, {
                    url: url,
                    success: function (result) {
                        var data;

                        try {
                            data = connection._parseResponse(result);
                        }
                        catch (error) {
                            deferral.reject(
                                signalR._.transportError(
                                    signalR.resources.pingServerFailedParse,
                                    connection.transport,
                                    error,
                                    xhr
                                )
                            );
                            connection.stop();
                            return;
                        }

                        if (data.Response === "pong") {
                            deferral.resolve();
                        }
                        else {
                            deferral.reject(
                                signalR._.transportError(
                                    signalR._.format(signalR.resources.pingServerFailedInvalidResponse, result),
                                    connection.transport,
                                    null /* error */,
                                    xhr
                                )
                            );
                        }
                    },
                    error: function (error) {
                        if (error.status === 401 || error.status === 403) {
                            deferral.reject(
                                signalR._.transportError(
                                    signalR._.format(signalR.resources.pingServerFailedStatusCode, error.status),
                                    connection.transport,
                                    error,
                                    xhr
                                )
                            );
                            connection.stop();
                        }
                        else {
                            deferral.reject(
                                signalR._.transportError(
                                    signalR.resources.pingServerFailed,
                                    connection.transport,
                                    error,
                                    xhr
                                )
                            );
                        }
                    }
                });
            }
            else {
                deferral.reject(
                    signalR._.transportError(
                        signalR.resources.noConnectionTransport,
                        connection.transport
                    )
                );
            }

            return deferral.promise();
        },

        prepareQueryString: function (connection, url) {
            var preparedUrl;

            // Use addQs to start since it handles the ?/& prefix for us
            preparedUrl = transportLogic.addQs(url, "clientProtocol=" + connection.clientProtocol);

            // Add the user-specified query string params if any
            preparedUrl = transportLogic.addQs(preparedUrl, connection.qs);

            if (connection.token) {
                preparedUrl += "&connectionToken=" + window.encodeURIComponent(connection.token);
            }

            if (connection.data) {
                preparedUrl += "&connectionData=" + window.encodeURIComponent(connection.data);
            }

            return preparedUrl;
        },

        addQs: function (url, qs) {
            var appender = url.indexOf("?") !== -1 ? "&" : "?",
                firstChar;

            if (!qs) {
                return url;
            }

            if (typeof (qs) === "object") {
                return url + appender + $.param(qs);
            }

            if (typeof (qs) === "string") {
                firstChar = qs.charAt(0);

                if (firstChar === "?" || firstChar === "&") {
                    appender = "";
                }

                return url + appender + qs;
            }

            throw new Error("Query string property must be either a string or object.");
        },

        // BUG #2953: The url needs to be same otherwise it will cause a memory leak
        getUrl: function (connection, transport, reconnecting, poll, ajaxPost) {
            /// <summary>Gets the url for making a GET based connect request</summary>
            var baseUrl = transport === "webSockets" ? "" : connection.baseUrl,
                url = baseUrl + connection.appRelativeUrl,
                qs = "transport=" + transport;

            if (!ajaxPost && connection.groupsToken) {
                qs += "&groupsToken=" + window.encodeURIComponent(connection.groupsToken);
            }

            if (!reconnecting) {
                url += "/connect";
            } else {
                if (poll) {
                    // longPolling transport specific
                    url += "/poll";
                } else {
                    url += "/reconnect";
                }

                if (!ajaxPost && connection.messageId) {
                    qs += "&messageId=" + window.encodeURIComponent(connection.messageId);
                }
            }
            url += "?" + qs;
            url = transportLogic.prepareQueryString(connection, url);

            if (!ajaxPost) {
                url += "&tid=" + Math.floor(Math.random() * 11);
            }

            return url;
        },

        maximizePersistentResponse: function (minPersistentResponse) {
            return {
                MessageId: minPersistentResponse.C,
                Messages: minPersistentResponse.M,
                Initialized: typeof (minPersistentResponse.S) !== "undefined" ? true : false,
                ShouldReconnect: typeof (minPersistentResponse.T) !== "undefined" ? true : false,
                LongPollDelay: minPersistentResponse.L,
                GroupsToken: minPersistentResponse.G
            };
        },

        updateGroups: function (connection, groupsToken) {
            if (groupsToken) {
                connection.groupsToken = groupsToken;
            }
        },

        stringifySend: function (connection, message) {
            if (typeof (message) === "string" || typeof (message) === "undefined" || message === null) {
                return message;
            }
            return connection.json.stringify(message);
        },

        ajaxSend: function (connection, data) {
            var payload = transportLogic.stringifySend(connection, data),
                url = getAjaxUrl(connection, "/send"),
                xhr,
                onFail = function (error, connection) {
                    $(connection).triggerHandler(events.onError, [signalR._.transportError(signalR.resources.sendFailed, connection.transport, error, xhr), data]);
                };


            xhr = transportLogic.ajax(connection, {
                url: url,
                type: connection.ajaxDataType === "jsonp" ? "GET" : "POST",
                contentType: signalR._.defaultContentType,
                data: {
                    data: payload
                },
                success: function (result) {
                    var res;

                    if (result) {
                        try {
                            res = connection._parseResponse(result);
                        }
                        catch (error) {
                            onFail(error, connection);
                            connection.stop();
                            return;
                        }

                        transportLogic.triggerReceived(connection, res);
                    }
                },
                error: function (error, textStatus) {
                    if (textStatus === "abort" || textStatus === "parsererror") {
                        // The parsererror happens for sends that don't return any data, and hence
                        // don't write the jsonp callback to the response. This is harder to fix on the server
                        // so just hack around it on the client for now.
                        return;
                    }

                    onFail(error, connection);
                }
            });

            return xhr;
        },

        ajaxAbort: function (connection, async) {
            if (typeof (connection.transport) === "undefined") {
                return;
            }

            // Async by default unless explicitly overidden
            async = typeof async === "undefined" ? true : async;

            var url = getAjaxUrl(connection, "/abort");

            transportLogic.ajax(connection, {
                url: url,
                async: async,
                timeout: 1000,
                type: "POST"
            });

            connection.log("Fired ajax abort async = " + async + ".");
        },

        ajaxStart: function (connection, onSuccess) {
            var rejectDeferred = function (error) {
                    var deferred = connection._deferral;
                    if (deferred) {
                        deferred.reject(error);
                    }
                },
                triggerStartError = function (error) {
                    connection.log("The start request failed. Stopping the connection.");
                    $(connection).triggerHandler(events.onError, [error]);
                    rejectDeferred(error);
                    connection.stop();
                };

            connection._.startRequest = transportLogic.ajax(connection, {
                url: getAjaxUrl(connection, "/start"),
                success: function (result, statusText, xhr) {
                    var data;

                    try {
                        data = connection._parseResponse(result);
                    } catch (error) {
                        triggerStartError(signalR._.error(
                            signalR._.format(signalR.resources.errorParsingStartResponse, result),
                            error, xhr));
                        return;
                    }

                    if (data.Response === "started") {
                        onSuccess();
                    } else {
                        triggerStartError(signalR._.error(
                            signalR._.format(signalR.resources.invalidStartResponse, result),
                            null /* error */, xhr));
                    }
                },
                error: function (xhr, statusText, error) {
                    if (statusText !== startAbortText) {
                        triggerStartError(signalR._.error(
                            signalR.resources.errorDuringStartRequest,
                            error, xhr));
                    } else {
                        // Stop has been called, no need to trigger the error handler
                        // or stop the connection again with onStartError
                        connection.log("The start request aborted because connection.stop() was called.");
                        rejectDeferred(signalR._.error(
                            signalR.resources.stoppedDuringStartRequest,
                            null /* error */, xhr));
                    }
                }
            });
        },

        tryAbortStartRequest: function (connection) {
            if (connection._.startRequest) {
                // If the start request has already completed this will noop.
                try {
                    connection._.startRequest.abort(startAbortText);
                } catch (e) { }
                delete connection._.startRequest;
            }
        },

        tryInitialize: function (connection, persistentResponse, onInitialized) {
            if (persistentResponse.Initialized && onInitialized) {
                onInitialized();
            } else if (persistentResponse.Initialized) {
                connection.log("WARNING! The client received an init message after reconnecting.");
            }

        },

        triggerReceived: function (connection, data) {
            if (!connection._.connectingMessageBuffer.tryBuffer(data)) {
                $(connection).triggerHandler(events.onReceived, [data]);
            }
        },

        processMessages: function (connection, minData, onInitialized) {
            var data;

            // Update the last message time stamp
            transportLogic.markLastMessage(connection);

            if (minData) {
                data = transportLogic.maximizePersistentResponse(minData);

                transportLogic.updateGroups(connection, data.GroupsToken);

                if (data.MessageId) {
                    connection.messageId = data.MessageId;
                }

                if (data.Messages) {
                    $.each(data.Messages, function (index, message) {
                        transportLogic.triggerReceived(connection, message);
                    });

                    transportLogic.tryInitialize(connection, data, onInitialized);
                }
            }
        },

        monitorKeepAlive: function (connection) {
            var keepAliveData = connection._.keepAliveData;

            // If we haven't initiated the keep alive timeouts then we need to
            if (!keepAliveData.monitoring) {
                keepAliveData.monitoring = true;

                transportLogic.markLastMessage(connection);

                // Save the function so we can unbind it on stop
                connection._.keepAliveData.reconnectKeepAliveUpdate = function () {
                    // Mark a new message so that keep alive doesn't time out connections
                    transportLogic.markLastMessage(connection);
                };

                // Update Keep alive on reconnect
                $(connection).bind(events.onReconnect, connection._.keepAliveData.reconnectKeepAliveUpdate);

                connection.log("Now monitoring keep alive with a warning timeout of " + keepAliveData.timeoutWarning + ", keep alive timeout of " + keepAliveData.timeout + " and disconnecting timeout of " + connection.disconnectTimeout);
            } else {
                connection.log("Tried to monitor keep alive but it's already being monitored.");
            }
        },

        stopMonitoringKeepAlive: function (connection) {
            var keepAliveData = connection._.keepAliveData;

            // Only attempt to stop the keep alive monitoring if its being monitored
            if (keepAliveData.monitoring) {
                // Stop monitoring
                keepAliveData.monitoring = false;

                // Remove the updateKeepAlive function from the reconnect event
                $(connection).unbind(events.onReconnect, connection._.keepAliveData.reconnectKeepAliveUpdate);

                // Clear all the keep alive data
                connection._.keepAliveData = {};
                connection.log("Stopping the monitoring of the keep alive.");
            }
        },

        startHeartbeat: function (connection) {
            connection._.lastActiveAt = new Date().getTime();
            beat(connection);
        },

        markLastMessage: function (connection) {
            connection._.lastMessageAt = new Date().getTime();
        },

        markActive: function (connection) {
            if (transportLogic.verifyLastActive(connection)) {
                connection._.lastActiveAt = new Date().getTime();
                return true;
            }

            return false;
        },

        isConnectedOrReconnecting: function (connection) {
            return connection.state === signalR.connectionState.connected ||
                   connection.state === signalR.connectionState.reconnecting;
        },

        ensureReconnectingState: function (connection) {
            if (changeState(connection,
                        signalR.connectionState.connected,
                        signalR.connectionState.reconnecting) === true) {
                $(connection).triggerHandler(events.onReconnecting);
            }
            return connection.state === signalR.connectionState.reconnecting;
        },

        clearReconnectTimeout: function (connection) {
            if (connection && connection._.reconnectTimeout) {
                window.clearTimeout(connection._.reconnectTimeout);
                delete connection._.reconnectTimeout;
            }
        },

        verifyLastActive: function (connection) {
            if (new Date().getTime() - connection._.lastActiveAt >= connection.reconnectWindow) {
                var message = signalR._.format(signalR.resources.reconnectWindowTimeout, new Date(connection._.lastActiveAt), connection.reconnectWindow);
                connection.log(message);
                $(connection).triggerHandler(events.onError, [signalR._.error(message, /* source */ "TimeoutException")]);
                connection.stop(/* async */ false, /* notifyServer */ false);
                return false;
            }

            return true;
        },

        reconnect: function (connection, transportName) {
            var transport = signalR.transports[transportName];

            // We should only set a reconnectTimeout if we are currently connected
            // and a reconnectTimeout isn't already set.
            if (transportLogic.isConnectedOrReconnecting(connection) && !connection._.reconnectTimeout) {
                // Need to verify before the setTimeout occurs because an application sleep could occur during the setTimeout duration.
                if (!transportLogic.verifyLastActive(connection)) {
                    return;
                }

                connection._.reconnectTimeout = window.setTimeout(function () {
                    if (!transportLogic.verifyLastActive(connection)) {
                        return;
                    }

                    transport.stop(connection);

                    if (transportLogic.ensureReconnectingState(connection)) {
                        connection.log(transportName + " reconnecting.");
                        transport.start(connection);
                    }
                }, connection.reconnectDelay);
            }
        },

        handleParseFailure: function (connection, result, error, onFailed, context) {
            var wrappedError = signalR._.transportError(
                signalR._.format(signalR.resources.parseFailed, result),
                connection.transport,
                error,
                context);

            // If we're in the initialization phase trigger onFailed, otherwise stop the connection.
            if (onFailed && onFailed(wrappedError)) {
                connection.log("Failed to parse server response while attempting to connect.");
            } else {
                $(connection).triggerHandler(events.onError, [wrappedError]);
                connection.stop();
            }
        },

        initHandler: function (connection) {
            return new InitHandler(connection);
        },

        foreverFrame: {
            count: 0,
            connections: {}
        }
    };

}(window.jQuery, window));
/* jquery.signalR.transports.webSockets.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


/*global window:false */
/// <reference path="jquery.signalR.transports.common.js" />

(function ($, window, undefined) {

    var signalR = $.signalR,
        events = $.signalR.events,
        changeState = $.signalR.changeState,
        transportLogic = signalR.transports._logic;

    signalR.transports.webSockets = {
        name: "webSockets",

        supportsKeepAlive: function () {
            return true;
        },

        send: function (connection, data) {
            var payload = transportLogic.stringifySend(connection, data);

            try {
                connection.socket.send(payload);
            } catch (ex) {
                $(connection).triggerHandler(events.onError,
                    [signalR._.transportError(
                        signalR.resources.webSocketsInvalidState,
                        connection.transport,
                        ex,
                        connection.socket
                    ),
                    data]);
            }
        },

        start: function (connection, onSuccess, onFailed) {
            var url,
                opened = false,
                that = this,
                reconnecting = !onSuccess,
                $connection = $(connection);

            if (!window.WebSocket) {
                onFailed();
                return;
            }

            if (!connection.socket) {
                if (connection.webSocketServerUrl) {
                    url = connection.webSocketServerUrl;
                } else {
                    url = connection.wsProtocol + connection.host;
                }

                url += transportLogic.getUrl(connection, this.name, reconnecting);

                connection.log("Connecting to websocket endpoint '" + url + "'.");
                connection.socket = new window.WebSocket(url);

                connection.socket.onopen = function () {
                    opened = true;
                    connection.log("Websocket opened.");

                    transportLogic.clearReconnectTimeout(connection);

                    if (changeState(connection,
                                    signalR.connectionState.reconnecting,
                                    signalR.connectionState.connected) === true) {
                        $connection.triggerHandler(events.onReconnect);
                    }
                };

                connection.socket.onclose = function (event) {
                    var error;

                    // Only handle a socket close if the close is from the current socket.
                    // Sometimes on disconnect the server will push down an onclose event
                    // to an expired socket.

                    if (this === connection.socket) {
                        if (opened && typeof event.wasClean !== "undefined" && event.wasClean === false) {
                            // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but
                            // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers.
                            error = signalR._.transportError(
                                signalR.resources.webSocketClosed,
                                connection.transport,
                                event);

                            connection.log("Unclean disconnect from websocket: " + (event.reason || "[no reason given]."));
                        } else {
                            connection.log("Websocket closed.");
                        }

                        if (!onFailed || !onFailed(error)) {
                            if (error) {
                                $(connection).triggerHandler(events.onError, [error]);
                            }

                            that.reconnect(connection);
                        }
                    }
                };

                connection.socket.onmessage = function (event) {
                    var data;

                    try {
                        data = connection._parseResponse(event.data);
                    }
                    catch (error) {
                        transportLogic.handleParseFailure(connection, event.data, error, onFailed, event);
                        return;
                    }

                    if (data) {
                        // data.M is PersistentResponse.Messages
                        if ($.isEmptyObject(data) || data.M) {
                            transportLogic.processMessages(connection, data, onSuccess);
                        } else {
                            // For websockets we need to trigger onReceived
                            // for callbacks to outgoing hub calls.
                            transportLogic.triggerReceived(connection, data);
                        }
                    }
                };
            }
        },

        reconnect: function (connection) {
            transportLogic.reconnect(connection, this.name);
        },

        lostConnection: function (connection) {
            this.reconnect(connection);
        },

        stop: function (connection) {
            // Don't trigger a reconnect after stopping
            transportLogic.clearReconnectTimeout(connection);

            if (connection.socket) {
                connection.log("Closing the Websocket.");
                connection.socket.close();
                connection.socket = null;
            }
        },

        abort: function (connection, async) {
            transportLogic.ajaxAbort(connection, async);
        }
    };

}(window.jQuery, window));
/* jquery.signalR.transports.serverSentEvents.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


/*global window:false */
/// <reference path="jquery.signalR.transports.common.js" />

(function ($, window, undefined) {

    var signalR = $.signalR,
        events = $.signalR.events,
        changeState = $.signalR.changeState,
        transportLogic = signalR.transports._logic,
        clearReconnectAttemptTimeout = function (connection) {
            window.clearTimeout(connection._.reconnectAttemptTimeoutHandle);
            delete connection._.reconnectAttemptTimeoutHandle;
        };

    signalR.transports.serverSentEvents = {
        name: "serverSentEvents",

        supportsKeepAlive: function () {
            return true;
        },

        timeOut: 3000,

        start: function (connection, onSuccess, onFailed) {
            var that = this,
                opened = false,
                $connection = $(connection),
                reconnecting = !onSuccess,
                url;

            if (connection.eventSource) {
                connection.log("The connection already has an event source. Stopping it.");
                connection.stop();
            }

            if (!window.EventSource) {
                if (onFailed) {
                    connection.log("This browser doesn't support SSE.");
                    onFailed();
                }
                return;
            }

            url = transportLogic.getUrl(connection, this.name, reconnecting);

            try {
                connection.log("Attempting to connect to SSE endpoint '" + url + "'.");
                connection.eventSource = new window.EventSource(url, { withCredentials: connection.withCredentials });
            }
            catch (e) {
                connection.log("EventSource failed trying to connect with error " + e.Message + ".");
                if (onFailed) {
                    // The connection failed, call the failed callback
                    onFailed();
                } else {
                    $connection.triggerHandler(events.onError, [signalR._.transportError(signalR.resources.eventSourceFailedToConnect, connection.transport, e)]);
                    if (reconnecting) {
                        // If we were reconnecting, rather than doing initial connect, then try reconnect again
                        that.reconnect(connection);
                    }
                }
                return;
            }

            if (reconnecting) {
                connection._.reconnectAttemptTimeoutHandle = window.setTimeout(function () {
                    if (opened === false) {
                        // If we're reconnecting and the event source is attempting to connect,
                        // don't keep retrying. This causes duplicate connections to spawn.
                        if (connection.eventSource.readyState !== window.EventSource.OPEN) {
                            // If we were reconnecting, rather than doing initial connect, then try reconnect again
                            that.reconnect(connection);
                        }
                    }
                },
                that.timeOut);
            }

            connection.eventSource.addEventListener("open", function (e) {
                connection.log("EventSource connected.");

                clearReconnectAttemptTimeout(connection);
                transportLogic.clearReconnectTimeout(connection);

                if (opened === false) {
                    opened = true;

                    if (changeState(connection,
                                         signalR.connectionState.reconnecting,
                                         signalR.connectionState.connected) === true) {
                        $connection.triggerHandler(events.onReconnect);
                    }
                }
            }, false);

            connection.eventSource.addEventListener("message", function (e) {
                var res;

                // process messages
                if (e.data === "initialized") {
                    return;
                }

                try {
                    res = connection._parseResponse(e.data);
                }
                catch (error) {
                    transportLogic.handleParseFailure(connection, e.data, error, onFailed, e);
                    return;
                }

                transportLogic.processMessages(connection, res, onSuccess);
            }, false);

            connection.eventSource.addEventListener("error", function (e) {
                var error = signalR._.transportError(
                    signalR.resources.eventSourceError,
                    connection.transport,
                    e);

                // Only handle an error if the error is from the current Event Source.
                // Sometimes on disconnect the server will push down an error event
                // to an expired Event Source.
                if (this !== connection.eventSource) {
                    return;
                }

                if (onFailed && onFailed(error)) {
                    return;
                }

                connection.log("EventSource readyState: " + connection.eventSource.readyState + ".");

                if (e.eventPhase === window.EventSource.CLOSED) {
                    // We don't use the EventSource's native reconnect function as it
                    // doesn't allow us to change the URL when reconnecting. We need
                    // to change the URL to not include the /connect suffix, and pass
                    // the last message id we received.
                    connection.log("EventSource reconnecting due to the server connection ending.");
                    that.reconnect(connection);
                } else {
                    // connection error
                    connection.log("EventSource error.");
                    $connection.triggerHandler(events.onError, [error]);
                }
            }, false);
        },

        reconnect: function (connection) {
            transportLogic.reconnect(connection, this.name);
        },

        lostConnection: function (connection) {
            this.reconnect(connection);
        },

        send: function (connection, data) {
            transportLogic.ajaxSend(connection, data);
        },

        stop: function (connection) {
            // Don't trigger a reconnect after stopping
            clearReconnectAttemptTimeout(connection);
            transportLogic.clearReconnectTimeout(connection);

            if (connection && connection.eventSource) {
                connection.log("EventSource calling close().");
                connection.eventSource.close();
                connection.eventSource = null;
                delete connection.eventSource;
            }
        },

        abort: function (connection, async) {
            transportLogic.ajaxAbort(connection, async);
        }
    };

}(window.jQuery, window));
/* jquery.signalR.transports.foreverFrame.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


/*global window:false */
/// <reference path="jquery.signalR.transports.common.js" />

(function ($, window, undefined) {

    var signalR = $.signalR,
        events = $.signalR.events,
        changeState = $.signalR.changeState,
        transportLogic = signalR.transports._logic,
        createFrame = function () {
            var frame = window.document.createElement("iframe");
            frame.setAttribute("style", "position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;");
            return frame;
        },
        // Used to prevent infinite loading icon spins in older versions of ie
        // We build this object inside a closure so we don't pollute the rest of
        // the foreverFrame transport with unnecessary functions/utilities.
        loadPreventer = (function () {
            var loadingFixIntervalId = null,
                loadingFixInterval = 1000,
                attachedTo = 0;

            return {
                prevent: function () {
                    // Prevent additional iframe removal procedures from newer browsers
                    if (signalR._.ieVersion <= 8) {
                        // We only ever want to set the interval one time, so on the first attachedTo
                        if (attachedTo === 0) {
                            // Create and destroy iframe every 3 seconds to prevent loading icon, super hacky
                            loadingFixIntervalId = window.setInterval(function () {
                                var tempFrame = createFrame();

                                window.document.body.appendChild(tempFrame);
                                window.document.body.removeChild(tempFrame);

                                tempFrame = null;
                            }, loadingFixInterval);
                        }

                        attachedTo++;
                    }
                },
                cancel: function () {
                    // Only clear the interval if there's only one more object that the loadPreventer is attachedTo
                    if (attachedTo === 1) {
                        window.clearInterval(loadingFixIntervalId);
                    }

                    if (attachedTo > 0) {
                        attachedTo--;
                    }
                }
            };
        })();

    signalR.transports.foreverFrame = {
        name: "foreverFrame",

        supportsKeepAlive: function () {
            return true;
        },

        // Added as a value here so we can create tests to verify functionality
        iframeClearThreshold: 50,

        start: function (connection, onSuccess, onFailed) {
            var that = this,
                frameId = (transportLogic.foreverFrame.count += 1),
                url,
                frame = createFrame(),
                frameLoadHandler = function () {
                    connection.log("Forever frame iframe finished loading and is no longer receiving messages.");
                    if (!onFailed || !onFailed()) {
                        that.reconnect(connection);
                    }
                };

            if (window.EventSource) {
                // If the browser supports SSE, don't use Forever Frame
                if (onFailed) {
                    connection.log("Forever Frame is not supported by SignalR on browsers with SSE support.");
                    onFailed();
                }
                return;
            }

            frame.setAttribute("data-signalr-connection-id", connection.id);

            // Start preventing loading icon
            // This will only perform work if the loadPreventer is not attached to another connection.
            loadPreventer.prevent();

            // Build the url
            url = transportLogic.getUrl(connection, this.name);
            url += "&frameId=" + frameId;

            // add frame to the document prior to setting URL to avoid caching issues.
            window.document.documentElement.appendChild(frame);

            connection.log("Binding to iframe's load event.");

            if (frame.addEventListener) {
                frame.addEventListener("load", frameLoadHandler, false);
            } else if (frame.attachEvent) {
                frame.attachEvent("onload", frameLoadHandler);
            }

            frame.src = url;
            transportLogic.foreverFrame.connections[frameId] = connection;

            connection.frame = frame;
            connection.frameId = frameId;

            if (onSuccess) {
                connection.onSuccess = function () {
                    connection.log("Iframe transport started.");
                    onSuccess();
                };
            }
        },

        reconnect: function (connection) {
            var that = this;

            // Need to verify connection state and verify before the setTimeout occurs because an application sleep could occur during the setTimeout duration.
            if (transportLogic.isConnectedOrReconnecting(connection) && transportLogic.verifyLastActive(connection)) {
                window.setTimeout(function () {
                    // Verify that we're ok to reconnect.
                    if (!transportLogic.verifyLastActive(connection)) {
                        return;
                    }

                    if (connection.frame && transportLogic.ensureReconnectingState(connection)) {
                        var frame = connection.frame,
                            src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId;
                        connection.log("Updating iframe src to '" + src + "'.");
                        frame.src = src;
                    }
                }, connection.reconnectDelay);
            }
        },

        lostConnection: function (connection) {
            this.reconnect(connection);
        },

        send: function (connection, data) {
            transportLogic.ajaxSend(connection, data);
        },

        receive: function (connection, data) {
            var cw,
                body,
                response;

            if (connection.json !== connection._originalJson) {
                // If there's a custom JSON parser configured then serialize the object
                // using the original (browser) JSON parser and then deserialize it using
                // the custom parser (connection._parseResponse does that). This is so we
                // can easily send the response from the server as "raw" JSON but still
                // support custom JSON deserialization in the browser.
                data = connection._originalJson.stringify(data);
            }

            response = connection._parseResponse(data);

            transportLogic.processMessages(connection, response, connection.onSuccess);

            // Protect against connection stopping from a callback trigger within the processMessages above.
            if (connection.state === $.signalR.connectionState.connected) {
                // Delete the script & div elements
                connection.frameMessageCount = (connection.frameMessageCount || 0) + 1;
                if (connection.frameMessageCount > signalR.transports.foreverFrame.iframeClearThreshold) {
                    connection.frameMessageCount = 0;
                    cw = connection.frame.contentWindow || connection.frame.contentDocument;
                    if (cw && cw.document && cw.document.body) {
                        body = cw.document.body;

                        // Remove all the child elements from the iframe's body to conserver memory
                        while (body.firstChild) {
                            body.removeChild(body.firstChild);
                        }
                    }
                }
            }
        },

        stop: function (connection) {
            var cw = null;

            // Stop attempting to prevent loading icon
            loadPreventer.cancel();

            if (connection.frame) {
                if (connection.frame.stop) {
                    connection.frame.stop();
                } else {
                    try {
                        cw = connection.frame.contentWindow || connection.frame.contentDocument;
                        if (cw.document && cw.document.execCommand) {
                            cw.document.execCommand("Stop");
                        }
                    }
                    catch (e) {
                        connection.log("Error occurred when stopping foreverFrame transport. Message = " + e.message + ".");
                    }
                }

                // Ensure the iframe is where we left it
                if (connection.frame.parentNode === window.document.documentElement) {
                    window.document.documentElement.removeChild(connection.frame);
                }

                delete transportLogic.foreverFrame.connections[connection.frameId];
                connection.frame = null;
                connection.frameId = null;
                delete connection.frame;
                delete connection.frameId;
                delete connection.onSuccess;
                delete connection.frameMessageCount;
                connection.log("Stopping forever frame.");
            }
        },

        abort: function (connection, async) {
            transportLogic.ajaxAbort(connection, async);
        },

        getConnection: function (id) {
            return transportLogic.foreverFrame.connections[id];
        },

        started: function (connection) {
            if (changeState(connection,
                signalR.connectionState.reconnecting,
                signalR.connectionState.connected) === true) {

                $(connection).triggerHandler(events.onReconnect);
            }
        }
    };

}(window.jQuery, window));
/* jquery.signalR.transports.longPolling.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


/*global window:false */
/// <reference path="jquery.signalR.transports.common.js" />

(function ($, window, undefined) {

    var signalR = $.signalR,
        events = $.signalR.events,
        changeState = $.signalR.changeState,
        isDisconnecting = $.signalR.isDisconnecting,
        transportLogic = signalR.transports._logic;

    signalR.transports.longPolling = {
        name: "longPolling",

        supportsKeepAlive: function () {
            return false;
        },

        reconnectDelay: 3000,

        start: function (connection, onSuccess, onFailed) {
            /// <summary>Starts the long polling connection</summary>
            /// <param name="connection" type="signalR">The SignalR connection to start</param>
            var that = this,
                fireConnect = function () {
                    fireConnect = $.noop;

                    connection.log("LongPolling connected.");

                    if (onSuccess) {
                        onSuccess();
                    } else {
                        connection.log("WARNING! The client received an init message after reconnecting.");
                    }
                },
                tryFailConnect = function (error) {
                    if (onFailed(error)) {
                        connection.log("LongPolling failed to connect.");
                        return true;
                    }

                    return false;
                },
                privateData = connection._,
                reconnectErrors = 0,
                fireReconnected = function (instance) {
                    window.clearTimeout(privateData.reconnectTimeoutId);
                    privateData.reconnectTimeoutId = null;

                    if (changeState(instance,
                                    signalR.connectionState.reconnecting,
                                    signalR.connectionState.connected) === true) {
                        // Successfully reconnected!
                        instance.log("Raising the reconnect event");
                        $(instance).triggerHandler(events.onReconnect);
                    }
                },
                // 1 hour
                maxFireReconnectedTimeout = 3600000;

            if (connection.pollXhr) {
                connection.log("Polling xhr requests already exists, aborting.");
                connection.stop();
            }

            connection.messageId = null;

            privateData.reconnectTimeoutId = null;

            privateData.pollTimeoutId = window.setTimeout(function () {
                (function poll(instance, raiseReconnect) {
                    var messageId = instance.messageId,
                        connect = (messageId === null),
                        reconnecting = !connect,
                        polling = !raiseReconnect,
                        url = transportLogic.getUrl(instance, that.name, reconnecting, polling, true /* use Post for longPolling */),
                        postData = {};

                    if (instance.messageId) {
                        postData.messageId = instance.messageId;
                    }

                    if (instance.groupsToken) {
                        postData.groupsToken = instance.groupsToken;
                    }

                    // If we've disconnected during the time we've tried to re-instantiate the poll then stop.
                    if (isDisconnecting(instance) === true) {
                        return;
                    }

                    connection.log("Opening long polling request to '" + url + "'.");
                    instance.pollXhr = transportLogic.ajax(connection, {
                        xhrFields: {
                            onprogress: function () {
                                transportLogic.markLastMessage(connection);
                            }
                        },
                        url: url,
                        type: "POST",
                        contentType: signalR._.defaultContentType,
                        data: postData,
                        timeout: connection._.pollTimeout,
                        success: function (result) {
                            var minData,
                                delay = 0,
                                data,
                                shouldReconnect;

                            connection.log("Long poll complete.");

                            // Reset our reconnect errors so if we transition into a reconnecting state again we trigger
                            // reconnected quickly
                            reconnectErrors = 0;

                            try {
                                // Remove any keep-alives from the beginning of the result
                                minData = connection._parseResponse(result);
                            }
                            catch (error) {
                                transportLogic.handleParseFailure(instance, result, error, tryFailConnect, instance.pollXhr);
                                return;
                            }

                            // If there's currently a timeout to trigger reconnect, fire it now before processing messages
                            if (privateData.reconnectTimeoutId !== null) {
                                fireReconnected(instance);
                            }

                            if (minData) {
                                data = transportLogic.maximizePersistentResponse(minData);
                            }

                            transportLogic.processMessages(instance, minData, fireConnect);

                            if (data &&
                                $.type(data.LongPollDelay) === "number") {
                                delay = data.LongPollDelay;
                            }

                            if (isDisconnecting(instance) === true) {
                                return;
                            }

                            shouldReconnect = data && data.ShouldReconnect;
                            if (shouldReconnect) {
                                // Transition into the reconnecting state
                                // If this fails then that means that the user transitioned the connection into a invalid state in processMessages.
                                if (!transportLogic.ensureReconnectingState(instance)) {
                                    return;
                                }
                            }

                            // We never want to pass a raiseReconnect flag after a successful poll.  This is handled via the error function
                            if (delay > 0) {
                                privateData.pollTimeoutId = window.setTimeout(function () {
                                    poll(instance, shouldReconnect);
                                }, delay);
                            } else {
                                poll(instance, shouldReconnect);
                            }
                        },

                        error: function (data, textStatus) {
                            var error = signalR._.transportError(signalR.resources.longPollFailed, connection.transport, data, instance.pollXhr);

                            // Stop trying to trigger reconnect, connection is in an error state
                            // If we're not in the reconnect state this will noop
                            window.clearTimeout(privateData.reconnectTimeoutId);
                            privateData.reconnectTimeoutId = null;

                            if (textStatus === "abort") {
                                connection.log("Aborted xhr request.");
                                return;
                            }

                            if (!tryFailConnect(error)) {

                                // Increment our reconnect errors, we assume all errors to be reconnect errors
                                // In the case that it's our first error this will cause Reconnect to be fired
                                // after 1 second due to reconnectErrors being = 1.
                                reconnectErrors++;

                                if (connection.state !== signalR.connectionState.reconnecting) {
                                    connection.log("An error occurred using longPolling. Status = " + textStatus + ".  Response = " + data.responseText + ".");
                                    $(instance).triggerHandler(events.onError, [error]);
                                }

                                // We check the state here to verify that we're not in an invalid state prior to verifying Reconnect.
                                // If we're not in connected or reconnecting then the next ensureReconnectingState check will fail and will return.
                                // Therefore we don't want to change that failure code path.
                                if ((connection.state === signalR.connectionState.connected ||
                                    connection.state === signalR.connectionState.reconnecting) &&
                                    !transportLogic.verifyLastActive(connection)) {
                                    return;
                                }

                                // Transition into the reconnecting state
                                // If this fails then that means that the user transitioned the connection into the disconnected or connecting state within the above error handler trigger.
                                if (!transportLogic.ensureReconnectingState(instance)) {
                                    return;
                                }

                                // Call poll with the raiseReconnect flag as true after the reconnect delay
                                privateData.pollTimeoutId = window.setTimeout(function () {
                                    poll(instance, true);
                                }, that.reconnectDelay);
                            }
                        }
                    });

                    // This will only ever pass after an error has occurred via the poll ajax procedure.
                    if (reconnecting && raiseReconnect === true) {
                        // We wait to reconnect depending on how many times we've failed to reconnect.
                        // This is essentially a heuristic that will exponentially increase in wait time before
                        // triggering reconnected.  This depends on the "error" handler of Poll to cancel this
                        // timeout if it triggers before the Reconnected event fires.
                        // The Math.min at the end is to ensure that the reconnect timeout does not overflow.
                        privateData.reconnectTimeoutId = window.setTimeout(function () { fireReconnected(instance); }, Math.min(1000 * (Math.pow(2, reconnectErrors) - 1), maxFireReconnectedTimeout));
                    }
                }(connection));
            }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab
        },

        lostConnection: function (connection) {
            if (connection.pollXhr) {
                connection.pollXhr.abort("lostConnection");
            }
        },

        send: function (connection, data) {
            transportLogic.ajaxSend(connection, data);
        },

        stop: function (connection) {
            /// <summary>Stops the long polling connection</summary>
            /// <param name="connection" type="signalR">The SignalR connection to stop</param>

            window.clearTimeout(connection._.pollTimeoutId);
            window.clearTimeout(connection._.reconnectTimeoutId);

            delete connection._.pollTimeoutId;
            delete connection._.reconnectTimeoutId;

            if (connection.pollXhr) {
                connection.pollXhr.abort();
                connection.pollXhr = null;
                delete connection.pollXhr;
            }
        },

        abort: function (connection, async) {
            transportLogic.ajaxAbort(connection, async);
        }
    };

}(window.jQuery, window));
/* jquery.signalR.hubs.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

/*global window:false */
/// <reference path="jquery.signalR.core.js" />

(function ($, window, undefined) {

    var eventNamespace = ".hubProxy",
        signalR = $.signalR;

    function makeEventName(event) {
        return event + eventNamespace;
    }

    // Equivalent to Array.prototype.map
    function map(arr, fun, thisp) {
        var i,
            length = arr.length,
            result = [];
        for (i = 0; i < length; i += 1) {
            if (arr.hasOwnProperty(i)) {
                result[i] = fun.call(thisp, arr[i], i, arr);
            }
        }
        return result;
    }

    function getArgValue(a) {
        return $.isFunction(a) ? null : ($.type(a) === "undefined" ? null : a);
    }

    function hasMembers(obj) {
        for (var key in obj) {
            // If we have any properties in our callback map then we have callbacks and can exit the loop via return
            if (obj.hasOwnProperty(key)) {
                return true;
            }
        }

        return false;
    }

    function clearInvocationCallbacks(connection, error) {
        /// <param name="connection" type="hubConnection" />
        var callbacks = connection._.invocationCallbacks,
            callback;

        if (hasMembers(callbacks)) {
            connection.log("Clearing hub invocation callbacks with error: " + error + ".");
        }

        // Reset the callback cache now as we have a local var referencing it
        connection._.invocationCallbackId = 0;
        delete connection._.invocationCallbacks;
        connection._.invocationCallbacks = {};

        // Loop over the callbacks and invoke them.
        // We do this using a local var reference and *after* we've cleared the cache
        // so that if a fail callback itself tries to invoke another method we don't
        // end up with its callback in the list we're looping over.
        for (var callbackId in callbacks) {
            callback = callbacks[callbackId];
            callback.method.call(callback.scope, { E: error });
        }
    }

    // hubProxy
    function hubProxy(hubConnection, hubName) {
        /// <summary>
        ///     Creates a new proxy object for the given hub connection that can be used to invoke
        ///     methods on server hubs and handle client method invocation requests from the server.
        /// </summary>
        return new hubProxy.fn.init(hubConnection, hubName);
    }

    hubProxy.fn = hubProxy.prototype = {
        init: function (connection, hubName) {
            this.state = {};
            this.connection = connection;
            this.hubName = hubName;
            this._ = {
                callbackMap: {}
            };
        },

        constructor: hubProxy,

        hasSubscriptions: function () {
            return hasMembers(this._.callbackMap);
        },

        on: function (eventName, callback) {
            /// <summary>Wires up a callback to be invoked when a invocation request is received from the server hub.</summary>
            /// <param name="eventName" type="String">The name of the hub event to register the callback for.</param>
            /// <param name="callback" type="Function">The callback to be invoked.</param>
            var that = this,
                callbackMap = that._.callbackMap;

            // Normalize the event name to lowercase
            eventName = eventName.toLowerCase();

            // If there is not an event registered for this callback yet we want to create its event space in the callback map.
            if (!callbackMap[eventName]) {
                callbackMap[eventName] = {};
            }

            // Map the callback to our encompassed function
            callbackMap[eventName][callback] = function (e, data) {
                callback.apply(that, data);
            };

            $(that).bind(makeEventName(eventName), callbackMap[eventName][callback]);

            return that;
        },

        off: function (eventName, callback) {
            /// <summary>Removes the callback invocation request from the server hub for the given event name.</summary>
            /// <param name="eventName" type="String">The name of the hub event to unregister the callback for.</param>
            /// <param name="callback" type="Function">The callback to be invoked.</param>
            var that = this,
                callbackMap = that._.callbackMap,
                callbackSpace;

            // Normalize the event name to lowercase
            eventName = eventName.toLowerCase();

            callbackSpace = callbackMap[eventName];

            // Verify that there is an event space to unbind
            if (callbackSpace) {
                // Only unbind if there's an event bound with eventName and a callback with the specified callback
                if (callbackSpace[callback]) {
                    $(that).unbind(makeEventName(eventName), callbackSpace[callback]);

                    // Remove the callback from the callback map
                    delete callbackSpace[callback];

                    // Check if there are any members left on the event, if not we need to destroy it.
                    if (!hasMembers(callbackSpace)) {
                        delete callbackMap[eventName];
                    }
                } else if (!callback) { // Check if we're removing the whole event and we didn't error because of an invalid callback
                    $(that).unbind(makeEventName(eventName));

                    delete callbackMap[eventName];
                }
            }

            return that;
        },

        invoke: function (methodName) {
            /// <summary>Invokes a server hub method with the given arguments.</summary>
            /// <param name="methodName" type="String">The name of the server hub method.</param>

            var that = this,
                connection = that.connection,
                args = $.makeArray(arguments).slice(1),
                argValues = map(args, getArgValue),
                data = { H: that.hubName, M: methodName, A: argValues, I: connection._.invocationCallbackId },
                d = $.Deferred(),
                callback = function (minResult) {
                    var result = that._maximizeHubResponse(minResult),
                        source,
                        error;

                    // Update the hub state
                    $.extend(that.state, result.State);

                    if (result.Progress) {
                        if (d.notifyWith) {
                            // Progress is only supported in jQuery 1.7+
                            d.notifyWith(that, [result.Progress.Data]);
                        } else if(!connection._.progressjQueryVersionLogged) {
                            connection.log("A hub method invocation progress update was received but the version of jQuery in use (" + $.prototype.jquery + ") does not support progress updates. Upgrade to jQuery 1.7+ to receive progress notifications.");
                            connection._.progressjQueryVersionLogged = true;
                        }
                    } else if (result.Error) {
                        // Server hub method threw an exception, log it & reject the deferred
                        if (result.StackTrace) {
                            connection.log(result.Error + "\n" + result.StackTrace + ".");
                        }

                        // result.ErrorData is only set if a HubException was thrown
                        source = result.IsHubException ? "HubException" : "Exception";
                        error = signalR._.error(result.Error, source);
                        error.data = result.ErrorData;

                        connection.log(that.hubName + "." + methodName + " failed to execute. Error: " + error.message);
                        d.rejectWith(that, [error]);
                    } else {
                        // Server invocation succeeded, resolve the deferred
                        connection.log("Invoked " + that.hubName + "." + methodName);
                        d.resolveWith(that, [result.Result]);
                    }
                };

            connection._.invocationCallbacks[connection._.invocationCallbackId.toString()] = { scope: that, method: callback };
            connection._.invocationCallbackId += 1;

            if (!$.isEmptyObject(that.state)) {
                data.S = that.state;
            }

            connection.log("Invoking " + that.hubName + "." + methodName);
            connection.send(data);

            return d.promise();
        },

        _maximizeHubResponse: function (minHubResponse) {
            return {
                State: minHubResponse.S,
                Result: minHubResponse.R,
                Progress: minHubResponse.P ? {
                    Id: minHubResponse.P.I,
                    Data: minHubResponse.P.D
                } : null,
                Id: minHubResponse.I,
                IsHubException: minHubResponse.H,
                Error: minHubResponse.E,
                StackTrace: minHubResponse.T,
                ErrorData: minHubResponse.D
            };
        }
    };

    hubProxy.fn.init.prototype = hubProxy.fn;

    // hubConnection
    function hubConnection(url, options) {
        /// <summary>Creates a new hub connection.</summary>
        /// <param name="url" type="String">[Optional] The hub route url, defaults to "/signalr".</param>
        /// <param name="options" type="Object">[Optional] Settings to use when creating the hubConnection.</param>
        var settings = {
            qs: null,
            logging: false,
            useDefaultPath: true
        };

        $.extend(settings, options);

        if (!url || settings.useDefaultPath) {
            url = (url || "") + "/signalr";
        }
        return new hubConnection.fn.init(url, settings);
    }

    hubConnection.fn = hubConnection.prototype = $.connection();

    hubConnection.fn.init = function (url, options) {
        var settings = {
                qs: null,
                logging: false,
                useDefaultPath: true
            },
            connection = this;

        $.extend(settings, options);

        // Call the base constructor
        $.signalR.fn.init.call(connection, url, settings.qs, settings.logging);

        // Object to store hub proxies for this connection
        connection.proxies = {};

        connection._.invocationCallbackId = 0;
        connection._.invocationCallbacks = {};

        // Wire up the received handler
        connection.received(function (minData) {
            var data, proxy, dataCallbackId, callback, hubName, eventName;
            if (!minData) {
                return;
            }

            // We have to handle progress updates first in order to ensure old clients that receive
            // progress updates enter the return value branch and then no-op when they can't find
            // the callback in the map (because the minData.I value will not be a valid callback ID)
            if (typeof (minData.P) !== "undefined") {
                // Process progress notification
                dataCallbackId = minData.P.I.toString();
                callback = connection._.invocationCallbacks[dataCallbackId];
                if (callback) {
                    callback.method.call(callback.scope, minData);
                }
            } else if (typeof (minData.I) !== "undefined") {
                // We received the return value from a server method invocation, look up callback by id and call it
                dataCallbackId = minData.I.toString();
                callback = connection._.invocationCallbacks[dataCallbackId];
                if (callback) {
                    // Delete the callback from the proxy
                    connection._.invocationCallbacks[dataCallbackId] = null;
                    delete connection._.invocationCallbacks[dataCallbackId];

                    // Invoke the callback
                    callback.method.call(callback.scope, minData);
                }
            } else {
                data = this._maximizeClientHubInvocation(minData);

                // We received a client invocation request, i.e. broadcast from server hub
                connection.log("Triggering client hub event '" + data.Method + "' on hub '" + data.Hub + "'.");

                // Normalize the names to lowercase
                hubName = data.Hub.toLowerCase();
                eventName = data.Method.toLowerCase();

                // Trigger the local invocation event
                proxy = this.proxies[hubName];

                // Update the hub state
                $.extend(proxy.state, data.State);
                $(proxy).triggerHandler(makeEventName(eventName), [data.Args]);
            }
        });

        connection.error(function (errData, origData) {
            var callbackId, callback;

            if (!origData) {
                // No original data passed so this is not a send error
                return;
            }

            callbackId = origData.I;
            callback = connection._.invocationCallbacks[callbackId];

            // Verify that there is a callback bound (could have been cleared)
            if (callback) {
                // Delete the callback
                connection._.invocationCallbacks[callbackId] = null;
                delete connection._.invocationCallbacks[callbackId];

                // Invoke the callback with an error to reject the promise
                callback.method.call(callback.scope, { E: errData });
            }
        });

        connection.reconnecting(function () {
            if (connection.transport && connection.transport.name === "webSockets") {
                clearInvocationCallbacks(connection, "Connection started reconnecting before invocation result was received.");
            }
        });

        connection.disconnected(function () {
            clearInvocationCallbacks(connection, "Connection was disconnected before invocation result was received.");
        });
    };

    hubConnection.fn._maximizeClientHubInvocation = function (minClientHubInvocation) {
        return {
            Hub: minClientHubInvocation.H,
            Method: minClientHubInvocation.M,
            Args: minClientHubInvocation.A,
            State: minClientHubInvocation.S
        };
    };

    hubConnection.fn._registerSubscribedHubs = function () {
        /// <summary>
        ///     Sets the starting event to loop through the known hubs and register any new hubs
        ///     that have been added to the proxy.
        /// </summary>
        var connection = this;

        if (!connection._subscribedToHubs) {
            connection._subscribedToHubs = true;
            connection.starting(function () {
                // Set the connection's data object with all the hub proxies with active subscriptions.
                // These proxies will receive notifications from the server.
                var subscribedHubs = [];

                $.each(connection.proxies, function (key) {
                    if (this.hasSubscriptions()) {
                        subscribedHubs.push({ name: key });
                        connection.log("Client subscribed to hub '" + key + "'.");
                    }
                });

                if (subscribedHubs.length === 0) {
                    connection.log("No hubs have been subscribed to.  The client will not receive data from hubs.  To fix, declare at least one client side function prior to connection start for each hub you wish to subscribe to.");
                }

                connection.data = connection.json.stringify(subscribedHubs);
            });
        }
    };

    hubConnection.fn.createHubProxy = function (hubName) {
        /// <summary>
        ///     Creates a new proxy object for the given hub connection that can be used to invoke
        ///     methods on server hubs and handle client method invocation requests from the server.
        /// </summary>
        /// <param name="hubName" type="String">
        ///     The name of the hub on the server to create the proxy for.
        /// </param>

        // Normalize the name to lowercase
        hubName = hubName.toLowerCase();

        var proxy = this.proxies[hubName];
        if (!proxy) {
            proxy = hubProxy(this, hubName);
            this.proxies[hubName] = proxy;
        }

        this._registerSubscribedHubs();

        return proxy;
    };

    hubConnection.fn.init.prototype = hubConnection.fn;

    $.hubConnection = hubConnection;

}(window.jQuery, window));
/* jquery.signalR.version.js */
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.


/*global window:false */
/// <reference path="jquery.signalR.core.js" />
(function ($, undefined) {
    $.signalR.version = "2.2.2";
}(window.jQuery));
;
var t,e;t=self,e=()=>(()=>{var t={d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})}};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),t.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"t",{value:!0})};var e,s={};t.r(s),t.d(s,{AbortError:()=>r,DefaultHttpClient:()=>H,HttpClient:()=>d,HttpError:()=>i,HttpResponse:()=>u,HttpTransportType:()=>F,HubConnection:()=>q,HubConnectionBuilder:()=>tt,HubConnectionState:()=>A,JsonHubProtocol:()=>Y,LogLevel:()=>e,MessageType:()=>x,NullLogger:()=>f,Subject:()=>U,TimeoutError:()=>n,TransferFormat:()=>B,VERSION:()=>p});class i extends Error{constructor(t,e){const s=new.target.prototype;super(`${t}: Status code '${e}'`),this.statusCode=e,this.__proto__=s}}class n extends Error{constructor(t="A timeout occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class r extends Error{constructor(t="An abort occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class o extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="UnsupportedTransportError",this.__proto__=s}}class h extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="DisabledTransportError",this.__proto__=s}}class c extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="FailedToStartTransportError",this.__proto__=s}}class a extends Error{constructor(t){const e=new.target.prototype;super(t),this.errorType="FailedToNegotiateWithServerError",this.__proto__=e}}class l extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.innerErrors=e,this.__proto__=s}}class u{constructor(t,e,s){this.statusCode=t,this.statusText=e,this.content=s}}class d{get(t,e){return this.send({...e,method:"GET",url:t})}post(t,e){return this.send({...e,method:"POST",url:t})}delete(t,e){return this.send({...e,method:"DELETE",url:t})}getCookieString(t){return""}}!function(t){t[t.Trace=0]="Trace",t[t.Debug=1]="Debug",t[t.Information=2]="Information",t[t.Warning=3]="Warning",t[t.Error=4]="Error",t[t.Critical=5]="Critical",t[t.None=6]="None"}(e||(e={}));class f{constructor(){}log(t,e){}}f.instance=new f;const p="8.0.0";class w{static isRequired(t,e){if(null==t)throw new Error(`The '${e}' argument is required.`)}static isNotEmpty(t,e){if(!t||t.match(/^\s*$/))throw new Error(`The '${e}' argument should not be empty.`)}static isIn(t,e,s){if(!(t in e))throw new Error(`Unknown ${s} value: ${t}.`)}}class g{static get isBrowser(){return!g.isNode&&"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return!g.isNode&&"object"==typeof self&&"importScripts"in self}static get isReactNative(){return!g.isNode&&"object"==typeof window&&void 0===window.document}static get isNode(){return"undefined"!=typeof process&&process.release&&"node"===process.release.name}}function m(t,e){let s="";return y(t)?(s=`Binary data of length ${t.byteLength}`,e&&(s+=`. Content: '${function(t){const e=new Uint8Array(t);let s="";return e.forEach((t=>{s+=`0x${t<16?"0":""}${t.toString(16)} `})),s.substr(0,s.length-1)}(t)}'`)):"string"==typeof t&&(s=`String data of length ${t.length}`,e&&(s+=`. Content: '${t}'`)),s}function y(t){return t&&"undefined"!=typeof ArrayBuffer&&(t instanceof ArrayBuffer||t.constructor&&"ArrayBuffer"===t.constructor.name)}async function b(t,s,i,n,r,o){const h={},[c,a]=$();h[c]=a,t.log(e.Trace,`(${s} transport) sending data. ${m(r,o.logMessageContent)}.`);const l=y(r)?"arraybuffer":"text",u=await i.post(n,{content:r,headers:{...h,...o.headers},responseType:l,timeout:o.timeout,withCredentials:o.withCredentials});t.log(e.Trace,`(${s} transport) request complete. Response status: ${u.statusCode}.`)}class v{constructor(t,e){this.i=t,this.h=e}dispose(){const t=this.i.observers.indexOf(this.h);t>-1&&this.i.observers.splice(t,1),0===this.i.observers.length&&this.i.cancelCallback&&this.i.cancelCallback().catch((t=>{}))}}class E{constructor(t){this.l=t,this.out=console}log(t,s){if(t>=this.l){const i=`[${(new Date).toISOString()}] ${e[t]}: ${s}`;switch(t){case e.Critical:case e.Error:this.out.error(i);break;case e.Warning:this.out.warn(i);break;case e.Information:this.out.info(i);break;default:this.out.log(i)}}}}function $(){let t="X-SignalR-User-Agent";return g.isNode&&(t="User-Agent"),[t,C(p,S(),g.isNode?"NodeJS":"Browser",k())]}function C(t,e,s,i){let n="Microsoft SignalR/";const r=t.split(".");return n+=`${r[0]}.${r[1]}`,n+=` (${t}; `,n+=e&&""!==e?`${e}; `:"Unknown OS; ",n+=`${s}`,n+=i?`; ${i}`:"; Unknown Runtime Version",n+=")",n}function S(){if(!g.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function k(){if(g.isNode)return process.versions.node}function P(t){return t.stack?t.stack:t.message?t.message:`${t}`}class T extends d{constructor(e){super(),this.u=e,this.p=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==t.g)return t.g;throw new Error("could not find global")}()),this.m=AbortController,this.m}async send(t){if(t.abortSignal&&t.abortSignal.aborted)throw new r;if(!t.method)throw new Error("No method defined.");if(!t.url)throw new Error("No url defined.");const s=new this.m;let o;t.abortSignal&&(t.abortSignal.onabort=()=>{s.abort(),o=new r});let h,c=null;if(t.timeout){const i=t.timeout;c=setTimeout((()=>{s.abort(),this.u.log(e.Warning,"Timeout from HTTP request."),o=new n}),i)}""===t.content&&(t.content=void 0),t.content&&(t.headers=t.headers||{},y(t.content)?t.headers["Content-Type"]="application/octet-stream":t.headers["Content-Type"]="text/plain;charset=UTF-8");try{h=await this.p(t.url,{body:t.content,cache:"no-cache",credentials:!0===t.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...t.headers},method:t.method,mode:"cors",redirect:"follow",signal:s.signal})}catch(t){if(o)throw o;throw this.u.log(e.Warning,`Error from HTTP request. ${t}.`),t}finally{c&&clearTimeout(c),t.abortSignal&&(t.abortSignal.onabort=null)}if(!h.ok){const t=await I(h,"text");throw new i(t||h.statusText,h.status)}const a=I(h,t.responseType),l=await a;return new u(h.status,h.statusText,l)}getCookieString(t){let e="";return g.isNode&&this.v&&this.v.getCookies(t,((t,s)=>e=s.join("; "))),e}}function I(t,e){let s;switch(e){case"arraybuffer":s=t.arrayBuffer();break;case"text":default:s=t.text();break;case"blob":case"document":case"json":throw new Error(`${e} is not supported.`)}return s}class _ extends d{constructor(t){super(),this.u=t}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?new Promise(((s,o)=>{const h=new XMLHttpRequest;h.open(t.method,t.url,!0),h.withCredentials=void 0===t.withCredentials||t.withCredentials,h.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===t.content&&(t.content=void 0),t.content&&(y(t.content)?h.setRequestHeader("Content-Type","application/octet-stream"):h.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const c=t.headers;c&&Object.keys(c).forEach((t=>{h.setRequestHeader(t,c[t])})),t.responseType&&(h.responseType=t.responseType),t.abortSignal&&(t.abortSignal.onabort=()=>{h.abort(),o(new r)}),t.timeout&&(h.timeout=t.timeout),h.onload=()=>{t.abortSignal&&(t.abortSignal.onabort=null),h.status>=200&&h.status<300?s(new u(h.status,h.statusText,h.response||h.responseText)):o(new i(h.response||h.responseText||h.statusText,h.status))},h.onerror=()=>{this.u.log(e.Warning,`Error from HTTP request. ${h.status}: ${h.statusText}.`),o(new i(h.statusText,h.status))},h.ontimeout=()=>{this.u.log(e.Warning,"Timeout from HTTP request."),o(new n)},h.send(t.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class H extends d{constructor(t){if(super(),"undefined"!=typeof fetch||g.isNode)this.$=new T(t);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this.$=new _(t)}}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?this.$.send(t):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(t){return this.$.getCookieString(t)}}class D{static write(t){return`${t}${D.RecordSeparator}`}static parse(t){if(t[t.length-1]!==D.RecordSeparator)throw new Error("Message is incomplete.");const e=t.split(D.RecordSeparator);return e.pop(),e}}D.RecordSeparatorCode=30,D.RecordSeparator=String.fromCharCode(D.RecordSeparatorCode);class R{writeHandshakeRequest(t){return D.write(JSON.stringify(t))}parseHandshakeResponse(t){let e,s;if(y(t)){const i=new Uint8Array(t),n=i.indexOf(D.RecordSeparatorCode);if(-1===n)throw new Error("Message is incomplete.");const r=n+1;e=String.fromCharCode.apply(null,Array.prototype.slice.call(i.slice(0,r))),s=i.byteLength>r?i.slice(r).buffer:null}else{const i=t,n=i.indexOf(D.RecordSeparator);if(-1===n)throw new Error("Message is incomplete.");const r=n+1;e=i.substring(0,r),s=i.length>r?i.substring(r):null}const i=D.parse(e),n=JSON.parse(i[0]);if(n.type)throw new Error("Expected a handshake response from the server.");return[s,n]}}var x,A;!function(t){t[t.Invocation=1]="Invocation",t[t.StreamItem=2]="StreamItem",t[t.Completion=3]="Completion",t[t.StreamInvocation=4]="StreamInvocation",t[t.CancelInvocation=5]="CancelInvocation",t[t.Ping=6]="Ping",t[t.Close=7]="Close",t[t.Ack=8]="Ack",t[t.Sequence=9]="Sequence"}(x||(x={}));class U{constructor(){this.observers=[]}next(t){for(const e of this.observers)e.next(t)}error(t){for(const e of this.observers)e.error&&e.error(t)}complete(){for(const t of this.observers)t.complete&&t.complete()}subscribe(t){return this.observers.push(t),new v(this,t)}}class L{constructor(t,e,s){this.C=1e5,this.S=[],this.k=0,this.P=!1,this.T=1,this.I=0,this._=0,this.H=!1,this.D=t,this.R=e,this.C=s}async A(t){const e=this.D.writeMessage(t);let s=Promise.resolve();if(this.U(t)){this.k++;let t=()=>{},i=()=>{};y(e)?this._+=e.byteLength:this._+=e.length,this._>=this.C&&(s=new Promise(((e,s)=>{t=e,i=s}))),this.S.push(new N(e,this.k,t,i))}try{this.H||await this.R.send(e)}catch{this.L()}await s}N(t){let e=-1;for(let s=0;s<this.S.length;s++){const i=this.S[s];if(i.q<=t.sequenceId)e=s,y(i.M)?this._-=i.M.byteLength:this._-=i.M.length,i.j();else{if(!(this._<this.C))break;i.j()}}-1!==e&&(this.S=this.S.slice(e+1))}W(t){if(this.P)return t.type===x.Sequence&&(this.P=!1,!0);if(!this.U(t))return!0;const e=this.T;return this.T++,e<=this.I?(e===this.I&&this.O(),!1):(this.I=e,this.O(),!0)}F(t){t.sequenceId>this.T?this.R.stop(new Error("Sequence ID greater than amount of messages we've received.")):this.T=t.sequenceId}L(){this.H=!0,this.P=!0}async B(){const t=0!==this.S.length?this.S[0].q:this.k+1;await this.R.send(this.D.writeMessage({type:x.Sequence,sequenceId:t}));const e=this.S;for(const t of e)await this.R.send(t.M);this.H=!1}X(t){null!=t||(t=new Error("Unable to reconnect to server."));for(const e of this.S)e.J(t)}U(t){switch(t.type){case x.Invocation:case x.StreamItem:case x.Completion:case x.StreamInvocation:case x.CancelInvocation:return!0;case x.Close:case x.Sequence:case x.Ping:case x.Ack:return!1}}O(){void 0===this.V&&(this.V=setTimeout((async()=>{try{this.H||await this.R.send(this.D.writeMessage({type:x.Ack,sequenceId:this.I}))}catch{}clearTimeout(this.V),this.V=void 0}),1e3))}}class N{constructor(t,e,s,i){this.M=t,this.q=e,this.j=s,this.J=i}}!function(t){t.Disconnected="Disconnected",t.Connecting="Connecting",t.Connected="Connected",t.Disconnecting="Disconnecting",t.Reconnecting="Reconnecting"}(A||(A={}));class q{static create(t,e,s,i,n,r,o){return new q(t,e,s,i,n,r,o)}constructor(t,s,i,n,r,o,h){this.K=0,this.G=()=>{this.u.log(e.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},w.isRequired(t,"connection"),w.isRequired(s,"logger"),w.isRequired(i,"protocol"),this.serverTimeoutInMilliseconds=null!=r?r:3e4,this.keepAliveIntervalInMilliseconds=null!=o?o:15e3,this.Y=null!=h?h:1e5,this.u=s,this.D=i,this.connection=t,this.Z=n,this.tt=new R,this.connection.onreceive=t=>this.et(t),this.connection.onclose=t=>this.st(t),this.it={},this.nt={},this.rt=[],this.ot=[],this.ht=[],this.ct=0,this.lt=!1,this.ut=A.Disconnected,this.dt=!1,this.ft=this.D.writeMessage({type:x.Ping})}get state(){return this.ut}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(t){if(this.ut!==A.Disconnected&&this.ut!==A.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!t)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=t}start(){return this.wt=this.gt(),this.wt}async gt(){if(this.ut!==A.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this.ut=A.Connecting,this.u.log(e.Debug,"Starting HubConnection.");try{await this.yt(),g.isBrowser&&window.document.addEventListener("freeze",this.G),this.ut=A.Connected,this.dt=!0,this.u.log(e.Debug,"HubConnection connected successfully.")}catch(t){return this.ut=A.Disconnected,this.u.log(e.Debug,`HubConnection failed to start successfully because of error '${t}'.`),Promise.reject(t)}}async yt(){this.bt=void 0,this.lt=!1;const t=new Promise(((t,e)=>{this.vt=t,this.Et=e}));await this.connection.start(this.D.transferFormat);try{let s=this.D.version;this.connection.features.reconnect||(s=1);const i={protocol:this.D.name,version:s};if(this.u.log(e.Debug,"Sending handshake request."),await this.$t(this.tt.writeHandshakeRequest(i)),this.u.log(e.Information,`Using HubProtocol '${this.D.name}'.`),this.Ct(),this.St(),this.kt(),await t,this.bt)throw this.bt;!!this.connection.features.reconnect&&(this.Pt=new L(this.D,this.connection,this.Y),this.connection.features.disconnected=this.Pt.L.bind(this.Pt),this.connection.features.resend=()=>{if(this.Pt)return this.Pt.B()}),this.connection.features.inherentKeepAlive||await this.$t(this.ft)}catch(t){throw this.u.log(e.Debug,`Hub handshake failed with error '${t}' during start(). Stopping HubConnection.`),this.Ct(),this.Tt(),await this.connection.stop(t),t}}async stop(){const t=this.wt;this.connection.features.reconnect=!1,this.It=this._t(),await this.It;try{await t}catch(t){}}_t(t){if(this.ut===A.Disconnected)return this.u.log(e.Debug,`Call to HubConnection.stop(${t}) ignored because it is already in the disconnected state.`),Promise.resolve();if(this.ut===A.Disconnecting)return this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It;const s=this.ut;return this.ut=A.Disconnecting,this.u.log(e.Debug,"Stopping HubConnection."),this.Ht?(this.u.log(e.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this.Ht),this.Ht=void 0,this.Dt(),Promise.resolve()):(s===A.Connected&&this.Rt(),this.Ct(),this.Tt(),this.bt=t||new r("The connection was stopped before the hub handshake could complete."),this.connection.stop(t))}async Rt(){try{await this.xt(this.At())}catch{}}stream(t,...e){const[s,i]=this.Ut(e),n=this.Lt(t,e,i);let r;const o=new U;return o.cancelCallback=()=>{const t=this.Nt(n.invocationId);return delete this.it[n.invocationId],r.then((()=>this.xt(t)))},this.it[n.invocationId]=(t,e)=>{e?o.error(e):t&&(t.type===x.Completion?t.error?o.error(new Error(t.error)):o.complete():o.next(t.item))},r=this.xt(n).catch((t=>{o.error(t),delete this.it[n.invocationId]})),this.qt(s,r),o}$t(t){return this.kt(),this.connection.send(t)}xt(t){return this.Pt?this.Pt.A(t):this.$t(this.D.writeMessage(t))}send(t,...e){const[s,i]=this.Ut(e),n=this.xt(this.Mt(t,e,!0,i));return this.qt(s,n),n}invoke(t,...e){const[s,i]=this.Ut(e),n=this.Mt(t,e,!1,i);return new Promise(((t,e)=>{this.it[n.invocationId]=(s,i)=>{i?e(i):s&&(s.type===x.Completion?s.error?e(new Error(s.error)):t(s.result):e(new Error(`Unexpected message type: ${s.type}`)))};const i=this.xt(n).catch((t=>{e(t),delete this.it[n.invocationId]}));this.qt(s,i)}))}on(t,e){t&&e&&(t=t.toLowerCase(),this.nt[t]||(this.nt[t]=[]),-1===this.nt[t].indexOf(e)&&this.nt[t].push(e))}off(t,e){if(!t)return;t=t.toLowerCase();const s=this.nt[t];if(s)if(e){const i=s.indexOf(e);-1!==i&&(s.splice(i,1),0===s.length&&delete this.nt[t])}else delete this.nt[t]}onclose(t){t&&this.rt.push(t)}onreconnecting(t){t&&this.ot.push(t)}onreconnected(t){t&&this.ht.push(t)}et(t){if(this.Ct(),this.lt||(t=this.jt(t),this.lt=!0),t){const s=this.D.parseMessages(t,this.u);for(const t of s)if(!this.Pt||this.Pt.W(t))switch(t.type){case x.Invocation:this.Wt(t);break;case x.StreamItem:case x.Completion:{const s=this.it[t.invocationId];if(s){t.type===x.Completion&&delete this.it[t.invocationId];try{s(t)}catch(t){this.u.log(e.Error,`Stream callback threw error: ${P(t)}`)}}break}case x.Ping:break;case x.Close:{this.u.log(e.Information,"Close message received from server.");const s=t.error?new Error("Server returned an error on close: "+t.error):void 0;!0===t.allowReconnect?this.connection.stop(s):this.It=this._t(s);break}case x.Ack:this.Pt&&this.Pt.N(t);break;case x.Sequence:this.Pt&&this.Pt.F(t);break;default:this.u.log(e.Warning,`Invalid message type: ${t.type}.`)}}this.St()}jt(t){let s,i;try{[i,s]=this.tt.parseHandshakeResponse(t)}catch(t){const s="Error parsing handshake response: "+t;this.u.log(e.Error,s);const i=new Error(s);throw this.Et(i),i}if(s.error){const t="Server returned handshake error: "+s.error;this.u.log(e.Error,t);const i=new Error(t);throw this.Et(i),i}return this.u.log(e.Debug,"Server handshake complete."),this.vt(),i}kt(){this.connection.features.inherentKeepAlive||(this.K=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this.Tt())}St(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this.Ot=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this.Ft))){let t=this.K-(new Date).getTime();t<0&&(t=0),this.Ft=setTimeout((async()=>{if(this.ut===A.Connected)try{await this.$t(this.ft)}catch{this.Tt()}}),t)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async Wt(t){const s=t.target.toLowerCase(),i=this.nt[s];if(!i)return this.u.log(e.Warning,`No client method with the name '${s}' found.`),void(t.invocationId&&(this.u.log(e.Warning,`No result given for '${s}' method and invocation ID '${t.invocationId}'.`),await this.xt(this.Bt(t.invocationId,"Client didn't provide a result.",null))));const n=i.slice(),r=!!t.invocationId;let o,h,c;for(const i of n)try{const n=o;o=await i.apply(this,t.arguments),r&&o&&n&&(this.u.log(e.Error,`Multiple results provided for '${s}'. Sending error to server.`),c=this.Bt(t.invocationId,"Client provided multiple results.",null)),h=void 0}catch(t){h=t,this.u.log(e.Error,`A callback for the method '${s}' threw error '${t}'.`)}c?await this.xt(c):r?(h?c=this.Bt(t.invocationId,`${h}`,null):void 0!==o?c=this.Bt(t.invocationId,null,o):(this.u.log(e.Warning,`No result given for '${s}' method and invocation ID '${t.invocationId}'.`),c=this.Bt(t.invocationId,"Client didn't provide a result.",null)),await this.xt(c)):o&&this.u.log(e.Error,`Result given for '${s}' method but server is not expecting a result.`)}st(t){this.u.log(e.Debug,`HubConnection.connectionClosed(${t}) called while in state ${this.ut}.`),this.bt=this.bt||t||new r("The underlying connection was closed before the hub handshake could complete."),this.vt&&this.vt(),this.Xt(t||new Error("Invocation canceled due to the underlying connection being closed.")),this.Ct(),this.Tt(),this.ut===A.Disconnecting?this.Dt(t):this.ut===A.Connected&&this.Z?this.Jt(t):this.ut===A.Connected&&this.Dt(t)}Dt(t){if(this.dt){this.ut=A.Disconnected,this.dt=!1,this.Pt&&(this.Pt.X(null!=t?t:new Error("Connection closed.")),this.Pt=void 0),g.isBrowser&&window.document.removeEventListener("freeze",this.G);try{this.rt.forEach((e=>e.apply(this,[t])))}catch(s){this.u.log(e.Error,`An onclose callback called with error '${t}' threw error '${s}'.`)}}}async Jt(t){const s=Date.now();let i=0,n=void 0!==t?t:new Error("Attempting to reconnect due to a unknown error."),r=this.zt(i++,0,n);if(null===r)return this.u.log(e.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this.Dt(t);if(this.ut=A.Reconnecting,t?this.u.log(e.Information,`Connection reconnecting because of error '${t}'.`):this.u.log(e.Information,"Connection reconnecting."),0!==this.ot.length){try{this.ot.forEach((e=>e.apply(this,[t])))}catch(s){this.u.log(e.Error,`An onreconnecting callback called with error '${t}' threw error '${s}'.`)}if(this.ut!==A.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==r;){if(this.u.log(e.Information,`Reconnect attempt number ${i} will start in ${r} ms.`),await new Promise((t=>{this.Ht=setTimeout(t,r)})),this.Ht=void 0,this.ut!==A.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this.yt(),this.ut=A.Connected,this.u.log(e.Information,"HubConnection reconnected successfully."),0!==this.ht.length)try{this.ht.forEach((t=>t.apply(this,[this.connection.connectionId])))}catch(t){this.u.log(e.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${t}'.`)}return}catch(t){if(this.u.log(e.Information,`Reconnect attempt failed because of error '${t}'.`),this.ut!==A.Reconnecting)return this.u.log(e.Debug,`Connection moved to the '${this.ut}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this.ut===A.Disconnecting&&this.Dt());n=t instanceof Error?t:new Error(t.toString()),r=this.zt(i++,Date.now()-s,n)}}this.u.log(e.Information,`Reconnect retries have been exhausted after ${Date.now()-s} ms and ${i} failed attempts. Connection disconnecting.`),this.Dt()}zt(t,s,i){try{return this.Z.nextRetryDelayInMilliseconds({elapsedMilliseconds:s,previousRetryCount:t,retryReason:i})}catch(i){return this.u.log(e.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${t}, ${s}) threw error '${i}'.`),null}}Xt(t){const s=this.it;this.it={},Object.keys(s).forEach((i=>{const n=s[i];try{n(null,t)}catch(s){this.u.log(e.Error,`Stream 'error' callback called with '${t}' threw error: ${P(s)}`)}}))}Tt(){this.Ft&&(clearTimeout(this.Ft),this.Ft=void 0)}Ct(){this.Ot&&clearTimeout(this.Ot)}Mt(t,e,s,i){if(s)return 0!==i.length?{arguments:e,streamIds:i,target:t,type:x.Invocation}:{arguments:e,target:t,type:x.Invocation};{const s=this.ct;return this.ct++,0!==i.length?{arguments:e,invocationId:s.toString(),streamIds:i,target:t,type:x.Invocation}:{arguments:e,invocationId:s.toString(),target:t,type:x.Invocation}}}qt(t,e){if(0!==t.length){e||(e=Promise.resolve());for(const s in t)t[s].subscribe({complete:()=>{e=e.then((()=>this.xt(this.Bt(s))))},error:t=>{let i;i=t instanceof Error?t.message:t&&t.toString?t.toString():"Unknown error",e=e.then((()=>this.xt(this.Bt(s,i))))},next:t=>{e=e.then((()=>this.xt(this.Vt(s,t))))}})}}Ut(t){const e=[],s=[];for(let i=0;i<t.length;i++){const n=t[i];if(this.Kt(n)){const r=this.ct;this.ct++,e[r]=n,s.push(r.toString()),t.splice(i,1)}}return[e,s]}Kt(t){return t&&t.subscribe&&"function"==typeof t.subscribe}Lt(t,e,s){const i=this.ct;return this.ct++,0!==s.length?{arguments:e,invocationId:i.toString(),streamIds:s,target:t,type:x.StreamInvocation}:{arguments:e,invocationId:i.toString(),target:t,type:x.StreamInvocation}}Nt(t){return{invocationId:t,type:x.CancelInvocation}}Vt(t,e){return{invocationId:t,item:e,type:x.StreamItem}}Bt(t,e,s){return e?{error:e,invocationId:t,type:x.Completion}:{invocationId:t,result:s,type:x.Completion}}At(){return{type:x.Close}}}const M=[0,2e3,1e4,3e4,null];class j{constructor(t){this.Gt=void 0!==t?[...t,null]:M}nextRetryDelayInMilliseconds(t){return this.Gt[t.previousRetryCount]}}class W{}W.Authorization="Authorization",W.Cookie="Cookie";class O extends d{constructor(t,e){super(),this.Qt=t,this.Yt=e}async send(t){let e=!0;this.Yt&&(!this.Zt||t.url&&t.url.indexOf("/negotiate?")>0)&&(e=!1,this.Zt=await this.Yt()),this.te(t);const s=await this.Qt.send(t);return e&&401===s.statusCode&&this.Yt?(this.Zt=await this.Yt(),this.te(t),await this.Qt.send(t)):s}te(t){t.headers||(t.headers={}),this.Zt?t.headers[W.Authorization]=`Bearer ${this.Zt}`:this.Yt&&t.headers[W.Authorization]&&delete t.headers[W.Authorization]}getCookieString(t){return this.Qt.getCookieString(t)}}var F,B;!function(t){t[t.None=0]="None",t[t.WebSockets=1]="WebSockets",t[t.ServerSentEvents=2]="ServerSentEvents",t[t.LongPolling=4]="LongPolling"}(F||(F={})),function(t){t[t.Text=1]="Text",t[t.Binary=2]="Binary"}(B||(B={}));class X{constructor(){this.ee=!1,this.onabort=null}abort(){this.ee||(this.ee=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this.ee}}class J{get pollAborted(){return this.se.aborted}constructor(t,e,s){this.$=t,this.u=e,this.se=new X,this.ie=s,this.ne=!1,this.onreceive=null,this.onclose=null}async connect(t,s){if(w.isRequired(t,"url"),w.isRequired(s,"transferFormat"),w.isIn(s,B,"transferFormat"),this.re=t,this.u.log(e.Trace,"(LongPolling transport) Connecting."),s===B.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[n,r]=$(),o={[n]:r,...this.ie.headers},h={abortSignal:this.se.signal,headers:o,timeout:1e5,withCredentials:this.ie.withCredentials};s===B.Binary&&(h.responseType="arraybuffer");const c=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${c}.`);const a=await this.$.get(c,h);200!==a.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${a.statusCode}.`),this.oe=new i(a.statusText||"",a.statusCode),this.ne=!1):this.ne=!0,this.he=this.ce(this.re,h)}async ce(t,s){try{for(;this.ne;)try{const n=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${n}.`);const r=await this.$.get(n,s);204===r.statusCode?(this.u.log(e.Information,"(LongPolling transport) Poll terminated by server."),this.ne=!1):200!==r.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this.oe=new i(r.statusText||"",r.statusCode),this.ne=!1):r.content?(this.u.log(e.Trace,`(LongPolling transport) data received. ${m(r.content,this.ie.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(t){this.ne?t instanceof n?this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this.oe=t,this.ne=!1):this.u.log(e.Trace,`(LongPolling transport) Poll errored after shutdown: ${t.message}`)}}finally{this.u.log(e.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this.ae()}}async send(t){return this.ne?b(this.u,"LongPolling",this.$,this.re,t,this.ie):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this.u.log(e.Trace,"(LongPolling transport) Stopping polling."),this.ne=!1,this.se.abort();try{await this.he,this.u.log(e.Trace,`(LongPolling transport) sending DELETE request to ${this.re}.`);const t={},[s,n]=$();t[s]=n;const r={headers:{...t,...this.ie.headers},timeout:this.ie.timeout,withCredentials:this.ie.withCredentials};let o;try{await this.$.delete(this.re,r)}catch(t){o=t}o?o instanceof i&&(404===o.statusCode?this.u.log(e.Trace,"(LongPolling transport) A 404 response was returned from sending a DELETE request."):this.u.log(e.Trace,`(LongPolling transport) Error sending a DELETE request: ${o}`)):this.u.log(e.Trace,"(LongPolling transport) DELETE request accepted.")}finally{this.u.log(e.Trace,"(LongPolling transport) Stop finished."),this.ae()}}ae(){if(this.onclose){let t="(LongPolling transport) Firing onclose event.";this.oe&&(t+=" Error: "+this.oe),this.u.log(e.Trace,t),this.onclose(this.oe)}}}class z{constructor(t,e,s,i){this.$=t,this.Zt=e,this.u=s,this.ie=i,this.onreceive=null,this.onclose=null}async connect(t,s){return w.isRequired(t,"url"),w.isRequired(s,"transferFormat"),w.isIn(s,B,"transferFormat"),this.u.log(e.Trace,"(SSE transport) Connecting."),this.re=t,this.Zt&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this.Zt)}`),new Promise(((i,n)=>{let r,o=!1;if(s===B.Text){if(g.isBrowser||g.isWebWorker)r=new this.ie.EventSource(t,{withCredentials:this.ie.withCredentials});else{const e=this.$.getCookieString(t),s={};s.Cookie=e;const[i,n]=$();s[i]=n,r=new this.ie.EventSource(t,{withCredentials:this.ie.withCredentials,headers:{...s,...this.ie.headers}})}try{r.onmessage=t=>{if(this.onreceive)try{this.u.log(e.Trace,`(SSE transport) data received. ${m(t.data,this.ie.logMessageContent)}.`),this.onreceive(t.data)}catch(t){return void this.le(t)}},r.onerror=t=>{o?this.le():n(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},r.onopen=()=>{this.u.log(e.Information,`SSE connected to ${this.re}`),this.ue=r,o=!0,i()}}catch(t){return void n(t)}}else n(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(t){return this.ue?b(this.u,"SSE",this.$,this.re,t,this.ie):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this.le(),Promise.resolve()}le(t){this.ue&&(this.ue.close(),this.ue=void 0,this.onclose&&this.onclose(t))}}class V{constructor(t,e,s,i,n,r){this.u=s,this.Yt=e,this.de=i,this.fe=n,this.$=t,this.onreceive=null,this.onclose=null,this.pe=r}async connect(t,s){let i;return w.isRequired(t,"url"),w.isRequired(s,"transferFormat"),w.isIn(s,B,"transferFormat"),this.u.log(e.Trace,"(WebSockets transport) Connecting."),this.Yt&&(i=await this.Yt()),new Promise(((n,r)=>{let o;t=t.replace(/^http/,"ws");const h=this.$.getCookieString(t);let c=!1;if(g.isNode||g.isReactNative){const e={},[s,n]=$();e[s]=n,i&&(e[W.Authorization]=`Bearer ${i}`),h&&(e[W.Cookie]=h),o=new this.fe(t,void 0,{headers:{...e,...this.pe}})}else i&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(i)}`);o||(o=new this.fe(t)),s===B.Binary&&(o.binaryType="arraybuffer"),o.onopen=s=>{this.u.log(e.Information,`WebSocket connected to ${t}.`),this.we=o,c=!0,n()},o.onerror=t=>{let s=null;s="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"There was an error with the transport",this.u.log(e.Information,`(WebSockets transport) ${s}.`)},o.onmessage=t=>{if(this.u.log(e.Trace,`(WebSockets transport) data received. ${m(t.data,this.de)}.`),this.onreceive)try{this.onreceive(t.data)}catch(t){return void this.le(t)}},o.onclose=t=>{if(c)this.le(t);else{let e=null;e="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",r(new Error(e))}}}))}send(t){return this.we&&this.we.readyState===this.fe.OPEN?(this.u.log(e.Trace,`(WebSockets transport) sending data. ${m(t,this.de)}.`),this.we.send(t),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this.we&&this.le(void 0),Promise.resolve()}le(t){this.we&&(this.we.onclose=()=>{},this.we.onmessage=()=>{},this.we.onerror=()=>{},this.we.close(),this.we=void 0),this.u.log(e.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this.ge(t)||!1!==t.wasClean&&1e3===t.code?t instanceof Error?this.onclose(t):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${t.code} (${t.reason||"no reason given"}).`)))}ge(t){return t&&"boolean"==typeof t.wasClean&&"number"==typeof t.code}}class K{constructor(t,s={}){var i;if(this.me=()=>{},this.features={},this.ye=1,w.isRequired(t,"url"),this.u=void 0===(i=s.logger)?new E(e.Information):null===i?f.instance:void 0!==i.log?i:new E(i),this.baseUrl=this.be(t),(s=s||{}).logMessageContent=void 0!==s.logMessageContent&&s.logMessageContent,"boolean"!=typeof s.withCredentials&&void 0!==s.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");s.withCredentials=void 0===s.withCredentials||s.withCredentials,s.timeout=void 0===s.timeout?1e5:s.timeout;let n=null,r=null;g.isNode&&(n=function(){throw new Error("Trying to import 'ws' in the browser.")}(),r=function(){throw new Error("Trying to import 'eventsource' in the browser.")}()),g.isNode||"undefined"==typeof WebSocket||s.WebSocket?g.isNode&&!s.WebSocket&&n&&(s.WebSocket=n):s.WebSocket=WebSocket,g.isNode||"undefined"==typeof EventSource||s.EventSource?g.isNode&&!s.EventSource&&void 0!==r&&(s.EventSource=r):s.EventSource=EventSource,this.$=new O(s.httpClient||new H(this.u),s.accessTokenFactory),this.ut="Disconnected",this.dt=!1,this.ie=s,this.onreceive=null,this.onclose=null}async start(t){if(t=t||B.Binary,w.isIn(t,B,"transferFormat"),this.u.log(e.Debug,`Starting connection with transfer format '${B[t]}'.`),"Disconnected"!==this.ut)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this.ut="Connecting",this.ve=this.yt(t),await this.ve,"Disconnecting"===this.ut){const t="Failed to start the HttpConnection before stop() was called.";return this.u.log(e.Error,t),await this.It,Promise.reject(new r(t))}if("Connected"!==this.ut){const t="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this.u.log(e.Error,t),Promise.reject(new r(t))}this.dt=!0}send(t){return"Connected"!==this.ut?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this.Ee||(this.Ee=new G(this.transport)),this.Ee.send(t))}async stop(t){return"Disconnected"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It):(this.ut="Disconnecting",this.It=new Promise((t=>{this.me=t})),await this._t(t),void await this.It)}async _t(t){this.$e=t;try{await this.ve}catch(t){}if(this.transport){try{await this.transport.stop()}catch(t){this.u.log(e.Error,`HttpConnection.transport.stop() threw error '${t}'.`),this.Ce()}this.transport=void 0}else this.u.log(e.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async yt(t){let s=this.baseUrl;this.Yt=this.ie.accessTokenFactory,this.$.Yt=this.Yt;try{if(this.ie.skipNegotiation){if(this.ie.transport!==F.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this.Se(F.WebSockets),await this.ke(s,t)}else{let e=null,i=0;do{if(e=await this.Pe(s),"Disconnecting"===this.ut||"Disconnected"===this.ut)throw new r("The connection was stopped during negotiation.");if(e.error)throw new Error(e.error);if(e.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(e.url&&(s=e.url),e.accessToken){const t=e.accessToken;this.Yt=()=>t,this.$.Zt=t,this.$.Yt=void 0}i++}while(e.url&&i<100);if(100===i&&e.url)throw new Error("Negotiate redirection limit exceeded.");await this.Te(s,this.ie.transport,e,t)}this.transport instanceof J&&(this.features.inherentKeepAlive=!0),"Connecting"===this.ut&&(this.u.log(e.Debug,"The HttpConnection connected successfully."),this.ut="Connected")}catch(t){return this.u.log(e.Error,"Failed to start the connection: "+t),this.ut="Disconnected",this.transport=void 0,this.me(),Promise.reject(t)}}async Pe(t){const s={},[n,r]=$();s[n]=r;const o=this.Ie(t);this.u.log(e.Debug,`Sending negotiation request: ${o}.`);try{const t=await this.$.post(o,{content:"",headers:{...s,...this.ie.headers},timeout:this.ie.timeout,withCredentials:this.ie.withCredentials});if(200!==t.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${t.statusCode}'`));const e=JSON.parse(t.content);return(!e.negotiateVersion||e.negotiateVersion<1)&&(e.connectionToken=e.connectionId),e.useStatefulReconnect&&!0!==this.ie._e?Promise.reject(new a("Client didn't negotiate Stateful Reconnect but the server did.")):e}catch(t){let s="Failed to complete negotiation with the server: "+t;return t instanceof i&&404===t.statusCode&&(s+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this.u.log(e.Error,s),Promise.reject(new a(s))}}He(t,e){return e?t+(-1===t.indexOf("?")?"?":"&")+`id=${e}`:t}async Te(t,s,i,n){let o=this.He(t,i.connectionToken);if(this.De(s))return this.u.log(e.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=s,await this.ke(o,n),void(this.connectionId=i.connectionId);const h=[],a=i.availableTransports||[];let u=i;for(const i of a){const a=this.Re(i,s,n,!0===(null==u?void 0:u.useStatefulReconnect));if(a instanceof Error)h.push(`${i.transport} failed:`),h.push(a);else if(this.De(a)){if(this.transport=a,!u){try{u=await this.Pe(t)}catch(t){return Promise.reject(t)}o=this.He(t,u.connectionToken)}try{return await this.ke(o,n),void(this.connectionId=u.connectionId)}catch(t){if(this.u.log(e.Error,`Failed to start the transport '${i.transport}': ${t}`),u=void 0,h.push(new c(`${i.transport} failed: ${t}`,F[i.transport])),"Connecting"!==this.ut){const t="Failed to select transport before stop() was called.";return this.u.log(e.Debug,t),Promise.reject(new r(t))}}}}return h.length>0?Promise.reject(new l(`Unable to connect to the server with any of the available transports. ${h.join(" ")}`,h)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}Se(t){switch(t){case F.WebSockets:if(!this.ie.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new V(this.$,this.Yt,this.u,this.ie.logMessageContent,this.ie.WebSocket,this.ie.headers||{});case F.ServerSentEvents:if(!this.ie.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new z(this.$,this.$.Zt,this.u,this.ie);case F.LongPolling:return new J(this.$,this.u,this.ie);default:throw new Error(`Unknown transport: ${t}.`)}}ke(t,e){return this.transport.onreceive=this.onreceive,this.features.reconnect?this.transport.onclose=async s=>{let i=!1;if(this.features.reconnect){try{this.features.disconnected(),await this.transport.connect(t,e),await this.features.resend()}catch{i=!0}i&&this.Ce(s)}else this.Ce(s)}:this.transport.onclose=t=>this.Ce(t),this.transport.connect(t,e)}Re(t,s,i,n){const r=F[t.transport];if(null==r)return this.u.log(e.Debug,`Skipping transport '${t.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${t.transport}' because it is not supported by this client.`);if(!function(t,e){return!t||0!=(e&t)}(s,r))return this.u.log(e.Debug,`Skipping transport '${F[r]}' because it was disabled by the client.`),new h(`'${F[r]}' is disabled by the client.`,r);if(!(t.transferFormats.map((t=>B[t])).indexOf(i)>=0))return this.u.log(e.Debug,`Skipping transport '${F[r]}' because it does not support the requested transfer format '${B[i]}'.`),new Error(`'${F[r]}' does not support ${B[i]}.`);if(r===F.WebSockets&&!this.ie.WebSocket||r===F.ServerSentEvents&&!this.ie.EventSource)return this.u.log(e.Debug,`Skipping transport '${F[r]}' because it is not supported in your environment.'`),new o(`'${F[r]}' is not supported in your environment.`,r);this.u.log(e.Debug,`Selecting transport '${F[r]}'.`);try{return this.features.reconnect=r===F.WebSockets?n:void 0,this.Se(r)}catch(t){return t}}De(t){return t&&"object"==typeof t&&"connect"in t}Ce(t){if(this.u.log(e.Debug,`HttpConnection.stopConnection(${t}) called while in state ${this.ut}.`),this.transport=void 0,t=this.$e||t,this.$e=void 0,"Disconnected"!==this.ut){if("Connecting"===this.ut)throw this.u.log(e.Warning,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${t}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this.ut&&this.me(),t?this.u.log(e.Error,`Connection disconnected with error '${t}'.`):this.u.log(e.Information,"Connection disconnected."),this.Ee&&(this.Ee.stop().catch((t=>{this.u.log(e.Error,`TransportSendQueue.stop() threw error '${t}'.`)})),this.Ee=void 0),this.connectionId=void 0,this.ut="Disconnected",this.dt){this.dt=!1;try{this.onclose&&this.onclose(t)}catch(s){this.u.log(e.Error,`HttpConnection.onclose(${t}) threw error '${s}'.`)}}}else this.u.log(e.Debug,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is already in the disconnected state.`)}be(t){if(0===t.lastIndexOf("https://",0)||0===t.lastIndexOf("http://",0))return t;if(!g.isBrowser)throw new Error(`Cannot resolve '${t}'.`);const s=window.document.createElement("a");return s.href=t,this.u.log(e.Information,`Normalizing '${t}' to '${s.href}'.`),s.href}Ie(t){const e=new URL(t);e.pathname.endsWith("/")?e.pathname+="negotiate":e.pathname+="/negotiate";const s=new URLSearchParams(e.searchParams);return s.has("negotiateVersion")||s.append("negotiateVersion",this.ye.toString()),s.has("useStatefulReconnect")?"true"===s.get("useStatefulReconnect")&&(this.ie._e=!0):!0===this.ie._e&&s.append("useStatefulReconnect","true"),e.search=s.toString(),e.toString()}}class G{constructor(t){this.xe=t,this.Ae=[],this.Ue=!0,this.Le=new Q,this.Ne=new Q,this.qe=this.Me()}send(t){return this.je(t),this.Ne||(this.Ne=new Q),this.Ne.promise}stop(){return this.Ue=!1,this.Le.resolve(),this.qe}je(t){if(this.Ae.length&&typeof this.Ae[0]!=typeof t)throw new Error(`Expected data to be of type ${typeof this.Ae} but was of type ${typeof t}`);this.Ae.push(t),this.Le.resolve()}async Me(){for(;;){if(await this.Le.promise,!this.Ue){this.Ne&&this.Ne.reject("Connection stopped.");break}this.Le=new Q;const t=this.Ne;this.Ne=void 0;const e="string"==typeof this.Ae[0]?this.Ae.join(""):G.We(this.Ae);this.Ae.length=0;try{await this.xe.send(e),t.resolve()}catch(e){t.reject(e)}}}static We(t){const e=t.map((t=>t.byteLength)).reduce(((t,e)=>t+e)),s=new Uint8Array(e);let i=0;for(const e of t)s.set(new Uint8Array(e),i),i+=e.byteLength;return s.buffer}}class Q{constructor(){this.promise=new Promise(((t,e)=>[this.j,this.Oe]=[t,e]))}resolve(){this.j()}reject(t){this.Oe(t)}}class Y{constructor(){this.name="json",this.version=2,this.transferFormat=B.Text}parseMessages(t,s){if("string"!=typeof t)throw new Error("Invalid input for JSON hub protocol. Expected a string.");if(!t)return[];null===s&&(s=f.instance);const i=D.parse(t),n=[];for(const t of i){const i=JSON.parse(t);if("number"!=typeof i.type)throw new Error("Invalid payload.");switch(i.type){case x.Invocation:this.U(i);break;case x.StreamItem:this.Fe(i);break;case x.Completion:this.Be(i);break;case x.Ping:case x.Close:break;case x.Ack:this.Xe(i);break;case x.Sequence:this.Je(i);break;default:s.log(e.Information,"Unknown message type '"+i.type+"' ignored.");continue}n.push(i)}return n}writeMessage(t){return D.write(JSON.stringify(t))}U(t){this.ze(t.target,"Invalid payload for Invocation message."),void 0!==t.invocationId&&this.ze(t.invocationId,"Invalid payload for Invocation message.")}Fe(t){if(this.ze(t.invocationId,"Invalid payload for StreamItem message."),void 0===t.item)throw new Error("Invalid payload for StreamItem message.")}Be(t){if(t.result&&t.error)throw new Error("Invalid payload for Completion message.");!t.result&&t.error&&this.ze(t.error,"Invalid payload for Completion message."),this.ze(t.invocationId,"Invalid payload for Completion message.")}Xe(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Ack message.")}Je(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Sequence message.")}ze(t,e){if("string"!=typeof t||""===t)throw new Error(e)}}const Z={trace:e.Trace,debug:e.Debug,info:e.Information,information:e.Information,warn:e.Warning,warning:e.Warning,error:e.Error,critical:e.Critical,none:e.None};class tt{configureLogging(t){if(w.isRequired(t,"logging"),void 0!==t.log)this.logger=t;else if("string"==typeof t){const e=function(t){const e=Z[t.toLowerCase()];if(void 0!==e)return e;throw new Error(`Unknown log level: ${t}`)}(t);this.logger=new E(e)}else this.logger=new E(t);return this}withUrl(t,e){return w.isRequired(t,"url"),w.isNotEmpty(t,"url"),this.url=t,this.httpConnectionOptions="object"==typeof e?{...this.httpConnectionOptions,...e}:{...this.httpConnectionOptions,transport:e},this}withHubProtocol(t){return w.isRequired(t,"protocol"),this.protocol=t,this}withAutomaticReconnect(t){if(this.reconnectPolicy)throw new Error("A reconnectPolicy has already been set.");return t?Array.isArray(t)?this.reconnectPolicy=new j(t):this.reconnectPolicy=t:this.reconnectPolicy=new j,this}withServerTimeout(t){return w.isRequired(t,"milliseconds"),this.Ve=t,this}withKeepAliveInterval(t){return w.isRequired(t,"milliseconds"),this.Ke=t,this}withStatefulReconnect(t){return void 0===this.httpConnectionOptions&&(this.httpConnectionOptions={}),this.httpConnectionOptions._e=!0,this.Y=null==t?void 0:t.bufferSize,this}build(){const t=this.httpConnectionOptions||{};if(void 0===t.logger&&(t.logger=this.logger),!this.url)throw new Error("The 'HubConnectionBuilder.withUrl' method must be called before building the connection.");const e=new K(this.url,t);return q.create(e,this.logger||f.instance,this.protocol||new Y,this.reconnectPolicy,this.Ve,this.Ke,this.Y)}}return Uint8Array.prototype.indexOf||Object.defineProperty(Uint8Array.prototype,"indexOf",{value:Array.prototype.indexOf,writable:!0}),Uint8Array.prototype.slice||Object.defineProperty(Uint8Array.prototype,"slice",{value:function(t,e){return new Uint8Array(Array.prototype.slice.call(this,t,e))},writable:!0}),Uint8Array.prototype.forEach||Object.defineProperty(Uint8Array.prototype,"forEach",{value:Array.prototype.forEach,writable:!0}),s})(),"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.signalR=e():t.signalR=e();
//# sourceMappingURL=signalr.min.js.map;
/*! nanoScrollerJS - v0.7.4 - (c) 2013 James Florentino; Licensed MIT */
!function(a,b,c){"use strict";var d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x;w={paneClass:"pane",sliderClass:"slider",contentClass:"content",iOSNativeScrolling:!1,preventPageScrolling:!1,disableResize:!1,alwaysVisible:!1,flashDelay:1500,sliderMinHeight:20,sliderMaxHeight:null,documentContext:null,windowContext:null},s="scrollbar",r="scroll",k="mousedown",l="mousemove",n="mousewheel",m="mouseup",q="resize",h="drag",u="up",p="panedown",f="DOMMouseScroll",g="down",v="wheel",i="keydown",j="keyup",t="touchmove",d="Microsoft Internet Explorer"===b.navigator.appName&&/msie 7./i.test(b.navigator.appVersion)&&b.ActiveXObject,e=null,x=function(){var a,b,d;return a=c.createElement("div"),b=a.style,b.position="absolute",b.width="100px",b.height="100px",b.overflow=r,b.top="-9999px",c.body.appendChild(a),d=a.offsetWidth-a.clientWidth,c.body.removeChild(a),d},o=function(){function i(d,f){this.el=d,this.options=f,e||(e=x()),this.$el=a(this.el),this.doc=a(this.options.documentContext||c),this.win=a(this.options.windowContext||b),this.$content=this.$el.children("."+f.contentClass),this.$content.attr("tabindex",this.options.tabIndex||0),this.content=this.$content[0],this.options.iOSNativeScrolling&&null!=this.el.style.WebkitOverflowScrolling?this.nativeScrolling():this.generate(),this.createEvents(),this.addEvents(),this.reset()}return i.prototype.preventScrolling=function(a,b){if(this.isActive)if(a.type===f)(b===g&&a.originalEvent.detail>0||b===u&&a.originalEvent.detail<0)&&a.preventDefault();else if(a.type===n){if(!a.originalEvent||!a.originalEvent.wheelDelta)return;(b===g&&a.originalEvent.wheelDelta<0||b===u&&a.originalEvent.wheelDelta>0)&&a.preventDefault()}},i.prototype.nativeScrolling=function(){this.$content.css({WebkitOverflowScrolling:"touch"}),this.iOSNativeScrolling=!0,this.isActive=!0},i.prototype.updateScrollValues=function(){var a;a=this.content,this.maxScrollTop=a.scrollHeight-a.clientHeight,this.prevScrollTop=this.contentScrollTop||0,this.contentScrollTop=a.scrollTop,this.iOSNativeScrolling||(this.maxSliderTop=this.paneHeight-this.sliderHeight,this.sliderTop=0===this.maxScrollTop?0:this.contentScrollTop*this.maxSliderTop/this.maxScrollTop)},i.prototype.createEvents=function(){var a=this;this.events={down:function(b){return a.isBeingDragged=!0,a.offsetY=b.pageY-a.slider.offset().top,a.pane.addClass("active"),a.doc.bind(l,a.events[h]).bind(m,a.events[u]),!1},drag:function(b){return a.sliderY=b.pageY-a.$el.offset().top-a.offsetY,a.scroll(),a.updateScrollValues(),a.contentScrollTop>=a.maxScrollTop&&a.prevScrollTop!==a.maxScrollTop?a.$el.trigger("scrollend"):0===a.contentScrollTop&&0!==a.prevScrollTop&&a.$el.trigger("scrolltop"),!1},up:function(){return a.isBeingDragged=!1,a.pane.removeClass("active"),a.doc.unbind(l,a.events[h]).unbind(m,a.events[u]),!1},resize:function(){a.reset()},panedown:function(b){return a.sliderY=(b.offsetY||b.originalEvent.layerY)-.5*a.sliderHeight,a.scroll(),a.events.down(b),!1},scroll:function(b){a.isBeingDragged||(a.updateScrollValues(),a.iOSNativeScrolling||(a.sliderY=a.sliderTop,a.slider.css({top:a.sliderTop})),null!=b&&(a.contentScrollTop>=a.maxScrollTop?(a.options.preventPageScrolling&&a.preventScrolling(b,g),a.prevScrollTop!==a.maxScrollTop&&a.$el.trigger("scrollend")):0===a.contentScrollTop&&(a.options.preventPageScrolling&&a.preventScrolling(b,u),0!==a.prevScrollTop&&a.$el.trigger("scrolltop"))))},wheel:function(b){var c;if(null!=b)return c=b.delta||b.wheelDelta||b.originalEvent&&b.originalEvent.wheelDelta||-b.detail||b.originalEvent&&-b.originalEvent.detail,c&&(a.sliderY+=-c/3),a.scroll(),!1}}},i.prototype.addEvents=function(){var a;this.removeEvents(),a=this.events,this.options.disableResize||this.win.bind(q,a[q]),this.iOSNativeScrolling||(this.slider.bind(k,a[g]),this.pane.bind(k,a[p]).bind(""+n+" "+f,a[v])),this.$content.bind(""+r+" "+n+" "+f+" "+t,a[r])},i.prototype.removeEvents=function(){var a;a=this.events,this.win.unbind(q,a[q]),this.iOSNativeScrolling||(this.slider.unbind(),this.pane.unbind()),this.$content.unbind(""+r+" "+n+" "+f+" "+t,a[r])},i.prototype.generate=function(){var a,b,c,d,f;return c=this.options,d=c.paneClass,f=c.sliderClass,a=c.contentClass,this.$el.find(""+d).length||this.$el.find(""+f).length||this.$el.append('<div class="'+d+'"><div class="'+f+'" /></div>'),this.pane=this.$el.children("."+d),this.slider=this.pane.find("."+f),e&&(b={right:-e},this.$el.addClass("has-scrollbar")),null!=b&&this.$content.css(b),this},i.prototype.restore=function(){this.stopped=!1,this.pane.show(),this.addEvents()},i.prototype.reset=function(){var a,b,c,f,g,h,i,j,k,l;return this.iOSNativeScrolling?(this.contentHeight=this.content.scrollHeight,void 0):(this.$el.find("."+this.options.paneClass).length||this.generate().stop(),this.stopped&&this.restore(),a=this.content,c=a.style,f=c.overflowY,d&&this.$content.css({height:this.$content.height()}),b=a.scrollHeight+e,k=parseInt(this.$el.css("max-height"),10),k>0&&(this.$el.height(""),this.$el.height(a.scrollHeight>k?k:a.scrollHeight)),h=this.pane.outerHeight(!1),j=parseInt(this.pane.css("top"),10),g=parseInt(this.pane.css("bottom"),10),i=h+j+g,l=Math.round(i/b*i),l<this.options.sliderMinHeight?l=this.options.sliderMinHeight:null!=this.options.sliderMaxHeight&&l>this.options.sliderMaxHeight&&(l=this.options.sliderMaxHeight),f===r&&c.overflowX!==r&&(l+=e),this.maxSliderTop=i-l,this.contentHeight=b,this.paneHeight=h,this.paneOuterHeight=i,this.sliderHeight=l,this.slider.height(l),this.events.scroll(),this.pane.show(),this.isActive=!0,a.scrollHeight===a.clientHeight||this.pane.outerHeight(!0)>=a.scrollHeight&&f!==r?(this.pane.hide(),this.isActive=!1):this.el.clientHeight===a.scrollHeight&&f===r?this.slider.hide():this.slider.show(),this.pane.css({opacity:this.options.alwaysVisible?1:"",visibility:this.options.alwaysVisible?"visible":""}),this)},i.prototype.scroll=function(){return this.isActive?(this.sliderY=Math.max(0,this.sliderY),this.sliderY=Math.min(this.maxSliderTop,this.sliderY),this.$content.scrollTop(-1*((this.paneHeight-this.contentHeight+e)*this.sliderY/this.maxSliderTop)),this.iOSNativeScrolling||this.slider.css({top:this.sliderY}),this):void 0},i.prototype.scrollBottom=function(a){return this.isActive?(this.reset(),this.$content.scrollTop(this.contentHeight-this.$content.height()-a).trigger(n),this):void 0},i.prototype.scrollTop=function(a){return this.isActive?(this.reset(),this.$content.scrollTop(+a).trigger(n),this):void 0},i.prototype.scrollTo=function(b){return this.isActive?(this.reset(),this.scrollTop(a(b).get(0).offsetTop),this):void 0},i.prototype.stop=function(){return this.stopped=!0,this.removeEvents(),this.pane.hide(),this},i.prototype.destroy=function(){return this.stopped||this.stop(),this.pane.length&&this.pane.remove(),d&&this.$content.height(""),this.$content.removeAttr("tabindex"),this.$el.hasClass("has-scrollbar")&&(this.$el.removeClass("has-scrollbar"),this.$content.css({right:""})),this},i.prototype.flash=function(){var a=this;if(this.isActive)return this.reset(),this.pane.addClass("flashed"),setTimeout(function(){a.pane.removeClass("flashed")},this.options.flashDelay),this},i}(),a.fn.nanoScroller=function(b){return this.each(function(){var c,d;if((d=this.nanoscroller)||(c=a.extend({},w,b),this.nanoscroller=d=new o(this,c)),b&&"object"==typeof b){if(a.extend(d.options,b),b.scrollBottom)return d.scrollBottom(b.scrollBottom);if(b.scrollTop)return d.scrollTop(b.scrollTop);if(b.scrollTo)return d.scrollTo(b.scrollTo);if("bottom"===b.scroll)return d.scrollBottom(0);if("top"===b.scroll)return d.scrollTop(0);if(b.scroll&&b.scroll instanceof a)return d.scrollTo(b.scroll);if(b.stop)return d.stop();if(b.destroy)return d.destroy();if(b.flash)return d.flash()}return d.reset()})},a.fn.nanoScroller.Constructor=o}(jQuery,window,document);
/* //# sourceMappingURL=jquery.nanoscroller.min.js.map */;
/*!
 * Bootstrap v3.1.1 (http://getbootstrap.com)
 * Copyright 2011-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown",h),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=" li:not(.divider):visible a",i=f.find("[role=menu]"+h+", [role=listbox]"+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j<i.length-1&&j++,~j||(j=0),i.eq(j).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu], [role=listbox]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show().scrollTop(0),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());c.is("a")&&b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this,d=this.tip();this.setContent(),this.options.animation&&d.addClass("fade");var e="function"==typeof this.options.placement?this.options.placement.call(this,d[0],this.$element[0]):this.options.placement,f=/\s?auto?\s?/i,g=f.test(e);g&&(e=e.replace(f,"")||"top"),d.detach().css({top:0,left:0,display:"block"}).addClass(e),this.options.container?d.appendTo(this.options.container):d.insertAfter(this.$element);var h=this.getPosition(),i=d[0].offsetWidth,j=d[0].offsetHeight;if(g){var k=this.$element.parent(),l=e,m=document.documentElement.scrollTop||document.body.scrollTop,n="body"==this.options.container?window.innerWidth:k.outerWidth(),o="body"==this.options.container?window.innerHeight:k.outerHeight(),p="body"==this.options.container?0:k.offset().left;e="bottom"==e&&h.top+h.height+j-m>o?"top":"top"==e&&h.top-m-j<0?"bottom":"right"==e&&h.right+i>n?"left":"left"==e&&h.left-i<p?"right":e,d.removeClass(l).addClass(e)}var q=this.getCalculatedOffset(e,h,i,j);this.applyPlacement(q,e),this.hoverState=null;var r=function(){c.$element.trigger("shown.bs."+c.type)};a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,r).emulateTransitionEnd(150):r()}},b.prototype.applyPlacement=function(b,c){var d,e=this.tip(),f=e[0].offsetWidth,g=e[0].offsetHeight,h=parseInt(e.css("margin-top"),10),i=parseInt(e.css("margin-left"),10);isNaN(h)&&(h=0),isNaN(i)&&(i=0),b.top=b.top+h,b.left=b.left+i,a.offset.setOffset(e[0],a.extend({using:function(a){e.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),e.addClass("in");var j=e[0].offsetWidth,k=e[0].offsetHeight;if("top"==c&&k!=g&&(d=!0,b.top=b.top+g-k),/bottom|top/.test(c)){var l=0;b.left<0&&(l=-2*b.left,b.left=0,e.offset(b),j=e[0].offsetWidth,k=e[0].offsetHeight),this.replaceArrow(l-f+j,j,"left")}else this.replaceArrow(k-g,k,"top");d&&e.offset(b)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach(),c.$element.trigger("hidden.bs."+c.type)}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.hoverState=null,this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){clearTimeout(this.timeout),this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);;
var jstz=function(){var b=function(a){a=-a.getTimezoneOffset();return null!==a?a:0},c=function(){return b(new Date(2010,0,1,0,0,0,0))},f=function(){return b(new Date(2010,5,1,0,0,0,0))},e=function(){var a=c(),d=f(),b=c()-f();return new jstz.TimeZone(jstz.olson.timezones[0>b?a+",1":0<b?d+",1,s":a+",0"])};return{determine_timezone:function(){"undefined"!==typeof console&&console.log("jstz.determine_timezone() is deprecated and will be removed in an upcoming version. Please use jstz.determine() instead.");
return e()},determine:e,date_is_dst:function(a){var d=5<a.getMonth()?f():c(),a=b(a);return 0!==d-a}}}();jstz.TimeZone=function(b){var c=null,c=b;"undefined"!==typeof jstz.olson.ambiguity_list[c]&&function(){for(var b=jstz.olson.ambiguity_list[c],e=b.length,a=0,d=b[0];a<e;a+=1)if(d=b[a],jstz.date_is_dst(jstz.olson.dst_start_dates[d])){c=d;break}}();return{name:function(){return c}}};jstz.olson={};
jstz.olson.timezones={"-720,0":"Etc/GMT+12","-660,0":"Pacific/Pago_Pago","-600,1":"America/Adak","-600,0":"Pacific/Honolulu","-570,0":"Pacific/Marquesas","-540,0":"Pacific/Gambier","-540,1":"America/Anchorage","-480,1":"America/Los_Angeles","-480,0":"Pacific/Pitcairn","-420,0":"America/Phoenix","-420,1":"America/Denver","-360,0":"America/Guatemala","-360,1":"America/Chicago","-360,1,s":"Pacific/Easter","-300,0":"America/Bogota","-300,1":"America/New_York","-270,0":"America/Caracas","-240,1":"America/Halifax",
"-240,0":"America/Santo_Domingo","-240,1,s":"America/Asuncion","-210,1":"America/St_Johns","-180,1":"America/Godthab","-180,0":"America/Argentina/Buenos_Aires","-180,1,s":"America/Montevideo","-120,0":"America/Noronha","-120,1":"Etc/GMT+2","-60,1":"Atlantic/Azores","-60,0":"Atlantic/Cape_Verde","0,0":"Etc/UTC","0,1":"Europe/London","60,1":"Europe/Berlin","60,0":"Africa/Lagos","60,1,s":"Africa/Windhoek","120,1":"Asia/Beirut","120,0":"Africa/Johannesburg","180,1":"Europe/Moscow","180,0":"Asia/Baghdad",
"210,1":"Asia/Tehran","240,0":"Asia/Dubai","240,1":"Asia/Yerevan","270,0":"Asia/Kabul","300,1":"Asia/Yekaterinburg","300,0":"Asia/Karachi","330,0":"Asia/Kolkata","345,0":"Asia/Kathmandu","360,0":"Asia/Dhaka","360,1":"Asia/Omsk","390,0":"Asia/Rangoon","420,1":"Asia/Krasnoyarsk","420,0":"Asia/Jakarta","480,0":"Asia/Shanghai","480,1":"Asia/Irkutsk","525,0":"Australia/Eucla","525,1,s":"Australia/Eucla","540,1":"Asia/Yakutsk","540,0":"Asia/Tokyo","570,0":"Australia/Darwin","570,1,s":"Australia/Adelaide",
"600,0":"Australia/Brisbane","600,1":"Asia/Vladivostok","600,1,s":"Australia/Sydney","630,1,s":"Australia/Lord_Howe","660,1":"Asia/Kamchatka","660,0":"Pacific/Noumea","690,0":"Pacific/Norfolk","720,1,s":"Pacific/Auckland","720,0":"Pacific/Tarawa","765,1,s":"Pacific/Chatham","780,0":"Pacific/Tongatapu","780,1,s":"Pacific/Apia","840,0":"Pacific/Kiritimati"};
jstz.olson.dst_start_dates={"America/Denver":new Date(2011,2,13,3,0,0,0),"America/Mazatlan":new Date(2011,3,3,3,0,0,0),"America/Chicago":new Date(2011,2,13,3,0,0,0),"America/Mexico_City":new Date(2011,3,3,3,0,0,0),"Atlantic/Stanley":new Date(2011,8,4,7,0,0,0),"America/Asuncion":new Date(2011,9,2,3,0,0,0),"America/Santiago":new Date(2011,9,9,3,0,0,0),"America/Campo_Grande":new Date(2011,9,16,5,0,0,0),"America/Montevideo":new Date(2011,9,2,3,0,0,0),"America/Sao_Paulo":new Date(2011,9,16,5,0,0,0),"America/Los_Angeles":new Date(2011,
2,13,8,0,0,0),"America/Santa_Isabel":new Date(2011,3,5,8,0,0,0),"America/Havana":new Date(2011,2,13,2,0,0,0),"America/New_York":new Date(2011,2,13,7,0,0,0),"Asia/Gaza":new Date(2011,2,26,23,0,0,0),"Asia/Beirut":new Date(2011,2,27,1,0,0,0),"Europe/Minsk":new Date(2011,2,27,2,0,0,0),"Europe/Helsinki":new Date(2011,2,27,4,0,0,0),"Europe/Istanbul":new Date(2011,2,28,5,0,0,0),"Asia/Damascus":new Date(2011,3,1,2,0,0,0),"Asia/Jerusalem":new Date(2011,3,1,6,0,0,0),"Africa/Cairo":new Date(2010,3,30,4,0,0,
0),"Asia/Yerevan":new Date(2011,2,27,4,0,0,0),"Asia/Baku":new Date(2011,2,27,8,0,0,0),"Pacific/Auckland":new Date(2011,8,26,7,0,0,0),"Pacific/Fiji":new Date(2010,11,29,23,0,0,0),"America/Halifax":new Date(2011,2,13,6,0,0,0),"America/Goose_Bay":new Date(2011,2,13,2,1,0,0),"America/Miquelon":new Date(2011,2,13,5,0,0,0),"America/Godthab":new Date(2011,2,27,1,0,0,0)};
jstz.olson.ambiguity_list={"America/Denver":["America/Denver","America/Mazatlan"],"America/Chicago":["America/Chicago","America/Mexico_City"],"America/Asuncion":["Atlantic/Stanley","America/Asuncion","America/Santiago","America/Campo_Grande"],"America/Montevideo":["America/Montevideo","America/Sao_Paulo"],"Asia/Beirut":"Asia/Gaza Asia/Beirut Europe/Minsk Europe/Helsinki Europe/Istanbul Asia/Damascus Asia/Jerusalem Africa/Cairo".split(" "),"Asia/Yerevan":["Asia/Yerevan","Asia/Baku"],"Pacific/Auckland":["Pacific/Auckland",
"Pacific/Fiji"],"America/Los_Angeles":["America/Los_Angeles","America/Santa_Isabel"],"America/New_York":["America/Havana","America/New_York"],"America/Halifax":["America/Goose_Bay","America/Halifax"],"America/Godthab":["America/Miquelon","America/Godthab"]};;
var BotConversationFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; },
        subscribers: [],
        onGoogleMapsLoaded: function () {
            var factoryData = this.getFactoryData();
            factoryData.googleMapsLoadStatus.loaded = true;
            factoryData.googleMapsLoadStatus.pending = false;            
            console.log("google maps loaded, notifying subscribers...");                                            

            $.each(factoryData.googleMapsSubscribers, function (index, callback) {
                callback();
            });      

            console.log("google maps subscribers notified");

            factoryData.googleMapsSubscribers = [];
        },
        onMapQuestLoaded: function () {
            var factoryData = this.getFactoryData();
            factoryData.mapQuestLoadStatus.loaded = true;
            factoryData.mapQuestLoadStatus.pending = false;
            console.log('mapquest loaded, notifying subscribers...');

            $.each(factoryData.mapQuestSubscribers, function (index, callback) {
                callback();
            });

            console.log('mapquest subscribers notified');

            factoryData.mapQuestSubscribers = [];
        },
    };

    function init() {
        _factoryData.defaultInstanceOptions = {
            container: null,
            linkifyText: function (text) { return text; },
            googleMapsLoaded: false,
            mapQuestLoaded: false,
            googleMapsSubscribers: [],
            mapQuestSubscribers: [],
            loadGoogleMapsScript: function (callback) { }, 
            loadMapQuestScript: function (callback) { },
            drawEvents: false,
            userIsAgent: false,
            messengerStyle: "v1",
            showTypingIndicator: false,
            showTimestamps: false,
            highlightMessageId: null,
            isMobile: false,
            isTest: false,
            callbacks: {
                onDrawn: function (shouldScroll) { },
                onUserPostback: function (payload, label) { },
                onUserPostbacks: function (payloads, label) { },
                onUserLocation: function (lat, long) { },
                onUserMessage: function (message) { },
                onUserEvent: function (event) { },
                onMessageReaction: function (toMessageId, reactionType) { },
                onModalRequested: function () {},
                onModalExitRequested: function () {},
                onUnviewedMessagesChanged: function() {},
                analyticsEvent: function () { },
                onOverlayPanelOpen: function (onCancel, onSubmit, buttonLabels) { },
                onOverlayPanelCanSubmitChanged: function (canSubmit) { },
                onAdaLastMessagesUpdated: function () { },
            }
        };
        
        _factoryData.templateDefinitionsv1 = {

            authorConversations:                
                '<div class="messages">' +
                    '<div class="placeholder">' +
                        '<div class="adaLastMessages" aria-live="polite" role="region" style="top:2px; height: 1px; position: relative; overflow: hidden; opacity: .001;">' +
                        '</div>' +
                    '</div>' +
                '</div>',

            embeddedReactions:
                '{{?(it.allowedReactionsDisplayStyleType == "0" || it.allowedReactionsDisplayStyleType == "2") && it.allowedReactions.length > 0}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("You may react with the following") }} </span>' +
                    '<div class="embedded_reactions_container{{?it.allowedReactionsDisplayStyleType == "2"}} vertical_reactions{{?}}">' +
                        '{{~it.allowedReactions :reaction:index}}' +
                            '<span class="srOnly">{{=reaction.title}} </span>' +
                            '<i class="clickable reaction_option clickableADA {{=reaction.class}}" data-id="{{=reaction.id}}" title="{{=reaction.title}}" tabindex="0" role="switch" aria-checked="false"></i>' +
                        '{{~}}' +
                    '</div>' +
                '{{?}}',

            popupReactions:
                '{{?it.allowedReactionsDisplayStyleType == "1" && it.allowedReactions.length > 0}}' +
                    '<div class="popup_reactions_container btn-group dropup caret_up">' +
                        '<i class="si-pos clickable reactions_menu_button"></i>' +
                        '<ul class="dropdown_caret dropdown-menu">' +
                            '{{~it.allowedReactions :reaction:index}}' +
                                '<li class="reaction_option_container">' +
                                    '<i class="clickable reaction_option clickableADA {{=reaction.class}}" data-id="{{=reaction.id}}" title="{{=reaction.title}}" tabindex="0" role="switch" aria-checked="false"></i>' +
                                '</li>' +
                            '{{~}}' +
                        '</ul>' +                        
                    '</div>' +
                '{{?}}',

            message:
                '<div lang="{{=it.message.context.messageLanguage}}" class="{{?(it.message.fromBot && !it.userIsAgent) || (!it.message.fromBot && it.userIsAgent)}}left_message{{??}}right_message{{?}} message {{?it.isHighlighted}}highlighted{{?}}" {{?it.message.contentType == 10}}data-event-type="{{=it.message.content.type}}"{{?}} data-id="{{=it.message.id}}" data-time="{{=it.message.date.getTime()}}">' +
                    '{{?it.containerTemplateFn != null}}' +
                        '{{=it.containerTemplateFn(it)}}' +
                    '{{?}}' +
                    '{{?it.embeddedReactionsTemplateFn != null}}' +
                        '{{=it.embeddedReactionsTemplateFn(it)}}' +
                    '{{?}}' +
                    '{{?it.popupReactionsTemplateFn != null}}' +
                        '{{=it.popupReactionsTemplateFn(it)}}' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<i class="si-pos message_reaction" style="display: none;"></i>' + // this is shown to the agent in the conversations page
                '</div>',

            text:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                '{{?it.message.content.audioUrl}}' +
                    '<div class="text_message_speech">{{=it.toHtmlFn(it.message.content.speech)}}</div>' +
                    '<i class="si-playbutton clickable text_message_audio"></i>' +
                '{{??}}' +
                    '{{=it.toHtmlFn(it.message.content.speech)}}' +
                '{{?}}' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            audio:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<i class="si-mic_on" style="position: relative; top: 3px;"></i>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            image:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content image clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<img src="{{=it.message.content.imageUrl}}" alt="{{?typeof it.message.content.altText != "undefined"}}{{=it.message.content.altText}}{{??}}Image{{?}}"></img>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            file:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content file clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '{{?it.message.content.text != null}}<div>{{=it.message.content.text}}</div>{{?}}' +
                    '{{~it.message.content.files :file:index}}' +
                        '{{?file.isImage}}' +
                            '<div><img src="{{=file.fileUrl}}" alt="{{?file.name != null}}{{=file.name}}{{??}}Image{{?}}"></img></div>' +
                        '{{??}}' +
                            '<div class="attachment"><a href="{{=file.fileUrl}}" target="_blank" data_analyticsType="FileLinkClicked"><i class="si-attachment1"></i>{{?file.name != null}}{{=file.name}}{{??}}File{{?}}</a></div>' +
                        '{{?}}' +
                    '{{~}}' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            location: // TODO: maybe put these API keys elsewhere
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content locations clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<span class="srOnly">{{= _i("Location") }} </span>' +
                    '<div id="map-{{=it.message.id}}" data-id="{{=it.message.id}}" data-time="{{=it.message.date.getTime()}}"></div>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            locations:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content locations clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<span class="srOnly">{{= _i("Locations") }} </span>' +
                    '<div id="map-{{=it.message.id}}" data-id="{{=it.message.id}}" data-time="{{=it.message.date.getTime()}}"></div>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            customPayload:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content custom_payload clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '[custom payload]' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            quickRepliesContainer:
                '<div class="bottom_border" width=100% style="overflow-x: hidden;">' +                    
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                            '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span></div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("Pick one of the following") }} </span>' +
                    '{{?it.message.content.orientation != "vertical"}}' +
                        '<div class="horizontal_scroller quick_reply_buttons {{?it.isMobile}}touch_scroller{{?}}" data-id="{{=it.message.id}}">' +
                            '{{~it.message.content.replies :value:index}}' +
                                '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                            '{{~}}' +
                            '<div class="scroll" data-direction="left"> <button class="btn btn-sm btn-primary" tabindex="0" aria-label="scroll left" role="button"> < </button> </div>' +
                            '<div class="scroll" data-direction="right"> <button class="btn btn-sm btn-primary" tabindex="0" aria-label="scroll right" role="button"> > </button> </div>' +
                        '</div>' +
                    '{{??}}' +
                        '<div class="quick_reply_buttons quick_reply_buttons_vertical" data-id="{{=it.message.id}}">' +
                            '{{~it.message.content.replies :value:index}}' +
                                '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                            '{{~}}' +
                        '</div>' +
                    '{{?}}' +
                '</div>',

            quickReply:
                '{{?it.button.type == "location"}}' +
                    '<span class="quick_reply clickable scroll_item location clickableADA" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplySendLocationClicked"><i class="si-location2"></i>Send Location</span>' +
                '{{??it.button.type == "live_agent_request"}}' +
                    '<span class="quick_reply clickable scroll_item live_agent_request clickableADA" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyLiveAgentRequestClicked">{{=it.button.label}}</span>' +
                '{{??it.button.type == "dial"}}' +
                    '<a class="quick_reply clickable scroll_item clickableADA" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyDialClicked" href="tel:{{=it.button.payload}}">{{=it.button.label}}</a>' +
                '{{??}}' +
                    '<span class="quick_reply clickable scroll_item clickableADA" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyClicked"{{?it.button.accessibilityLanguage != null && it.button.accessibilityLanguage.length > 0}} lang={{=it.button.accessibilityLanguage}}{{?}}>{{=it.button.label}}</span>' +
                '{{?}}',

            multiRepliesContainer:
                '<div class="bottom_border" width=100% style="overflow-x: hidden; width: 100%;">' +
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                            '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span></div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("Pick one or more of the following") }} </span>' +
                    '<div class="multi_reply_buttons" data-id="{{=it.message.id}}">' +
                        '{{~it.message.content.replies :value:index}}' +
                            '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                        '{{~}}' +
                    '</div>' +

                    '<div class="multi_reply_submit_container">' +
                        '<div class="multi_reply_submit clickableADA" tabindex="0" role="button" type="submit" aria-label="submit" data_analyticsType="MultiReplySubmit">{{=it.message.content.submitText || ""}}</div>' + 
                '</div>' +
                '<div class="multi_reply_cancel_container">' +
                    '<div class="multi_reply_cancel" tabindex="0" role="button" data_analyticsType="MultiReplyCancel">{{=it.message.content.cancelText || ""}}</div>' +
                    '</div>' +
                '</div>',

            multiReply:
                '<div class="multi_reply clickable clickableADA" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="MultiReplyClicked"{{?it.button.accessibilityLanguage != null && it.button.accessibilityLanguage.length > 0}} lang={{=it.button.accessibilityLanguage}}{{?}}><span class="multi_reply_sr" style="display: none;">{{= _i("You selected") }}</span>{{=it.button.label}}</div>',

            carousel:
                '<div class="bottom_border" width=100% style="overflow-x: hidden;">' +
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                            '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span></div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +

                    '<div class="horizontal_scroller {{?it.isMobile}}touch_scroller{{?}}">' +
                        '{{~it.message.content.elements :value:index}}' +
                            '{{=it.nodeTemplateFn({ content: value, index: index })}}' +
                        '{{~}}' +
                        '<div class="scroll" data-direction="left"> <button class="btn btn-sm btn-primary" tabindex="0" role="button"> < </button> </div>' +
                        '<div class="scroll" data-direction="right"> <button class="btn btn-sm btn-primary" tabindex="0" role="button"> > </button> </div>' +
                    '</div>' +
                '</div>',


            card:
                '<div class="message_content card clickable clickableADA scroll_item" data-index="{{=it.index}}" aria-label="Card {{=it.index + 1}}" role="ListItem" tabindex="0">' +
                    '{{?it.includeSrOnly}}<span class="srOnly">{{?it.message.fromBot}}Bot{{??}}You{{?}} said </span>{{?}}' +
                    '{{?it.content.imageUrl != null && it.content.imageUrl != ""}}' +
                        '<img src="{{=it.content.imageUrl}}" alt="{{?typeof it.content.altText != "undefined"}}{{=it.content.altText}}{{??}}Image{{?}}""></img>' +
                    '{{?}}' +

                    '<div class="card_row">' +
                        '<div class="name" title="{{=it.content.title}}">{{=it.content.title}}</div>' +
                        '{{?it.content.subtitle != null}}' +
                            '<div class="subtitle" title="{{=it.content.subtitle}}">{{=it.content.subtitle}}</div>' +
                        '{{?}}' +
                    '</div>' +

                    '{{~it.content.buttons :button:button_index}}' +
                        '<div class="card_row">' +
                            '{{?button.type == "postback"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="post_back" data-index="{{=button_index}}" role="link" data_analyticsType="CardPostbackLinkClicked">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "url"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="url" target="_blank" href="{{=button.payload}}" data_analyticsType="CardUrlLink">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "dial"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="url" target="_blank" href="tel:{{=button.payload}}" data_analyticsType="CardDialLink">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "account_link"}}' +
                                '<button type="button" class="btn btn-primary btn-xs clickableADA" tabindex="0" disabled data_analyticsType="CardLoginLink">Log In</button>' +
                            '{{?}}' +

                            '{{?button.type == "account_unlink"}}' +
                                '<button type="button" class="btn btn-primary btn-xs clickableADA" tabindex="0" disabled data_analyticsType="CardLogoutLink">Log Out</button>' +
                            '{{?}}' +

                            '{{?button.type == "live_agent_request"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="live_agent_request" role="link" data_analyticsType="LiveAgentLinkClicked">{{=button.text}}</a>' +
                            '{{?}}' +
                        '</div>' +
                    '{{~}}' +
                    '{{?it.includeSrOnly}}<span class="srOnly no_live">at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>{{?}}' +
                '</div>',
            /*
            notificationRequest: 
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<div class="event_message">Notification Request</div>' +
                '</div>',
            */
            notificationResponse:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<div class="event_message">Notification Response</div>' +
                '</div>',                    

            modification:
                '<div class="message modification-message text-center" data-mod-id="{{=it.modification.id}}" data-time="{{=it.modification.appliedDate.getTime()}}">' +

                    // assign
                    '{{?it.modification.type == "assign"}}' +
                        '<div class="modification noselect assign">' +
                            '{{?it.modification.outputContext.sentOk}}' +
                                'Assigned to ' +
                            '{{??}}' +
                                'Failed assigning to ' +
                            '{{?}}' +
                            '{{=it.modification.inputContext.handlerLabel}} on {{=it.modification.appliedDate.toString("d")}} {{=it.modification.appliedDate.toString("T")}}' +
                            '{{?it.modification.outputContext.errorMessage != null}}: {{=it.modification.outputContext.errorMessage}}{{?}}' +                            
                        '</div>' +
                    '{{?}}' +

                    // annotation (hack)
                    '{{?it.modification.type == "annotation"}}' +
                        '<div class="modification noselect annotation">' +
                            '{{=it.modification.outputContext.message}}' +
                            '{{?it.modification.outputContext.dataJson == "TOO_BIG"}}' +
                                ' <i class="clickable si-notification" data-id="{{=it.modification.id}}" title="Annotation Truncated"></i>' +
                            '{{??it.modification.outputContext.dataType != "none"}}' +
                                ' <i class="clickable si-search drilldown" data-id="{{=it.modification.id}}"></i>' +
                            '{{??}}' +

                            '{{?}}' +
                        '</div>' +
                    '{{?}}' +

                    // conversation handler change
                    '{{?it.modification.type == "conversation handler change"}}' +
                        '<div class="modification noselect assign">' +
                            '{{=it.modification.prettyValue}}' +
                        '</div>' +
                    '{{?}}' +

                    '{{?it.modification.type == "conversation break"}}' +
                        '<hr class="conversation_break">' +
                    '{{?}}' +

                '</div>',

            event:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '{{?it.message.content.type == 0}}' +
                        '<div class="event_message">Get Started</div>' +
                    '{{??it.message.content.type == 1}}' +
                        '<div class="event_message">Subscribe</div>' +
                    '{{??it.message.content.type == 2}}' +
                        '<div class="event_message">Call Started</div>' +
                    '{{??it.message.content.type == 3}}' +
                        '<div class="event_message">End Session</div>' +
                    '{{??it.message.content.type == 4}}' +
                        '<div class="event_message">Session Ended</div>' +
                    '{{??it.message.content.type == 5}}' +
                        '<div class="event_message">Polling Ended</div>' +
                    '{{??it.message.content.type == 6}}' +
                        '<div class="event_message">Account Linked</div>' +
                    '{{??it.message.content.type == 7}}' +
                        '<div class="event_message">Account Unlinked</div>' +
                    '{{??it.message.content.type == 8}}' +                        
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context != null}}' +
                                'Agent "{{=it.message.content.context.userInfo.name}}" connected' +
                            '{{??}}' +
                                'Agent connected' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 9}}' +
                        '<div class="systemMessage">' +
                            'Agent disconnected' +
                        '</div>' +
                    '{{??it.message.content.type == 10}}' +
                        '<div class="event_message">Cancel</div>' +
                    '{{??it.message.content.type == 11}}' +
                        '<div class="event_message">Stop</div>' +
                    '{{??it.message.content.type == 12}}' +
                        '<div class="event_message">Help</div>' +
                    '{{??it.message.content.type == 14}}' +
                        '<div class="event_message">User Disconnected</div>' +
                    '{{??it.message.content.type == 15}}' +
                        '<div class="event_message">Unable To Add To Agent Queue - No agents online</div>' +
                    '{{??it.message.content.type == 16}}' +
                        '<div class="event_message">Added To Agent Queue</div>' +
                    '{{??it.message.content.type == 17}}' +

                        '<div class="event_message">Removed From Agent Queue {{?it?.message?.content?.context?.reason}}: {{=it.message.content.context.reason}}{{?}}</div>' +
                    '{{??it.message.content.type == 18}}' +
                        '<div class="systemMessage">' +
                            '<i class="si-assign"></i>' +
                            'Agent "{{=it.message.content.context.from.userInfo.name}}" transferred conversation to {{=it.message.content.context.to.targetType}} "{{=it.message.content.context.to.displayName}}"' +
                        '</div>' +                    
                    '{{??it.message.content.type == 19}}' +
                        '<div class="event_message">User Connected</div>' +
                    '{{??it.message.content.type == 20}}' +
                        '<div class="systemMessage">' +
                            'Agent escalated' +
                        '</div>' +
                    '{{??it.message.content.type == 21}}' +
                        '<div class="systemMessage">' +
                            'Agents available' +
                        '</div>' +
                    '{{??it.message.content.type == 22}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call started' +
                        '</div>' +
                    '{{??it.message.content.type == 23}}' +
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context.agentInfo == null}}' +
                                'User connected to multimedia call' +
                            '{{??}}' +
                                'Agent "{{=it.message.content.context.agentInfo.displayName}}" connected to multimedia call' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 24}}' +
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context.agentInfo == null}}' +
                                'User disconnected from multimedia call' +
                            '{{??}}' +
                                'Agent {{=it.message.content.context.agentInfo.displayName}} disconnected from multimedia call' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 25}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call ended' +
                        '</div>' +
                    '{{??it.message.content.type == 26}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call declined' +
                        '</div>' +            
                    '{{??it.message.content.type == 27}}' +
                        '<div class="systemMessage">' +
                            'Transfer Requested' +
                        '</div>' +
                    '{{??it.message.content.type == 28}}' +
                        '<div class="event_message">Input Timeout</div>' +
                    '{{??it.message.content.type == 32}}' +
                        '<div class="event_message">' +
                            'Simple survey response: ' +
                            '{{?it.message.content.context.score != null}}' +
                                '{{=it.message.content.context.score}}/{{=it.message.content.context.maxScore}}' +
                            '{{?}}' +
                            '{{?it.message.content.context.text != null}}' +
                                ' {{=it.message.content.context.text}}' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 33}}' +
                        '<div class="event_message">Live Agent Requested</div>' +
                    '{{??it.message.content.type == 34}}' +
                        '<div class="event_message">Link Account Requested</div>' +
                    '{{??it.message.content.type == 35}}' +
                        '<div class="event_message">Checkout Requested</div>' +
                    '{{??it.message.content.type == 36}}' +
                        '<div class="event_message">Payment Succeeded</div>' +
                    '{{??it.message.content.type == 37}}' +
                        '<div class="event_message">Payment Failed</div>' +
                    '{{??it.message.content.type == 38}}' +
                        '<div class="event_message">Referral</div>' +
                    '{{??it.message.content.type == 39}}' +
                        '<div class="event_message">Error</div>' +
                    '{{??it.message.content.type == 40}}' +                
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                        //'<div class="systemMessage">' +
                            '<div class="ReviewMetaInfo">' +
                                 // location
                                '<a target="_blank" href="https://www.google.com/maps/place/?q=place_id:{{=it.message.content.context.location.placeId}}">{{=it.message.content.context.location.title}}</a><br/>' +                            

                                 // stars
                                '{{=it.message.content.context.review.stars}}' + ' / ' + '{{=it.message.content.context.review.starsMax}} stars<br/>' + 
                
                                // review created/updated time
                                '{{?it.message.content.context.review.isUpdate}}' +
                                    // Review updated.  Guaranteed to have "updatedTime"
                                    'Review updated {{=prettyDate(new Date(it.message.content.context.review.updatedTime))}}' +
                                '{{??}}' +
                                    // Review created.  Use review.createdDate if it exists else fallback to message.date
                                    '{{?it.message.content.context.review.createdTime}}' +
                                        'Review created {{=prettyDate(new Date(it.message.content.context.review.createdTime))}}' +
                                    '{{??}}' +
                                        'Review created {{=prettyDate(it.message.date)}}' +
                                    '{{?}}' +
                                '{{?}}' +
                             '</div>' +

                             // review text
                            '{{?it.message.content.context.review.text != null}}' +
                                '{{=it.message.content.context.review.text}}' +                            
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 41}}' +
                        '<div class="event_message">Unsupported Media</div>' +
                    '{{??it.message.content.type == 42}}' +
                        '<div class="event_message">No User Response</div>' +
                    '{{??it.message.content.type == 43}}' +
                        '<div class="event_message">Survey Request</div>' +
                    '{{??it.message.content.type == 44}}' +
                        '<div class="systemMessage">{{?it?.message?.content?.context?.message}}{{=it.message.content.context.message}}{{??}}Agent Queue Status{{?}}</div>' +
                    '{{?}}' +
                    
                '</div>',

            typingIndicator:
                '<div title="Bot is typing..." class="message_content">' +
                    '<div class="wave">' +
                        '<span class="dot"></span>' +
                        '<span class="dot"></span>' +
                        '<span class="dot"></span>' +
                    '</div>' +
                '</div>',

            dateInput:
                '<div class="expected_response expected_response_date">' +
                    '<input type="date" class="expected_response_date_input"></input>' +
                    '<div class="expected_response_submit expected_response_date_submit">{{=it.submitText}}</div>' +
                '</div>',

            dateTimeInput:
                '<div class="expected_response expected_response_datetime">' +
                    '<input type="date" class="expected_response_date_input"></input>' +
                    '<input type="time" class="expected_response_time_input"></input>' +
                    '<div class="expected_response_submit expected_response_datetime_submit">{{=it.submitText}}</div>' +
                '</div>',

            timestamp:
                '<div class="{{?(it.message.fromBot && !it.userIsAgent) || (!it.message.fromBot && it.userIsAgent)}}left_message{{??}}right_message{{?}} timestamp" data-time="{{=it.message.date.getTime()}}" data-id="{{=it.message.id}}">{{=it.text}}</div>',
        };

        _factoryData.templateDefinitionsv2 = {

            authorConversations:
                '<div class="messages">' +
                    '<div class="placeholder">' +
                        '<div class="adaLastMessages" aria-busy="false" aria-live="polite" aria-atomic="true" role="region" style="top:2px; height: 1px; position: relative; overflow: hidden; opacity: .001;">' +
                        '</div>' +
                    '</div>' +
                '</div>',

            embeddedReactions:
                '{{?(it.allowedReactionsDisplayStyleType == "0" || it.allowedReactionsDisplayStyleType == "2") && it.allowedReactions.length > 0}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("You may react with the following") }} </span>' +
                    '<div class="embedded_reactions_container{{?it.allowedReactionsDisplayStyleType == "2"}} vertical_reactions{{?}}">' +
                        '{{~it.allowedReactions :reaction:index}}' +
                            '<span class="srOnly">{{=reaction.title}} </span>' +
                            '<i class="clickable reaction_option clickableADA {{=reaction.class}}" data-id="{{=reaction.id}}" title="{{=reaction.title}}" tabindex="0" role="switch" aria-checked="false"></i>' +
                        '{{~}}' +
                    '</div>' +
                '{{?}}',

            popupReactions:
                '{{?it.allowedReactionsDisplayStyleType == "1" && it.allowedReactions.length > 0}}' +
                    '<div class="popup_reactions_container btn-group dropup caret_up">' +
                        '<i class="si-pos clickable reactions_menu_button"></i>' +
                        '<ul class="dropdown_caret dropdown-menu">' +
                            '{{~it.allowedReactions :reaction:index}}' +
                                '<li class="reaction_option_container">' +
                                    '<i class="clickable reaction_option clickableADA {{=reaction.class}}" data-id="{{=reaction.id}}" title="{{=reaction.title}}" tabindex="0" role="switch" aria-checked="false"></i>' +
                                '</li>' +
                            '{{~}}' +
                        '</ul>' +
                    '</div>' +
                '{{?}}',

            message:
                '<div lang="{{=it.message.context.messageLanguage}}" class="{{?(it.message.fromBot && !it.userIsAgent) || (!it.message.fromBot && it.userIsAgent)}}left_message{{??}}right_message{{?}} message {{?it.isHighlighted}}highlighted{{?}}" {{?it.message.contentType == 10}}data-event-type="{{=it.message.content.type}}"{{?}} data-id="{{=it.message.id}}" data-type="{{=it.message.contentType}}" data-time="{{=it.message.date.getTime()}}">' +
                    '{{?it.containerTemplateFn != null}}' +
                        '{{=it.containerTemplateFn(it)}}' +
                    '{{?}}' +
                    '{{?it.embeddedReactionsTemplateFn != null}}' +
                        '{{=it.embeddedReactionsTemplateFn(it)}}' +
                    '{{?}}' +
                    '{{?it.popupReactionsTemplateFn != null}}' +
                        '{{=it.popupReactionsTemplateFn(it)}}' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<i class="si-pos message_reaction" style="display: none;"></i>' + // this is shown to the agent in the conversations page
                '</div>',

            text:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '{{?it.message.content.audioUrl}}' +
                        '<div class="text_message_speech">{{=it.toHtmlFn(it.message.content.speech)}}</div>' +
                        '<i class="si-playbutton clickable text_message_audio"></i>' +
                    '{{??}}' +
                        '{{=it.toHtmlFn(it.message.content.speech)}}' +
                    '{{?}}' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +

                    '{{?it.message.content.article != null}}' +
                    '{{?it.message.content.article.showMoreText != ""}} <button type="button" class="show_more_link" tabindex="0">{{=it.message.content.article.showMoreText}}</button> {{?}}' +
                    '<div class="overlay_panel" style="display: none;">' +                            
                        '<div class="nano">' +
                            '<div class="content article_html_container">' +
                                '{{=it.message.content.article.html}}' +                      
                            '</div>' +                                
                        '</div>' +
                        '<div class="overlay_footer"></div>' +
                    '</div>' + 
                    '{{?}}' +
                '</div>',

            audio:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<i class="si-mic_on" style="position: relative; top: 3px;"></i>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            image:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content image clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<img src="{{=it.message.content.imageUrl}}" alt="{{?typeof it.message.content.altText != "undefined"}}{{=it.message.content.altText}}{{??}}Image{{?}}"></img>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            file:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content file clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '{{?it.message.content.text != null}}<div>{{=it.message.content.text}}</div>{{?}}' +
                    '{{~it.message.content.files :file:index}}' +
                        '{{?file.isImage}}' +
                            '<div><img src="{{=file.fileUrl}}" alt="{{?file.name != null}}{{=file.name}}{{??}}Image{{?}}"></img></div>' +
                        '{{??}}' +
                            '<div class="attachment"><a href="{{=file.fileUrl}}" target="_blank" data_analyticsType="FileLinkClicked"><i class="si-attachment1"></i>{{?file.name != null}}{{=file.name}}{{??}}File{{?}}</a></div>' +
                        '{{?}}' +
                    '{{~}}' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            location: // TODO: maybe put these API keys elsewhere 
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content locations clickableADA" tabindex="0" style="overflow:hidden;">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<span class="srOnly">{{= _i("Location") }} </span>' +
                    '<div id="map-{{=it.message.id}}" class="mapContainer" data-id="{{=it.message.id}}" data-time="{{=it.message.date.getTime()}}"></div>' +
                '<div class="extraInfo" style="float:left; padding:12px; font-size:11px;">' +
                    '<b class = "title"></b>' +
                    '<div>' +
                        '<div><span class="addressLine1"></span></div>' +
                        '<div><span class="addressLine2"></span></div>' +
                        '<div class="navigation_details" style="margin-top:8px;" hidden>' +
                            '<svg class="mapPin" width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5 14C5 14 10 9.5 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5C0 9.5 5 14 5 14ZM5 7C6.10457 7 7 6.10457 7 5C7 3.89543 6.10457 3 5 3C3.89543 3 3 3.89543 3 5C3 6.10457 3.89543 7 5 7Z" fill="#21272A"/></svg><span class="direction_distance" style="font-weight:700; margin-right:14px; margin-left:8px;"></span>' +
                            '<svg class="mapCar" width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill- rule="evenodd" clip - rule="evenodd" d = "M10.6133 0.694375C10.48 0.28875 10.1067 0 9.66667 0H2.33333C1.89333 0 1.52667 0.28875 1.38667 0.694375L0 4.8125V9.3125C0 9.69063 0.3 10 0.666667 10H1.33333C1.7 10 2 9.69063 2 9.3125V8.625H10V9.3125C10 9.69063 10.3 10 10.6667 10H11.3333C11.7 10 12 9.69063 12 9.3125V4.8125L10.6133 0.694375ZM2.33333 7.5625C1.78 7.5625 1.33333 7.10187 1.33333 6.53125C1.33333 5.96063 1.78 5.5 2.33333 5.5C2.88667 5.5 3.33333 5.96063 3.33333 6.53125C3.33333 7.10187 2.88667 7.5625 2.33333 7.5625ZM9.66667 7.5625C9.11333 7.5625 8.66667 7.10187 8.66667 6.53125C8.66667 5.96063 9.11333 5.5 9.66667 5.5C10.22 5.5 10.6667 5.96063 10.6667 6.53125C10.6667 7.10187 10.22 7.5625 9.66667 7.5625ZM1.33333 4.125L2.33333 1.03125H9.66667L10.6667 4.125H1.33333Z" fill = "#2F3744"/></svg><span class="direction_traveltime" style="font-weight:700; margin-left:8px;"></span>' +
                        '</div > ' +
                    '</div>' +
                '</div>' +
                    '<div class="mapActionButton" style="" tabindex="0" role="button">Open Map</div>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            locations:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content locations clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '<span class="srOnly">{{= _i("Locations") }} </span>' +
                    '<div id="map-{{=it.message.id}}" data-id="{{=it.message.id}}" data-time="{{=it.message.date.getTime()}}"></div>' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            customPayload:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content custom_payload clickableADA" tabindex="0">' +
                    '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                    '[custom payload]' +
                    '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                '</div>',

            quickRepliesContainer:
                '<div class="bottom_border quick_reply_container" width=100% style="overflow-x: hidden;">' +
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                            '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span>' +
                            '{{?it.message.content.article != null}}' +
                                '{{?it.message.content.article.showMoreText != ""}}<button type="button" class="show_more_link" tabindex="0">{{=it.message.content.article.showMoreText}}</button> {{?}}' +
                                '<div class="overlay_panel" style="display: none;">' +                            
                                    '<div class="nano">' +
                                        '<div class="content article_html_container">' +
                                            '{{=it.message.content.article.html}}' +                      
                                        '</div>' +                                
                                    '</div>' +
                                    '<div class="overlay_footer"></div>' +
                                '</div>' + 
                            '{{?}}' +
                            '</div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("Pick one of the following") }} </span>' +
                    '{{?it.message.content.orientation != "vertical"}}' +
                        '<div class="quick_reply_buttons" data-id="{{=it.message.id}}">' +
                            '{{~it.message.content.replies :value:index}}' +
                                '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                            '{{~}}' +                            
                        '</div>' +
                    '{{??}}' +
                        '<div class="quick_reply_buttons quick_reply_buttons_vertical" data-id="{{=it.message.id}}">' +
                            '{{~it.message.content.replies :value:index}}' +
                                '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                            '{{~}}' +
                        '</div>' +
                    '{{?}}' +
                '</div>',

            quickReply:
                '{{?it.button.type == "location"}}' +
                    '<button class="quick_reply clickable scroll_item location noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplySendLocationClicked"><i class="si-location2"></i>Send Location</button>' +
                '{{??it.button.type == "live_agent_request"}}' +
                    '<button class="quick_reply clickable scroll_item live_agent_request noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyLiveAgentRequestClicked">{{=it.button.label}}</button>' +
                '{{??it.button.type == "dial"}}' +
                    '<a class="quick_reply clickable scroll_item" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyDialClicked" href="tel:{{=it.button.payload}}">{{=it.button.label}}</a>' +
                '{{??it.button.type == "url"}}' +
                    '<button class="quick_reply clickable scroll_item noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickUrlReplyClicked">{{=it.button.label}}</button>' +
                '{{??}}' +
                    '<button class="quick_reply clickable scroll_item noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="QuickReplyClicked">{{=it.button.label}}</button>' +
                '{{?}}',

            multiRepliesContainer:
                '<div class="bottom_border multi_replies_container" width=100% style="overflow-x: hidden; width: 100%;">' +
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                            '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span></div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +
                    '<span class="srOnly">{{= _i("Pick one or more of the following") }} </span>' +
                    '<div class="multi_reply_buttons" data-id="{{=it.message.id}}">' +
                        '<div class="open_overlay_panel_button_container">' +
                            '<div class="open_overlay_panel_button clickable {{?it.message.content.recieveSubtitle != null && it.message.content.recieveSubtitle.length > 1}}with_label{{?}}" tabindex="0" role="button" aria-label="open picker">' +
                                '{{?it.message.content.recieveSubtitle != null && it.message.content.recieveSubtitle.length > 1}}' +
                                    '<div class="label_container">' +
                                        '{{=it.message.content.recieveSubtitle}}' +
                                    '</div>' +
                                '{{?}}' +
                                '<div class="icon_container">' +
                                    '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">' +
                                        '<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 14L4 12.5L10 7L16 12.5L14.5 14L10 9.69995L5.5 14Z" fill="#21272A"/>' +
                                    '</svg>' +
                                '</div>' +
                            '</div>' +
                        '</div>' +
                        '<div class="overlay_panel" style="display: none;">' +                            
                            '<div class="nano">' +
                                '<div class="content">' +
                                    '<div class="overlay_header">' +
                                        '<div class="overlay_label" tabindex="0">{{?it.message.content.title != null}}{{=it.message.content.title}}{{?}}</div>' +
                                    '</div>' +
                                    '<div class="grid">' +
                                        '{{~it.message.content.replies :value:index}}' +
                                            '{{=it.nodeTemplateFn({ button: value, index: index })}}' +
                                        '{{~}}' +
                                    '</div>' +                                    
                                '</div>' +                                
                            '</div>' +
                '<div class="overlay_footer">'+
                    '<div class="multi_reply_submit" style="display: none">{{=it.message.content.submitText || ""}}</div>' +
                    '<div class="multi_reply_cancel" style="display: none">{{=it.message.content.cancelText || ""}}</div>' +
                    '</div >' +
                '</div>' + 
                    '</div>' +
                '</div>',

            multiReply:
                '<div class="multi_reply clickable noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="MultiReplyClicked">' +
                    '<span class="multi_reply_sr" style="display: none;">{{= _i("You selected") }}</span>' +

                    '{{?it.button.url != null && it.button.url.length > 0}}' +
                        '<div class="image_holder">' +
                            '<img src="{{=it.button.url}}"></img>' +
                            '<div class="check_placeholder">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">' +
                                    '<circle cx="9" cy="9" r="9" fill="white"/>' +
                                    '<path d="M14.0001 5L7.00004 12L4 9" stroke="#0038E5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
                                '</svg>' +
                            '</div>' +
                        '</div>' +
                    '{{?}}' +

                    '<div class="multi_reply_label">{{=it.button.label}}</div>' +
                '</div>',

            //multiReply:
            //    '<div class="multi_reply clickable noselect" data-index="{{=it.index}}" tabindex="0" role="button" data_analyticsType="MultiReplyClicked"><span class="multi_reply_sr" style="display: none;">You selected </span>{{=it.button.label}}</div>',

            carousel:
                '<div class="carousel bottom_border" width=100% style="overflow-x: hidden;">' +
                    '{{?it.message.content.title != ""}}' +
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                            '<span class="srOnly">{{?it.message.fromBot}}{{= _i("Bot said")}}{{??}}{{= _i("You said")}}{{?}} </span>' +
                                '<div class="selection_message text-center"><span>{{=it.message.content.title}}<span></div>' +
                            '<span class="srOnly no_live"> at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>' +
                        '</div>' +
                    '{{?}}' +
                    '<div class="clearfix"></div>' +

                    '<div class="horizontal_scroller">' +
                        '<div class="scroll_container">' +
                            '<div class="scroll" data-direction="left"> <button tabindex="0" role="button"><i class="si-left_chevron"/></button> </div>' +
                            '<div class="scroll" data-direction="right"> <button tabindex="0" role="button"><i class="si-right_chevron"/></button> </div>' +
                        '</div>' +
                        '<div class="scroller {{=it.style}}{{?it.isMobile}} touch_scroller{{?}}">' +
                            '{{~it.message.content.elements :value:index}}' +
                                '{{=it.nodeTemplateFn({ content: value, index: index })}}' +
                            '{{~}}' +
                        '</div>' +                        
                    '</div>' +
                '</div>',


            card:
                '<div class="message_content card clickable scroll_item v2" data-index="{{=it.index}}" aria-label="Card {{=it.index + 1}}" role="ListItem" tabindex="0">' +
                    '{{?it.includeSrOnly}}<span class="srOnly">{{?it.message.fromBot}}Bot{{??}}You{{?}} said </span>{{?}}' +
                    '{{?it.content.imageUrl != null && it.content.imageUrl != ""}}' +
                        '<img src="{{=it.content.imageUrl}}" alt="{{?typeof it.content.altText != "undefined"}}{{=it.content.altText}}{{??}}Image{{?}}""></img>' +
                    '{{?}}' +

                    '<div class="card_row">' +
                        '{{?it.content.title != null && it.content.title != ""}}' +
                            '<div class="name" title="{{=it.content.title}}">{{=it.content.title}}</div>' +
                        '{{?}}' +                        
                    '</div>' +

                    '<div class="card_row">' +
                        '{{?it.content.subtitle != null && it.content.subtitle != ""}}' +
                                '<div class="subtitle" title="{{=it.content.subtitle}}">{{=it.content.subtitle}}</div>' +
                        '{{?}}' +
                    '</div>' +

                    '{{~it.content.buttons :button:button_index}}' +
                        '<div class="card_row">' +
                            '{{?button.type == "postback"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="post_back" data-index="{{=button_index}}" role="link" data_analyticsType="CardPostbackLinkClicked">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "url"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="url" target="_blank" href="{{=button.payload}}" data_analyticsType="CardUrlLink">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "dial"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="url" target="_blank" href="tel:{{=button.payload}}" data_analyticsType="CardDialLink">{{=button.text}}</a>' +
                            '{{?}}' +

                            '{{?button.type == "account_link"}}' +
                                '<button type="button" class="btn btn-primary btn-xs clickableADA" tabindex="0" disabled data_analyticsType="CardLoginLink">Log In</button>' +
                            '{{?}}' +

                            '{{?button.type == "account_unlink"}}' +
                                '<button type="button" class="btn btn-primary btn-xs clickableADA" tabindex="0" disabled data_analyticsType="CardLogoutLink">Log Out</button>' +
                            '{{?}}' +

                            '{{?button.type == "live_agent_request"}}' +
                                '<a class="clickableADA" tabindex="0" data-type="live_agent_request" role="link" data_analyticsType="LiveAgentLinkClicked">{{=button.text}}</a>' +
                            '{{?}}' +
                        '</div>' +
                    '{{~}}' +
                    '{{?it.includeSrOnly}}<span class="srOnly no_live">at {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}} </span>{{?}}' +
                '</div>',

            modification:
                '<div class="message modification-message text-center" data-mod-id="{{=it.modification.id}}" data-time="{{=it.modification.appliedDate.getTime()}}">' +

                    // assign
                    '{{?it.modification.type == "assign"}}' +
                        '<div class="modification noselect assign">' +
                            '{{?it.modification.outputContext.sentOk}}' +
                                'Assigned to ' +
                            '{{??}}' +
                                'Failed assigning to ' +
                            '{{?}}' +
                            '{{=it.modification.inputContext.handlerLabel}} on {{=it.modification.appliedDate.toString("d")}} {{=it.modification.appliedDate.toString("T")}}' +
                            '{{?it.modification.outputContext.errorMessage != null}}: {{=it.modification.outputContext.errorMessage}}{{?}}' +
                        '</div>' +
                    '{{?}}' +

                    // annotation (hack)
                    '{{?it.modification.type == "annotation"}}' +
                        '<div class="modification noselect annotation">' +
                            '{{=it.modification.outputContext.message}}' +
                                '{{?it.modification.outputContext.dataJson == "TOO_BIG"}}' +
                                    '<ul><li></li></ul> Truncated' +
                                '{{??it.modification.outputContext.dataType != "none"}}' +
                                    '<ul><li></li></ul> <span class="clickable drilldown" data-id="{{=it.modification.id}}">View</span>' +
                                '{{??}}' +
                            '{{?}}' +
                        '</div>' +
                    '{{?}}' +

                    // conversation handler change
                    '{{?it.modification.type == "conversation handler change"}}' +
                        '<div class="modification noselect assign">' +
                            '{{=it.modification.prettyValue}}' +
                        '</div>' +
                    '{{?}}' +

                    '{{?it.modification.type == "conversation break"}}' +
                        '<hr class="conversation_break">' +
                    '{{?}}' +

                '</div>',

            event:
                '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                    '{{?it.message.content.type == 0}}' +
                        '<div class="event_message">Get Started</div>' +
                    '{{??it.message.content.type == 1}}' +
                        '<div class="event_message">Subscribe</div>' +
                    '{{??it.message.content.type == 2}}' +
                        '<div class="event_message">Call Started</div>' +
                    '{{??it.message.content.type == 3}}' +
                        '<div class="event_message">End Session</div>' +
                    '{{??it.message.content.type == 4}}' +
                        '<div class="event_message">Session Ended</div>' +
                    '{{??it.message.content.type == 5}}' +
                        '<div class="event_message">Polling Ended</div>' +
                    '{{??it.message.content.type == 6}}' +
                        '<div class="event_message">Account Linked</div>' +
                    '{{??it.message.content.type == 7}}' +
                        '<div class="event_message">Account Unlinked</div>' +
                    '{{??it.message.content.type == 8}}' +
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context != null}}' +
                                'Agent "{{=it.message.content.context.userInfo.name}}" connected' +
                            '{{??}}' +
                                'Agent connected' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 9}}' +
                        '<div class="systemMessage">' +
                            'Agent disconnected' +
                        '</div>' +
                    '{{??it.message.content.type == 10}}' +
                        '<div class="event_message">Cancel</div>' +
                    '{{??it.message.content.type == 11}}' +
                        '<div class="event_message">Stop</div>' +
                    '{{??it.message.content.type == 12}}' +
                        '<div class="event_message">Help</div>' +
                    '{{??it.message.content.type == 14}}' +
                        '<div class="event_message">User Disconnected</div>' +
                    '{{??it.message.content.type == 15}}' +
                        '<div class="event_message">Unable To Add To Agent Queue - No agents online</div>' +
                    '{{??it.message.content.type == 16}}' +
                        '<div class="event_message">Added To Agent Queue</div>' +
                    '{{??it.message.content.type == 17}}' +
                        '<div class="event_message">Removed From Agent Queue {{?it?.message?.content?.context?.reason}}: {{=it.message.content.context.reason}}{{?}}</div>' +
                    '{{??it.message.content.type == 18}}' +
                        '<div class="systemMessage">' +
                            '<i class="si-assign"></i>' +
                            'Agent "{{=it.message.content.context.from.userInfo.name}}" transferred conversation to {{=it.message.content.context.to.targetType}} "{{=it.message.content.context.to.displayName}}"' +
                        '</div>' +
                    '{{??it.message.content.type == 19}}' +
                        '<div class="event_message">User Connected</div>' +
                    '{{??it.message.content.type == 20}}' +
                        '<div class="systemMessage">' +
                            'Agent escalated' +
                        '</div>' +
                    '{{??it.message.content.type == 21}}' +
                        '<div class="systemMessage">' +
                            'Agents available' +
                        '</div>' +
                    '{{??it.message.content.type == 22}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call started' +
                        '</div>' +
                    '{{??it.message.content.type == 23}}' +
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context.agentInfo == null}}' +
                                'User connected to multimedia call' +
                            '{{??}}' +
                                'Agent "{{=it.message.content.context.agentInfo.displayName}}" connected to multimedia call' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 24}}' +
                        '<div class="systemMessage">' +
                            '{{?it.message.content.context.agentInfo == null}}' +
                                'User disconnected from multimedia call' +
                            '{{??}}' +
                                'Agent {{=it.message.content.context.agentInfo.displayName}} disconnected from multimedia call' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 25}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call ended' +
                        '</div>' +
                    '{{??it.message.content.type == 26}}' +
                        '<div class="systemMessage">' +
                            'Multimedia call declined' +
                        '</div>' +
                    '{{??it.message.content.type == 27}}' +
                        '<div class="systemMessage">' +
                            'Transfer Requested' +
                        '</div>' +                    
                    '{{??it.message.content.type == 32}}' +
                        '<div class="event_message">' +
                            'Simple survey response: ' +
                            '{{?it.message.content.context.score != null}}' +
                                '{{=it.message.content.context.score}}/{{=it.message.content.context.maxScore}}' +
                            '{{?}}' +
                            '{{?it.message.content.context.text != null}}' +
                                ' {{=it.message.content.context.text}}' +
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 33}}' +
                        '<div class="event_message">Live Agent Requested</div>' +
                    '{{??it.message.content.type == 34}}' +
                        '<div class="event_message">Link Account Requested</div>' +
                    '{{??it.message.content.type == 35}}' +
                        '<div class="event_message">Checkout Requested</div>' +
                    '{{??it.message.content.type == 36}}' +
                        '<div class="event_message">Payment Succeeded</div>' +
                    '{{??it.message.content.type == 37}}' +
                        '<div class="event_message">Payment Failed</div>' +
                    '{{??it.message.content.type == 28}}' +
                        '<div class="event_message">Input Timeout</div>' +
                    '{{??it.message.content.type == 38}}' +
                        '<div class="event_message">Referral</div>' +
                    '{{??it.message.content.type == 39}}' +
                        '<div class="event_message">Error</div>' +
                    '{{??it.message.content.type == 40}}' +                
                        '<div title="{{= _i("Posted")}} {{=it.message.date.toString(\'d\')}} {{=it.message.date.toString(\'T\')}}" class="message_content clickableADA" tabindex="0">' +
                        //'<div class="systemMessage">' +
                             // location
                            '<a target="_blank" href="https://www.google.com/maps/place/?q=place_id:{{=it.message.content.context.location.placeId}}">{{=it.message.content.context.location.title}}</a><br/>' +

                             // stars
                            '{{=it.message.content.context.review.stars}}' + ' / ' + '{{=it.message.content.context.review.starsMax}} stars<br/>' + 

                             // review text
                            '{{?it.message.content.context.review.text != null}}' +
                                '{{=it.message.content.context.review.text}}' +                            
                            '{{?}}' +
                        '</div>' +
                    '{{??it.message.content.type == 41}}' +
                        '<div class="event_message">Unsupported Media</div>' +
                    '{{??it.message.content.type == 42}}' +
                        '<div class="event_message">No User Response</div>' +
                    '{{??it.message.content.type == 43}}' +
                        '<div class="event_message">Survey Request</div>' +
                    '{{??it.message.content.type == 44}}' +
                        '<div class="systemMessage">{{?it?.message?.content?.context?.message}}{{=it.message.content.context.message}}{{??}}Agent Queue Status{{?}}</div>' +
                    '{{?}}' +

                '</div>',

            typingIndicator:
                '<div title="Bot is typing..." class="message_content">' +
                    '<div class="wave">' +
                        '<span class="dot"></span>' +
                        '<span class="dot"></span>' +
                        '<span class="dot"></span>' +
                    '</div>' +
                '</div>',

            dateInput:
                '<div class="expected_response expected_response_date">' +
                    '<div class="open_overlay_panel_button_container">' +
                        '<div class="open_overlay_panel_button clickable" tabindex="0" role="button" aria-label="open picker">' +                            
                            '<div class="icon_container">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">' +
                                    '<path d="M5 8H7V9H5V8Z" fill="#2F3744"/>' +
                                    '<path d="M5 11H7V12H5V11Z" fill="#2F3744"/>' +
                                    '<path d="M8 8H10V9H8V8Z" fill="#2F3744"/>' +
                                    '<path d="M8 11H10V12H8V11Z" fill="#2F3744"/>' +
                                    '<path d="M11 8H13V9H11V8Z" fill="#2F3744"/>' +
                                    '<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2V1H6V2H12V1H13V2H13.5C14.8807 2 16 3.11929 16 4.5V13.5C16 14.8807 14.8807 16 13.5 16H4.5C3.11929 16 2 14.8807 2 13.5V4.5C2 3.11929 3.11929 2 4.5 2H5ZM5 4V3H4.5C3.67157 3 3 3.67157 3 4.5V5H15V4.5C15 3.67157 14.3284 3 13.5 3H13V4H12V3H6V4H5ZM3 6V13.5C3 14.3284 3.67157 15 4.5 15H13.5C14.3284 15 15 14.3284 15 13.5V6H3Z" fill="#2F3744"/>' +
                                '</svg>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                    '<div class="overlay_panel" style="display: none;">' +                            
                        '<div class="nano">' +
                            '<div class="content">' +
                                '<div class="expected_date_picker"></div>' +                                    
                            '</div>' +                                
                        '</div>' +
                        '<div class="overlay_footer"></div>' +
                    '</div>' + 
                '</div>',

            dateTimeInput:
                '<div class="expected_response expected_response_date">' +
                    '<div class="open_overlay_panel_button_container">' +
                        '<div class="open_overlay_panel_button clickable" tabindex="0" role="button" aria-label="open picker">' +                            
                            '<div class="icon_container">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">' +
                                    '<path d="M5 8H7V9H5V8Z" fill="#2F3744"/>' +
                                    '<path d="M5 11H7V12H5V11Z" fill="#2F3744"/>' +
                                    '<path d="M8 8H10V9H8V8Z" fill="#2F3744"/>' +
                                    '<path d="M8 11H10V12H8V11Z" fill="#2F3744"/>' +
                                    '<path d="M11 8H13V9H11V8Z" fill="#2F3744"/>' +
                                    '<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2V1H6V2H12V1H13V2H13.5C14.8807 2 16 3.11929 16 4.5V13.5C16 14.8807 14.8807 16 13.5 16H4.5C3.11929 16 2 14.8807 2 13.5V4.5C2 3.11929 3.11929 2 4.5 2H5ZM5 4V3H4.5C3.67157 3 3 3.67157 3 4.5V5H15V4.5C15 3.67157 14.3284 3 13.5 3H13V4H12V3H6V4H5ZM3 6V13.5C3 14.3284 3.67157 15 4.5 15H13.5C14.3284 15 15 14.3284 15 13.5V6H3Z" fill="#2F3744"/>' +
                                '</svg>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                    '<div class="overlay_panel" style="display: none;">' +                            
                        '<div class="nano">' +
                            '<div class="content">' +
                                '<div class="expected_date_picker"></div>' +        
                                '<div>' +
                                    '<div class="divider"></div>' +
                                '</div>' +
                                '<div>' +
                                    '<div class="expected_time_picker">' +                                    
                                        '<div class="expected_time_picker_label">' +
                                            'Pick a time' + 
                                        '</div>' +
                                        '<input type="time" class="expected_time_picker_input"></input>' +
                                    '</div>' +                                    
                                '</div>' +
                            '</div>' +                                
                        '</div>' +
                        '<div class="overlay_footer"></div>' +
                    '</div>' + 
                '</div>',

            timestamp:
                '<div class="{{?(it.message.fromBot && !it.userIsAgent) || (!it.message.fromBot && it.userIsAgent)}}left_message{{??}}right_message{{?}} timestamp" data-time="{{=it.message.date.getTime()}}" data-id="{{=it.message.id}}">{{=it.text}}</div>',
        };

        // auto doT templates
        $.each(_factoryData.templateDefinitionsv1, function (key, value) {
            _factoryData.templateInstancesv1[key] = doT.template(_factoryData.templateDefinitionsv1[key]);
        });


        $.each(_factoryData.templateDefinitionsv2, function (key, value) {
            _factoryData.templateInstancesv2[key] = doT.template(_factoryData.templateDefinitionsv2[key]);
        });
        

        _factoryData.loadGoogleMapsScript = function (callback) {
            var factoryData = BotConversationFactory.getFactoryData();

            if (factoryData.googleMapsLoadStatus.loaded) {
                callback();
                return;
            }

            factoryData.googleMapsSubscribers.push(callback);

            // only load script once
            if (factoryData.googleMapsLoadStatus.pending) {
                return;
            }
            factoryData.googleMapsLoadStatus.pending = true;

            var googleMapsApiScript = document.createElement('script');
            // todo: put this api key elsewhere
            //Social url: https://maps.googleapis.com/maps/api/js?key=AIzaSyBCHmJFeu00a3eYQN89OV3tQ-B2r5rAOmw&callback=BotConversationFactory.onGoogleMapsLoaded
            googleMapsApiScript.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyB4ke6Y3GAAkSQvB9VlARKAYcXMVWTIaGg&callback=BotConversationFactory.onGoogleMapsLoaded';

            document.head.appendChild(googleMapsApiScript);
        };

        _factoryData.loadMapQuestScript = function (callback) {
            var factoryData = BotConversationFactory.getFactoryData();

            if (factoryData.mapQuestLoadStatus.loaded) {
                callback();
                return;
            }

            factoryData.mapQuestSubscribers.push(callback);

            // only load script once
            if (factoryData.mapQuestLoadStatus.pending) {
                return;
            }
            factoryData.mapQuestLoadStatus.pending = true;

            var mapQuestApiCss = document.createElement('link');
            mapQuestApiCss.type = 'text/css';
            mapQuestApiCss.rel = 'stylesheet';
            mapQuestApiCss.href = 'https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.css';

            document.head.appendChild(mapQuestApiCss);

            var mapQuestApiScript = document.createElement('script');
            document.head.appendChild(mapQuestApiScript);
            mapQuestApiScript.onload = function () {
                // todo: put this api key elsewhere
                L.mapquest.key = 'FGOeK9RDnwRZ4mM4Nd43fJb784JnULC9';

                BotConversationFactory.onMapQuestLoaded();
            };
            mapQuestApiScript.src = 'https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.js';
        };

        _factoryData.isInitialized = true;
    }


    var _factoryData = {
        isInitialized: false,
        //templateDefinitions: {},
        templateInstancesv1: {},
        templateInstancesv2: {},
        googleMapsLoaded: false,
        googleMapsLoadStatus: {
            pending: false,
            loaded: false
        },        
        googleMapsSubscribers: [],
        mapQuestLoaded: false,
        mapQuestLoadStatus: {
            pending: false,
            loaded: false
        },
        mapQuestSubscribers: [],
    };

    return _factoryInterface;
}());



BotConversationFactory.createInstance = function (options) {
    var _interface =
    {
        destroy: function () { },
        setData: function (data) { },
        syncMessagesScrollButtons: function (msgId) { },        
        clearUnviewedMessages: function() {},
        getActiveCallStartedEvent: function(messages) {},
        onShown: function () { },        
        clearAdaLastMessages: function () { },

        setIsTest: function (isTest) { },
    };

    // ---------------

    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);

    _interface.setIsTest = function (isTest) {
        _options.isTest = isTest;
    }

    _interface.destroy = function () {
        $(_options.container).off('click');        
    };

    var _allReactions = [
        { id: 0, title: 'Love', class: 'si-fb_love', },
        { id: 1, title: 'Haha', class: 'si-fb_haha', },
        { id: 2, title: 'Wow', class: 'si-fb_wow', },
        { id: 3, title: 'Sad', class: 'si-fb_sad', },
        { id: 4, title: 'Angry', class: 'si-fb_angry', },
        { id: 5, title: 'Helpful', class: 'si-fb_like', },
        { id: 6, title: 'Not helpful', class: 'si-fb_dislike' },
        { id: 7, title: 'Question', class: 'si-fb_question' },
    ];

    var _renderedMessages = {};
    var _renderedModifications = {};
    var _conversationHandlers = [];
    var _unviewedMessages = [];
    var _currentLanguage = null;

    var _autoOpenOverlayPanelIndex = -1;

    var _expectedDate = null;
    var _expectedTime = null;

    var _ariaBusyTimeoutId = null;

    function init() {
        _options.isMobile = _options.isMobile || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

        drawConversationWidget(_options.container, true);

        attachClickHandlers();

        if (_options.showTimestamps) {
            setTimeout(updateTimestamps, 10000);
        }
    }


    function updateTimestamps() {
        var timestamps = $(options.container).find('.timestamp').get().reverse();

        $.each(timestamps, function (index, timestamp) {
            var date = new Date(parseInt($(timestamp).attr('data-time')));

            if (!getWillNiceTimestampChange(date)) {
                return false;
            }

            $(timestamp).html(getNiceTimestamp(date));
        });

        setTimeout(updateTimestamps, 10000);
    }

    function attachClickHandlers() {
        $(_options.container).unbind('click').on('click', function (e) {
            var isTimePicker = false;
            onManual(e.target, '.expected_time_picker_input', function (ths) {
                isTimePicker = true;
            });
            if (isTimePicker) {
                return true;
            }

            var isExpectedResponse = false;
            onManual(e.target, '.expected_response_date_input', function (ths) {
                isExpectedResponse = true;
            });
            onManual(e.target, '.expected_response_time_input', function (ths) {
                isExpectedResponse = true;
            });
            if (isExpectedResponse) {
                e.stopPropagation();
                return true;
            }

            onManual(e.target, '[data_analyticsType]', analyticsEvent);

            onManual(e.target, '.expected_response_submit', function (ths) {
                var response = '';

                var dateValue = $(_options.container).find('.expected_response_date_input').val();                                
                if (dateValue != null) {
                    var year = dateValue.substring(0, 4);
                    var month = dateValue.substring(5, 7);
                    var day = dateValue.substring(8, 10);

                    var dateString = month + '/' + day + '/' + year;

                    if (response.length > 0) {
                        response = response + dateString;
                    } else {
                        response = dateString;
                    }
                }

                var timeValue = $(_options.container).find('.expected_response_time_input').val();
                if (timeValue != null) {
                    var hour = timeValue.substring(0, 2);
                    var minute = timeValue.substring(3, 5);                   
                    var amPm = 'AM';

                    if (hour >= 12) {
                        if (hour > 12) {
                            hour = hour - 12;
                        }
                        amPm = 'PM';
                    }

                    if (hour == 0) {
                        hour = 12;
                    }

                    var timeString = hour + ':' + minute + ':00 ' + amPm;
                    if (response.length > 0) {
                        response = response + ' ' + timeString;
                    } else {
                        response = timeString;
                    }
                }

                if (response.length > 0) {
                    _options.callbacks.onUserMessage(response);
                }
            });

            // clicking left/right scroll buttons
            onManual(e.target, '.scroll', function (ths) {
                var levelContainer = $(ths).closest('.message');

                var direction = $(ths).attr('data-direction');

                var scroller = $(ths).closest('.horizontal_scroller');
                if (_options.messengerStyle == 'v2') {
                    scroller = $(scroller).find('.scroller');
                }

                var uWidth = getHorizontalScrollerUsedWidth(scroller);
                var aWidth = $(scroller).width();
                var xPos = $(scroller).scrollLeft();

                var scrollAmt = aWidth * 0.6;
                var scrollMs = 450;
                var scrollTo = null;

                if (direction == 'right') {
                    var scrollTo = xPos + scrollAmt;
                    var thd = 32;
                    var max = (uWidth - aWidth) + thd;
                    if (scrollTo > max) scrollTo = max;
                } else if (direction == 'left') {
                    var scrollTo = xPos - scrollAmt;
                    if (scrollTo < 0) scrollTo = 0;
                }
                scroller.animate(
                    { scrollLeft: scrollTo }, 
                    {
                        duration: scrollMs,
                        easing: 'swing',
                        step: function () {
                            syncHorizontalScrollButtons(levelContainer);
                        },
                        complete: function () {
                            syncHorizontalScrollButtons(levelContainer);
                        }
                    }
                );
            });

            onManual(e.target, '.quick_reply', function (ths) {
                var message = $(ths).closest('.message');
                var messageId = parseInt($(message).attr('data-id'));
                var quickReplyIndex = parseInt($(ths).attr('data-index'));
                var qr = _renderedMessages[messageId].content.replies[quickReplyIndex];
                if (qr.type == 'location') {
                    if (navigator.geolocation) {
                        navigator.geolocation.getCurrentPosition(function (position) {
                            _options.callbacks.onUserLocation(position.coords.latitude, position.coords.longitude);
                        });
                    } else {

                    }
                } else if (qr.type == "live_agent_request") {
                    _options.callbacks.onUserEvent({
                        type: 33 // LIVE_AGENT_REQUEST
                    });
                } else if (qr.type == "dial") {
                    // do nothing?
                } else if (qr.type == "url") {
                    window.open(qr.payload, '_blank').focus();
                    _options.callbacks.onUserPostback((qr.payload != null && qr.payload != '') ? qr.payload : qr.label, qr.label);
                    return true;
                } else {
                    _options.callbacks.onUserPostback((qr.payload != null && qr.payload != '') ? qr.payload : qr.label, qr.label);
                }

                $(ths).blur();
                // hide message buttons in UI to prevent double-clicks (this is why we don't filter this postback)
                $(message).find('.quick_reply_buttons').hide();
            });

            var submitMultiReplies = function (message) {
                var messageId = parseInt($(message).attr('data-id'));
                var activeMultiReplies = $(message).find('.multi_reply.active:not(.multi_reply_submit)');
                var multiReplies = _u.map(activeMultiReplies, function (multiReply) {
                    var index = parseInt($(multiReply).attr('data-index'));
                    return _renderedMessages[messageId].content.replies[index];
                });

                var label = _u.map(multiReplies, function (multiReply) { return multiReply.label; }).join(', ');
                var payloads = _u.map(multiReplies, function (multiReply) { return multiReply.payload; });
                _options.callbacks.onUserPostbacks(payloads, label);
            };

            onManual(e.target, '.show_more_link', function (ths) {
                var overlayPanel = $(ths).closest('.message').find('.overlay_panel');
                openOverlayPanel(overlayPanel, function () { }, false, [ 'Close' ]);
            });

            onManual(e.target, '.quick_reply_show_more', function (ths) {
                var message = $(ths).closest('.message')
                message.find('.quick_reply_article').hide();
                message.find('.quick_reply_container').show();
            });

            onManual(e.target, '.multi_reply_buttons .open_overlay_panel_button', function (ths) {
                var message = (ths).closest('.message');

                var onSubmit = function () {
                    submitMultiReplies(message);
                };

                var overlayPanel = $(ths).closest('.multi_reply_buttons').find('.overlay_panel');
                var submitButtonText = $(overlayPanel).find('.multi_reply_submit')[0].innerHTML;
                var cancelButtonText = $(overlayPanel).find('.multi_reply_cancel')[0].innerHTML;

                if (cancelButtonText == '' || submitButtonText == '') {
                    openOverlayPanel(overlayPanel, onSubmit)
                } else {
                    openOverlayPanel(overlayPanel, onSubmit, false, [cancelButtonText, submitButtonText]);
                }
            });

            onManual(e.target, '.expected_response .open_overlay_panel_button', function (ths) {
                var onSubmit = function () {
                    var response = '';

                    var dateValue = _expectedDate;
                    if (dateValue != null) {
                        var year = dateValue.getFullYear();
                        var month = dateValue.getMonth() + 1;
                        var day = dateValue.getDate();

                        var dateString = month.toString().padStart(2, '0') + '/' + day.toString().padStart(2, '0') + '/' + year.toString();

                        if (response.length > 0) {
                            response = response + dateString;
                        } else {
                            response = dateString;
                        }
                    }
                    _expectedDate = null;

                    var timeValue = _expectedTime;
                    if (timeValue != null) {
                        var hour = timeValue.substring(0, 2);
                        var minute = timeValue.substring(3, 5);
                        var amPm = 'AM';

                        if (hour >= 12) {
                            if (hour > 12) {
                                hour = hour - 12;
                            }
                            amPm = 'PM';
                        }

                        if (hour == 0) {
                            hour = 12;
                        }

                        var timeString = hour + ':' + minute + ':00 ' + amPm;
                        if (response.length > 0) {
                            response = response + ' ' + timeString;
                        } else {
                            response = timeString;
                        }
                    }
                    _expectedTime = null;

                    if (response.length > 0) {
                        _options.callbacks.onUserMessage(response);
                    }                    
                };

                var overlayPanel = $(ths).closest('.expected_response').find('.overlay_panel');
                openOverlayPanel(overlayPanel, onSubmit, true);
            });

            onManual(e.target, '.multi_reply', function (ths) {
                $(ths).toggleClass('active');

                if ($(ths).hasClass('active')) {
                    $(ths).find('.multi_reply_sr').addClass('srOnly');
                    $(ths).find('.multi_reply_sr').show();
                } else {
                    $(ths).find('.multi_reply_sr').removeClass('srOnly');
                    $(ths).find('.multi_reply_sr').hide();
                }

                var message = $(ths).closest('.message');
                var multiReplySubmit = $(message).find('.multi_reply_submit');

                var activeMultiReplies = $(message).find('.multi_reply.active:not(.multi_reply_submit)');
                if (activeMultiReplies.length > 0) {
                    if (!$(multiReplySubmit).hasClass('active')) {
                        $(multiReplySubmit).addClass('active');                        
                    }
                    _options.callbacks.onOverlayPanelCanSubmitChanged(true);
                } else {
                    if ($(multiReplySubmit).hasClass('active')) {
                        $(multiReplySubmit).removeClass('active');                        
                    }
                    _options.callbacks.onOverlayPanelCanSubmitChanged(false);
                }
            });

            onManual(e.target, '.multi_reply_submit.active', function (ths) {
                var message = $(ths).closest('.message');
                submitMultiReplies(message);
            });

            onManual(e.target, '.card a[data-type="post_back"]', function (ths) {
                var message = $(ths).closest('.message');
                var messageId = parseInt($(message).attr('data-id'));

                var card = $(ths).closest('.card');
                var buttonIndex = parseInt($(ths).attr('data-index'));

                var isCard = (_renderedMessages[messageId].contentType == 1);
                var isCarousel = !isCard;

                if (isCard) {
                    filterUserPostback(
                        _renderedMessages[messageId].content.buttons[buttonIndex].payload,
                        _renderedMessages[messageId].content.buttons[buttonIndex].text
                    );
                }

                if (isCarousel) {
                    var cardIndex = parseInt($(card).attr('data-index'));
                    filterUserPostback(
                        _renderedMessages[messageId].content.elements[cardIndex].buttons[buttonIndex].payload,
                        _renderedMessages[messageId].content.elements[cardIndex].buttons[buttonIndex].text
                    );
                }
            });

            onManual(e.target, '.card a[data-type="live_agent_request"]', function (ths) {
                _options.callbacks.onUserEvent({
                    type: 33 // LIVE_AGENT_REQUEST
                });
            });

            onManual(e.target, '.text_message_audio', function (ths) {
                var message = $(ths).closest('.message');
                var messageId = parseInt($(message).attr('data-id'));

                var audioUrl = _renderedMessages[messageId].content.audioUrl;
                if (audioUrl != null) {
                    var a = new Audio(audioUrl);
                    a.play();
                }
            });


            onManual(e.target, '.annotation .drilldown', function (ths) {
                var id = parseInt($(ths).attr('data-id'));
                var modification = _renderedModifications[id];
                drilldownModification(modification);
                return false;
            });



            var isLink = $(e.target).closest('a').length > 0;
            if (isLink) {
                e.stopPropagation();
                return true;
            }

            return false;
        }).on('keyup', function(e){
            if (e.keyCode == 13 || e.keyCode == 32) {
                e.stopPropagation();
                $(e.target).click();
            }
        });
    }

    function openOverlayPanel(overlayPanel, onSubmit, canSubmit, buttonLabels) {
        // try to remove the focus from the input so the keyboard closes before the overlay is opened
        // and wait a little bit
        $(_options.container).find('.open_overlay_panel_button:last-of-type').focus();
        setTimeout(function () {
            var onCancel = function () {
                closeOverlayPanel(overlayPanel);
            };

            var onSubmitted = function () {
                onSubmit();
                closeOverlayPanel(overlayPanel);
            };
            
            var onLoaded = function () {
                var overlayHeight = $(_options.container).parent().height();                
                var overlayFooterHeight = $(_options.container).find('.overlay_footer').height();

                $(overlayPanel).height(overlayHeight);
                $(overlayPanel).find('.nano').height(overlayHeight - overlayFooterHeight);
                $(overlayPanel).find('.content').height(overlayHeight - overlayFooterHeight);

                $(overlayPanel).find('.nano').nanoScroller({ scroll: 'top' });

                $(overlayPanel).find('.overlay_label').focus();
            };

            var images = $(overlayPanel).find('img');
            if ($(images).length > 0) {
                var numLoadedImages = 0;
                $(images).on('load', function () {
                    numLoadedImages = numLoadedImages + 1;

                    if (numLoadedImages == $(images).length) {
                        setTimeout(function () {
                            onLoaded();
                        }, 1);
                    }
                }).each(function () {
                    if (this.complete) {
                        $(this).trigger('load');
                    }
                });

                overlayPanel.show();
            } else {
                overlayPanel.show();
                setTimeout(function () {
                    onLoaded();
                }, 1);
            }

            // hack to hide parent nanoscroller
            $(overlayPanel).parent().closest('.nano').find('> .pane').toggleClass('hidden', true);

            _options.callbacks.onOverlayPanelOpen(onCancel, onSubmitted, buttonLabels);

            if (canSubmit == null) {
                canSubmit = false;
            }

            _options.callbacks.onOverlayPanelCanSubmitChanged(canSubmit);
        }, 1000);
    }

    function closeOverlayPanel(overlayPanel) {
        // undo all the changes
        $(overlayPanel).find('.multi_reply').toggleClass('active', false);

        overlayPanel.hide();

        $(overlayPanel).parent().closest('.nano').find('> .pane').toggleClass('hidden', false);
    }

    function onManual(target, selector, matchFn) {
        var ancestorsMatch = $(target).closest(selector);
        if (ancestorsMatch.length > 0) {
            matchFn(ancestorsMatch[0]);
            return true;
        }
        return false;
    }

    var _postbackFilter = {
        lastSendTime: null,
        sendTimeThdMs: 1000
    };

    function filterUserPostback(payload, text) {
        var now = new Date();

        var lastPostbackWasRecent = (_postbackFilter.lastSendTime != null && (now - _postbackFilter.lastSendTime) < _postbackFilter.sendTimeThdMs);

        if (lastPostbackWasRecent) return;        

        _options.callbacks.onUserPostback(
            payload,
            text
        );

        _postbackFilter.lastSendTime = now;
    }

    _interface.onShown = function() {
        _unviewedMessages = [];
        _options.callbacks.onUnviewedMessagesChanged(_unviewedMessages);
    };

    _interface.syncMessagesScrollButtons = function (msgId) {
        syncHorizontalScrollButtons($(_options.container).find('.message[data-id="' + msgId + '"]'));
    };


    _interface.clearUnviewedMessages = function() {
        _unviewedMessages = [];
        _options.callbacks.onUnviewedMessagesChanged(_unviewedMessages);
    };

    _interface.clearAdaLastMessages = function () {
        $(_options.container).find('.adaLastMessages')[0].ariaBusy = "true";
        $(_options.container).find('.adaLastMessages').html('');
    };

    _interface.getActiveCallStartedEvent = function(messages) {        
        var callStartedEvent = null;
        if (typeof messages == 'undefined') return callStartedEvent;

        for (var i = messages.length - 1; i >= 0; i--) {
            var message = messages[i];                        
            var isCallStartedEvent = (message.contentType == 10) && (message.content.type == 22);
            var isCallEndedEvent = (message.contentType == 10) && (message.content.type == 25);
            var isCallDeclinedEvent = (message.contentType == 10) && (message.content.type == 26);
            var isAgentDisconnectedEvent = (message.contentType == 10) && (message.content.type == 9);

            // if a call ended event happened after the most recent call started event, then there should not be an active call because
            // there is only one active call per conversation hopefully
            if (isCallEndedEvent) break;
            
            // same applies for call declined:
            if (isCallDeclinedEvent) break;

            // and "agent disconnected":
            if (isAgentDisconnectedEvent) break;

            if (isCallStartedEvent) {
                callStartedEvent = message;
                break;
            }

        }
        return callStartedEvent;
    };


    _interface.setData = function (data) {
        // this is a temporary hack to increase performance until we can get some kind of paging
        if (data.messages.length > 500) {
            data.messages = _u.last(data.messages, 500);

            if (data.messages.length > 0) {
                data.modifications = _u.filter(data.modifications, function (modification) { return modification.appliedDate > data.messages[0].date; });
            }
        }

        var visibleMessages = false;

        // assign unique negative indexes to transient messages
        var negativeIndex = -1;
        $.each(data.messages, function (index, message) {
            if (message.id < 0) {
                message.id = negativeIndex;
                negativeIndex = negativeIndex - 1;
            }
        });

        var conversationMessages = data.messages;
        
        if (!_options.drawEvents) {
            conversationMessages = _u.reject(conversationMessages, function (message) { return message.contentType == 10 && message.content.type != 13; });             
        }

        var isOtherUserMessage = function (message) {
            return (_options.userIsAgent && !message.fromBot) || (!_options.userIsAgent && message.fromBot);
        };

        var typingIndicator = _u.chain(conversationMessages)
            .filter(function (message) {
                if (message.contentType == 10 && message.content.type == 13 && isOtherUserMessage(message)) {
                    return true
                }

                return false;
            })
            .sortBy(function (message) { return message.date; })
            .last()
            .value();
        if (typingIndicator != null) {
            // event context alreayd parsed
            var typingIndicatorContext = typingIndicator.content.context;// JSON.parse(typingIndicator.content.context);
            if (!typingIndicatorContext.on) {
                typingIndicator = null;
            }
        }

        conversationMessages = _u.reject(conversationMessages, function (message) { return message.contentType == 10 && message.content.type == 13; });             

        var lastRealOtherUserMessageDate = _u.max(_u.pluck(_u.filter(conversationMessages, function (message) { return isOtherUserMessage(message); }), 'date'));

        if (typingIndicator != null && ((lastRealOtherUserMessageDate == Number.NEGATIVE_INFINITY) || (typingIndicator.date.getTime() > lastRealOtherUserMessageDate.getTime()))) {
            // clone it so the real message doesn't get modified
            typingIndicator = JSON.parse(JSON.stringify(typingIndicator));

            // fake these values to push message down
            typingIndicator.id = Number.MAX_SAFE_INTEGER;
            typingIndicator.date = new Date();

            conversationMessages.push(typingIndicator);
        }

        // need a date to properly place in ui, sort by id first, then later we will insert at the proper timestamp position.  
        // this ensures that the later events are inserted later as long as timestamps are the same
        conversationMessages = _u.sortBy(conversationMessages, function (message1) { return message1.id; });
                
        if (_options.showTypingIndicator && conversationMessages.length > 0) {
            var fakeId = Number.MAX_SAFE_INTEGER;

            var lastMessage = conversationMessages[conversationMessages.length - 1];
            if (!lastMessage.fromBot && !_options.userIsAgent) {
                conversationMessages.push({
                    id: fakeId,
                    contentType: 10, // event
                    content: {
                        type: 13, // SET_TYPING
                        context: {
                            on: true
                        }
                    },
                    fromBot: true,
                    date: new Date(),
                    context: {
                        allowableReactions: {
                            reactionTypes: []
                        }
                    }
                });
            } else {
                $(_options.container).remove('.message[data-id="' + fakeId + '"]');
            }
        }

        // --------------
        // add/remove messages as needed
        detectLanguageChange(conversationMessages).then(function() {
            var newlyAddedMessages = syncMessageDrawing(conversationMessages);

            if (newlyAddedMessages.length > 0) {
                detectLanguageChange(newlyAddedMessages);
            }
            
            // store "unviewed messages" so clients can display e.g. notification icons depending on what messages are not viewed
            var isCurrentlyVisible = $(_options.container).find('.messages').is(':visible');
            if (!isCurrentlyVisible) {
                _unviewedMessages = _unviewedMessages.concat(newlyAddedMessages);            
            } else {
                _unviewedMessages = [];
            }
            _options.callbacks.onUnviewedMessagesChanged(_unviewedMessages);        

            if (typeof data.conversationHandlers != 'undefined') {
                _conversationHandlers = data.conversationHandlers;
            }

            syncModificationDrawing(data, conversationMessages);

            // hide quick reply buttons if they are NOT the most recent message in convo
            if (conversationMessages.length > 0) {
                var lastQuickReply = _u.chain(conversationMessages).filter(function (message) { return message.contentType == 2; }).last().value();
                if (lastQuickReply != null) {
                    var messagesAfterLastQuickReply = _u.filter(conversationMessages, function (message) { return message.id > lastQuickReply.id; });
                    if (_u.some(messagesAfterLastQuickReply, function (message) { return message.contentType != 12; }) || _options.userIsAgent) {
                        // some are not reactions
                        $(_options.container).find('.quick_reply_buttons').hide();                    
                    } else {
                        // no messages after quick reply are not reactions, show the latest quick reply buttons
                        $(_options.container).find('.quick_reply_buttons:not([data-id="' + lastQuickReply.id + '"])').hide();
                        $(_options.container).find('.quick_reply_buttons[data-id="' + lastQuickReply.id + '"]').show();                    
                    }
                }
            }

            // hide multi reply buttons if they are NOT the most recent message in convo
            if (conversationMessages.length > 0) {
                var lastMultiReply = _u.chain(conversationMessages).filter(function (message) { return message.contentType == 13; }).last().value();
                if (lastMultiReply != null) {
                    var messagesAfterLastMultiReply = _u.filter(conversationMessages, function (message) { return message.id > lastMultiReply.id; });
                    if (_u.some(messagesAfterLastMultiReply, function (message) { return message.contentType != 12; }) || _options.userIsAgent) {
                        // some are not reactions
                        $(_options.container).find('.multi_reply_buttons').hide();
                        $(_options.container).find('.multi_reply_submit_container').hide();
                    } else {
                        // no messages after multi reply are not reactions, show the latest multi reply buttons
                        $(_options.container).find('.multi_reply_buttons:not([data-id="' + lastMultiReply.id + '"])').hide();
                        $(_options.container).find('.multi_reply_buttons[data-id="' + lastMultiReply.id + '"]').show();

                        // only show if v2 and the last multi reply has not already automatically shown itself
                        if (_options.messengerStyle == 'v2' && lastMultiReply.id > _autoOpenOverlayPanelIndex) {
                            $(_options.container).find('.multi_reply_buttons[data-id="' + lastMultiReply.id + '"] .open_overlay_panel_button').click();
                            _autoOpenOverlayPanelIndex = lastMultiReply.id;
                        }                    
                    }
                }
            }

            // handle expected response custom UI
            if (!_options.userIsAgent) {
                var hasExpectedResponse = false;
                if (conversationMessages.length > 0) {
                    var lastMessage = _u.last(conversationMessages);
                    if (lastMessage.context != null &&
                        lastMessage.context.expectedResponse != null) {
                        hasExpectedResponse = true;
                        var otherExpectedResponse = $(_options.container).find('.expected_response');
                        if (otherExpectedResponse.length > 0) {
                            // can't do anything
                            return;
                        }

                        var uiMessage = $(_options.container).find('.message[data-id="' + lastMessage.id + '"]');

                        if (uiMessage.length < 1) {
                            // can't do anything
                            return;
                        }

                        var submitText = lastMessage.context.expectedResponse.submitText || 'Submit';

                        if (_options.messengerStyle == 'v2') {
                            switch (lastMessage.context.expectedResponse.type) {
                                case "Date":
                                    _expectedDate = new Date();

                                    uiMessage.after(_factoryData['templateInstances' + _options.messengerStyle].dateInput({}));

                                    var container = $(_options.container).find('.expected_date_picker');
                                    SimpleCalendarWidgetFactory.createInstance({
                                        container: container,
                                        callbacks: {
                                            onDateSelected: function (date) {
                                                _expectedDate = date;
                                            }
                                        }
                                    });                                                            
                                    break;
                                case "DateTime":
                                    _expectedDate = new Date();
                                    _expectedTime = (_expectedDate.getHours() + '').padStart(2, '0') + ':' + (_expectedDate.getMinutes() + '').padStart(2, '0');

                                    uiMessage.after(_factoryData['templateInstances' + _options.messengerStyle].dateTimeInput({}));

                                    var container = $(_options.container).find('.expected_date_picker');
                                    SimpleCalendarWidgetFactory.createInstance({
                                        container: container,
                                        callbacks: {
                                            onDateSelected: function (date) {
                                                _expectedDate = date;
                                            }
                                        }
                                    });

                                    if (!_options.isMobile) {
                                        $(_options.container).find('.expected_time_picker_input').toggleClass('desktop', true);
                                    }

                                    $(_options.container).find('.expected_time_picker_input').val(_expectedTime);
                                    $(_options.container).find('.expected_time_picker_input').on('change', function () {
                                        _expectedTime = $(this).val();
                                    });                                                                                                                                                          
                                    break;
                                default:
                                    break;
                            }   
                        } else {
                            switch (lastMessage.context.expectedResponse.type) {
                                case "Date":
                                    uiMessage.after(_factoryData['templateInstances' + _options.messengerStyle].dateInput({ submitText: submitText }));
                                    break;
                                case "DateTime":
                                    uiMessage.after(_factoryData['templateInstances' + _options.messengerStyle].dateTimeInput({ submitText: submitText }));
                                    break;
                                default:
                                    break;
                            }

                            var dateInput = $(_options.container).find('.expected_response_date_input');
                            if (dateInput != null) {
                                var year = new Date().getFullYear();
                                var month = ((new Date().getMonth() + 1) + '').padStart(2, '0');
                                var day = (new Date().getDate() + '').padStart(2, '0');
                                var dateVal = year + '-' + month + '-' + day;

                                dateInput.val(dateVal);
                            }

                            var timeInput = $(_options.container).find('.expected_response_time_input');
                            if (timeInput != null) {
                                var hour = (new Date().getHours() + '').padStart(2, '0');
                                var minute = (new Date().getMinutes() + '').padStart(2, '0');
                                var timeVal = hour + ':' + minute;

                                timeInput.val(timeVal);
                            }
                        }                                     
                    }
                }
                if (!hasExpectedResponse) {
                    $(_options.container).find('.expected_response').remove();
                }
            }

            //this is a hack to push the right scroller to the left where it should be when there are reactions
            if ($(_options.container).closest('.v1').length > 0) {
                var quickRepliesRightScroller = $(_options.container).find('.messages .horizontal_scroller .scroll[data-direction="right"]');
                $.each(quickRepliesRightScroller, function (index, quickReply) {
                    var reactionsWidth = 2;
                    var reactions = $(this).closest('.message').find('.embedded_reactions_container,.popup_reactions_container,.message_reaction');
                    $.each(reactions, function (index, reaction) {
                        if ($(reaction).css('display') != 'none') {
                            var marginLeft = parseInt($(reaction).css('margin-left').replace('px', ''));
                            var width = $(reaction).width();
                            var marginRight = parseInt($(reaction).css('margin-right').replace('px', ''));
                            reactionsWidth += marginLeft + width + marginRight;
                        }
                    });

                    if (reactionsWidth > 0) {
                        $(quickRepliesRightScroller).css('right', reactionsWidth + 'px');
                    }
                });
            }        
            
            _options.callbacks.onDrawn(newlyAddedMessages.length > 0);

            checkIfImagesLoaded();
        });
    };



    // todo: add grouped drawing by "appendAfter", like modifications do
    // - make sure messages are sorted by date
    // - group by "append after"
    // - seaparate "draw" logic from "update" logic (upadte = horizontal scrollbars, reactions), make sure "update" logic still works correctly
    function syncMessageDrawing(conversationMessages) {
        //var t1 = new Date();

        // check if new messages are a completely different set of messages from what is rendered.  this happens e.g. in agent worksapce when user selcts between conversations
        // if so, delete all old messages in a more efficient manner than the incredibly slow technique below
        {
            var allRenderedAreOld = true;
            for (var i = 0; i < conversationMessages.length; i++) {                
                var existsInUi = (typeof _renderedMessages[conversationMessages[i].id] != 'undefined');
                if (existsInUi) {
                    allRenderedAreOld = false;
                    break;
                }
            }
            if (allRenderedAreOld) {
                // clear all messages in one step
                drawConversationWidget(_options.container, false);
                _renderedMessages = {};
            }            
        }

        // check for messages that are not rendered yet, add them (in proper location)
        var newlyAddedMessages = [];
        var lastMessageHtml = null;
        var currentMessageIds = [];
        var style = '';
        $.each(conversationMessages, function (index, message) {
            currentMessageIds.push(message.id);
            var existsInUi = (typeof _renderedMessages[message.id] != 'undefined');
            if (!existsInUi) {                             
                var containerTemplateFn = null, nodeTemplateFn = null;
                switch (message.contentType) {
                    case 0:
                        if (message.content != null && message.content.speech != null) {
                            message.content.speech = message.content.speech.replace(/<\/a>/gi, '<span class="srOnly">Link opens in new window </span></a>');
                        }

                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].text;
                        nodeTemplateFn = null;
                        break;

                    case 1: // card
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].card;
                        nodeTemplateFn = null;
                        break;

                    case 2: // quick replies
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].quickRepliesContainer;
                        nodeTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].quickReply;
                        break;

                    case 3: // image
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].image;
                        nodeTemplateFn = null;
                        break;

                    case 4: // custom payload
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].customPayload;
                        nodeTemplateFn = null;
                        break;

                    case 5: // carousel
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].carousel;
                        nodeTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].card;
                        if (message.content.elements.length > 3) {
                            style = 'small_cards';
                        } else if (message.content.elements.length > 2) {
                            style = 'medium_cards';
                        } else {
                            style = 'large_cards';
                        }
                        break;

                    case 8: // location
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].location;
                        nodeTemplateFn = null;
                        break;

                    case 9: // audio
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].audio;
                        nodeTemplateFn = null;
                        break;

                    case 10: // event
                        if (message.content.type == 13) { // typing indicator
                            containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].typingIndicator
                        } else {
                            newlyAddedMessages.push(message);
                            containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].event;
                            _options.callbacks.analyticsEvent('ResponseEvent',{type: message.content.type,})
                        }
                        nodeTemplateFn = null;
                        break;

                    case 11: // locations
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].locations;
                        nodeTemplateFn = null;
                        break;

                    case 12: // reaction
                        // modify UI here                        
                        var reaction = _u.find(_allReactions, function (reaction) { return reaction.id == message.content.reactionType; });
                        var uiMessage = $(_options.container).find('.message[data-id="' + message.content.toMessageId + '"]');

                        if (uiMessage.length < 1) {
                            // can't do anything
                            return;
                        }

                        if (_options.userIsAgent) {
                            var messageReaction = $(uiMessage).find('.message_reaction');
                            var classList = $(messageReaction).attr('class').split(/\s+/);
                            $.each(classList, function (index, item) {
                                if (item.startsWith('si-')) {
                                    $(messageReaction).removeClass(item);
                                    if (reaction == null) {
                                        $(messageReaction).addClass('si-pos');
                                        $(messageReaction).hide();
                                    } else {
                                        $(messageReaction).addClass(reaction.class);
                                        $(messageReaction).show();
                                    }
                                }
                            });
                        } else {
                            var embeddedReactionsContainer = $(uiMessage).find('.embedded_reactions_container');
                            if (embeddedReactionsContainer.length > 0) {
                                var reactionOptions = $(embeddedReactionsContainer).find('.reaction_option');
                                $.each(reactionOptions, function (index, reactionOption) {
                                    var idStr = $(reactionOption).attr('data-id');
                                    var id = parseInt(idStr);

                                    if (reaction != null && id == reaction.id) {
                                        $(reactionOption).addClass('selected');
                                        $(reactionOption).attr('aria-checked', true);
                                    } else {
                                        $(reactionOption).removeClass('selected');
                                        $(reactionOption).attr('aria-checked', false);
                                    }
                                });
                            }

                            var popupReactionsContainer = $(uiMessage).find('.popup_reactions_container');
                            if (popupReactionsContainer.length > 0) {
                                if (reaction == null) {
                                    $(popupReactionsContainer).removeClass('always_visible');
                                } else {
                                    $(popupReactionsContainer).addClass('always_visible');
                                }

                                var reactionsMenuButton = $(popupReactionsContainer).find('.reactions_menu_button');
                                var classList = $(reactionsMenuButton).attr('class').split(/\s+/);
                                $.each(classList, function (index, item) {
                                    if (item.startsWith('si-')) {
                                        $(reactionsMenuButton).removeClass(item);
                                        if (reaction == null) {
                                            $(reactionsMenuButton).addClass('si-pos');
                                        } else {
                                            $(reactionsMenuButton).addClass(reaction.class);
                                        }
                                    }
                                });

                                var reactionOptions = $(popupReactionsContainer).find('.reaction_option');
                                $.each(reactionOptions, function (index, reactionOption) {
                                    var idStr = $(reactionOption).attr('data-id');
                                    var id = parseInt(idStr);

                                    if (reaction != null && id == reaction.id) {
                                        $(reactionOption).addClass('selected');
                                        $(reactionOption).attr('aria-checked', true);
                                    } else {
                                        $(reactionOption).removeClass('selected');
                                        $(reactionOption).attr('aria-checked', true);
                                    }
                                });
                            }
                        }

                        _renderedMessages[message.id] = message;
                        return; // don't need to do any of the below stuff

                    case 13: // multi replies
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].multiRepliesContainer;
                        nodeTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].multiReply;
                        break;

                    case 14:
                        newlyAddedMessages.push(message);
                        if (message.content.files != null) {
                            var imageMimeTypes = [
                                'image/png',
                                'image/gif',
                                'image/jpeg'
                            ];

                            $.each(message.content.files, function (index, file) {
                                if (file.mimeType != null) {
                                    if (imageMimeTypes.includes(file.mimeType)) {
                                        file.isImage = true;
                                    }
                                }
                            });

                            message.content.files = _u.sortBy(message.content.files, function (file) { return !file.isImage; });
                        }
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].file;
                        nodeTemplateFn = null;
                        break;

                    /*
                    case 15: // notification request
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].notificationRequest;
                        nodeTemplateFn = null;
                        break;
                    */
                    case 16: // notification response
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].notificationResponse;
                        nodeTemplateFn = null;
                        break;
                    case 17: // notification response
                        newlyAddedMessages.push(message);
                        containerTemplateFn = _factoryData['templateInstances' + _options.messengerStyle].notificationResponse;
                        nodeTemplateFn = null;
                        break;
                }

                var allowedReactions = [];
                if (!_options.userIsAgent) {
                    $.each(_allReactions, function (index, reaction) {
                        if (message.context.allowableReactions.reactionTypes.indexOf(reaction.id) != -1) {
                            allowedReactions.push(reaction);
                        }
                    });
                }

                var html = _factoryData['templateInstances' + _options.messengerStyle].message({
                    isMobile: _options.isMobile,
                    userIsAgent: _options.userIsAgent,
                    message: message,
                    content: message.content,
                    allowedReactions: allowedReactions,
                    allowedReactionsDisplayStyleType: message.context.allowableReactions.displayStyleType,
                    containerTemplateFn: containerTemplateFn,
                    embeddedReactionsTemplateFn: _factoryData['templateInstances' + _options.messengerStyle].embeddedReactions,
                    popupReactionsTemplateFn: _factoryData['templateInstances' + _options.messengerStyle].popupReactions,
                    nodeTemplateFn: nodeTemplateFn,
                    toHtmlFn: toHtml,
                    isHighlighted: _options.highlightMessageId == message.id,
                    includeSrOnly: containerTemplateFn == _factoryData['templateInstances' + _options.messengerStyle].card,
                    style: style
                });
                var previousMessage = getMessageAppendLocation(message.id, message.date.getTime());
                var appendAfter = previousMessage;

                if (_options.showTimestamps) {
                    var next = $(appendAfter).next();
                    if (next.length > 0 && $(next).hasClass('timestamp')) {
                        appendAfter = next;
                    }
                }

                // add to UI
                $(appendAfter).after(html);    

                var uiMessage = $(_options.container).find('.message[data-id="' + message.id + '"]');

                if (_options.showTimestamps) {
                    if ($(previousMessage).length > 0 &&
                        $(previousMessage).hasClass('message') &&
                        ($(previousMessage).hasClass('left_message') && $(uiMessage).hasClass('left_message')) || ($(previousMessage).hasClass('right_message') && $(uiMessage).hasClass('right_message'))) {
                        next = $(previousMessage).next();
                        if (next.length > 0 && $(next).hasClass('timestamp')) {
                            $(next).remove();
                        }
                    }
                }

                if ((message.fromBot && !_options.userIsAgent)){
                    _options.callbacks.analyticsEvent("BotMessageReceived");
                    $(uiMessage).find('a').each(function(index,item){
                        if(!item.hasAttribute('data_analyticsType')){
                            item.setAttribute('data_analyticsType', 'BotMessageEmbeddedLinkClicked')
                        }
                    });
                }
                else if ((!message.fromBot && _options.userIsAgent)){
                    _options.callbacks.analyticsEvent("AgentMessageReceived");
                    $(uiMessage).find('a').each(function(index,item){
                        if(!item.hasAttribute('data_analyticsType')){
                            item.setAttribute('data_analyticsType', 'AgentMessageEmbeddedLinkClicked')
                        }
                    });
                }
                                                
                if (message.contentType == 8) { // location
                    if (message.content.provider == 1) { // mapquest
                        drawMapQuestMap(uiMessage, [message.content]);
                    }
                    else {
                        drawGoogleMap(uiMessage, [message.content]);
                    }
                }
                if (message.contentType == 11) { // locations
                    if (message.content.provider == 1) { // mapquest
                        drawMapQuestMap(uiMessage, message.content.locations);
                    } else {
                        drawGoogleMap(uiMessage, message.content.locations);
                    }
                }

                var popupReactionsContainer = $(uiMessage).find('.popup_reactions_container');
                $(uiMessage).find('.reactions_menu_button').click(function () {                    
                    var buttonRect = $(this)[0].getBoundingClientRect();
                    var containerRect = $(_options.container)[0].getBoundingClientRect();
                    
                    // make menu pull in proper left/right direction if it is close to right edge of container
                    var distanceToLeftEdge = buttonRect.x - containerRect.x;
                    var distanceToRightEdge = (containerRect.x + containerRect.width) - buttonRect.x;
                    var pullMenuToRight = (distanceToRightEdge < distanceToLeftEdge);
                    $(this).parent().find('.dropdown-menu').toggleClass('pull-right', pullMenuToRight);

                    // make menu pull in proper up/down direction if it is close to top of container (favor dropping up)
                    var distanceToTopEdge = buttonRect.y - containerRect.y;
                    var distanceToBottomEdge = (containerRect.y + containerRect.height) - buttonRect.y;
                    var dropMenuUp = (distanceToTopEdge > 40);
                    $(this).parent()
                        .toggleClass('dropup', dropMenuUp)
                        .toggleClass('caret_up', dropMenuUp)
                        .toggleClass('dropdown', !dropMenuUp)
                        .toggleClass('caret_down', !dropMenuUp);

                    $(this).parent().find('.dropdown-menu').css('margin-top', dropMenuUp ? '-6px' : '-2px');

                    $(popupReactionsContainer).toggleClass('open');
                });

                $(uiMessage).find('.reaction_option').click(function () {
                    var idStr = $(this).attr('data-id');
                    var id = parseInt(idStr);
                    var alreadySelected = $(this).hasClass('selected');
                    if (alreadySelected) {
                        id = -1;
                    }

                    _options.callbacks.onMessageReaction(message.id, id);

                    $(popupReactionsContainer).removeClass('open');
                });

                syncHorizontalScrollButtons(uiMessage);

                lastMessageHtml = html;
                _renderedMessages[message.id] = message;       

                if (_options.showTimestamps) {
                    var timestampHtml = _factoryData['templateInstances' + _options.messengerStyle].timestamp({ message: message, text: getNiceTimestamp(message.date), userIsAgent: _options.userIsAgent })
                    $(uiMessage).after(timestampHtml);
                }
            }
        });       

        // update ada last message if needed
        if (lastMessageHtml != null) {            
            if (_ariaBusyTimeoutId != null) {
                clearTimeout(_ariaBusyTimeoutId);
            }
            $(_options.container).find('.adaLastMessages')[0].ariaBusy = "true";
            var html = lastMessageHtml;
            html = html.replace(/tabindex="0"/g, '');
            lastMessageHtml = html;
            var htmlDom = $.parseHTML(html);
            $(htmlDom).find('.no_live').remove();
            html = $(htmlDom).html();     
            $(_options.container).find('.adaLastMessages').append(html);        
            _ariaBusyTimeoutId = setTimeout(function ()
            {
                $(_options.container).find('.adaLastMessages')[0].ariaBusy = "false";
                _ariaBusyTimeoutId = null;
                $(_options.container).find('.adaLastMessages').append('<div class="srOnly"> </div>');  
                _options.callbacks.onAdaLastMessagesUpdated();
            }, 3000);
        }
        
        // check for messages that are rendered but no longer exist, delete them
        var renderedMessageIds = [];
        $.each(_renderedMessages, function (messageId, message) {
            renderedMessageIds.push(message.id);
        });
        $.each(renderedMessageIds, function (index, renderedMessageId) {
            var existsInData = (currentMessageIds.indexOf(renderedMessageId) >= 0);
            if (!existsInData) {
                // remove from ui
                $(options.container).find('.message[data-id="' + renderedMessageId + '"]').remove();
                $(options.container).find('.timestamp[data-id="' + renderedMessageId + '"]').remove();
                delete _renderedMessages[renderedMessageId];
            }
        });

        //var t2 = new Date();
        //console.log('messages took ' + (t2.getTime() - t1.getTime()) + ' - ' + conversationMessages.length);

        return newlyAddedMessages;
    }

    function detectLanguageChange(messages) {
        var deferred = $.Deferred();
        var index = 0;
        function processNext() {
            if (index >= messages.length) {
                deferred.resolve();
                return;
            }
            var message = messages[index];
            index++;
            if (message.context && message.context.messageLanguage) {
                var newMessageLanguage = message.context.messageLanguage;
                var loadPromise = null;
                if (_currentLanguage !== null) {
                    var baseNewLanguage = newMessageLanguage.split('-')[0].toLowerCase();
                    var baseCurrentLanguage = _currentLanguage.split('-')[0].toLowerCase();
                    if (baseCurrentLanguage !== baseNewLanguage) {
                        var langToLoad = null;
                        var supportedLangs = ['es', 'fr', 'it', 'nl', 'de'];
                        var newBaseLang = newMessageLanguage.split('-')[0].toLowerCase();
                        if (supportedLangs.indexOf(newBaseLang) > -1) {
                            langToLoad = newBaseLang;
                        } else {
                            langToLoad = 'en-US';
                        }
                        if (langToLoad) {
                            var scriptUrl = '/i18n/all.' + langToLoad + '.js';
                            loadPromise = $.getScript(scriptUrl).done(function () {
                                if (typeof localize === 'function') {
                                    localize(newMessageLanguage);
                                }
                            });
                        }
                    }
                }
                _currentLanguage = newMessageLanguage;
                if (loadPromise) {
                    loadPromise.always(processNext);
                } else {
                    processNext();
                }
            } else {
                processNext();
            }
        }
        processNext();
        return deferred.promise();
    }

    function getWillNiceTimestampChange(date) {
        var then = date;
        var now = new Date();

        var yearsAgo = now.getFullYear() - then.getFullYear();
        if (yearsAgo > 0) {
            return false;
        }

        var monthsAgo = (now.getMonth() + 1) - (then.getMonth() + 1);
        if (monthsAgo > 0) {
            return false;
        }

        var daysAgo = now.getDate() - then.getDate();
        if (daysAgo > 0) {
            return false;
        }

        return true;
    }

    function getNiceTimestamp(date) {
        var then = date;
        var now = new Date();

        var yearsAgo = now.getFullYear() - then.getFullYear();
        if (yearsAgo > 0) {
            return date.toString('d') + ' ' + date.toString('t');
        }

        var monthsAgo = (now.getMonth() + 1) - (then.getMonth() + 1);
        if (monthsAgo > 0) {
            return date.toString('d') + ' ' + date.toString('t');
        }

        var daysAgo = now.getDate() - then.getDate();
        if (daysAgo > 0) {
            return date.toString('d') + ' ' + date.toString('t');
        }

        var hoursAgo = (now.getHours() + 1) - (then.getHours() + 1);
        if (hoursAgo > 0) {
            return date.toString('t');
        }

        // TODO: localize these
        var minutesAgo = (now.getMinutes() + 1) - (then.getMinutes() + 1);
        if (minutesAgo > 0) {
            if (minutesAgo > 1) {
                return minutesAgo + ' minutes ago';
            } else {
                return minutesAgo + ' minute ago';
            }
        }

        // TODO: localize this
        return 'Just now';
    }


    function syncModificationDrawing(data, conversationMessages) {
        if (typeof data.modifications == 'undefined') return;

        // need a date to properly place in ui, sort by id first, then later we will insert at the proper timestamp position.  
        // this ensures that the later events are inserted later as long as timestamps are the same
        var modifications = $.each(data.modifications, function (index, modification) {
            if (modification.lastMessageId != null) {
                var message = _u.find(conversationMessages, function (message) { return message.id == modification.lastMessageId; });

                if (typeof message != 'undefined' && modification.appliedDate < message.date) {
                    modification.appliedDate = message.date;
                }
            }                
        });
        var modifications = _u.chain(modifications).filter(function (o) { return o.appliedDate != null; }).sortBy(function (o) { return o.id; }).value();                        

        var currentModificationIds = [];

        var itemsGroupedByAppendLocation = [];
        var existingMessageRows = getExistingDOMMessageRows();;

        // group modifications by the location in the DOM that they will append to.  we are trying to minimize calls to DOM for performance reasons
        for (var i = 0; i < modifications.length; i++) {
            var modification = modifications[i];
            
            currentModificationIds.push(modification.id);
            var existsInUi = (typeof _renderedModifications[modification.id] != 'undefined');
            if (existsInUi) continue;

            if (typeof modification.outputContext == 'string') modification.outputContext = JSON.parse(modification.outputContext);

            // group items by "append after"
            var appendAfter = null;
            var goesInSameLocation = false;
            
            // slight optimization: if message time is the same as previous message, assume same "append after" w/o actually calling "getModificationAppendLocation"
            if (!goesInSameLocation) {
                if (itemsGroupedByAppendLocation.length > 0) {
                    var currentGroup = itemsGroupedByAppendLocation[itemsGroupedByAppendLocation.length - 1];
                    goesInSameLocation = (currentGroup.items[currentGroup.items.length - 1].appliedDate.getTime() == modification.appliedDate.getTime());
                }            
            }

            if (!goesInSameLocation) {
                appendAfter = getModificationAppendLocation(modification.appliedDate.getTime(), existingMessageRows);
                goesInSameLocation =
                    (itemsGroupedByAppendLocation.length > 0) && (appendAfter == itemsGroupedByAppendLocation[itemsGroupedByAppendLocation.length - 1].appendAfter); 
            }


            if (goesInSameLocation) {
                itemsGroupedByAppendLocation[itemsGroupedByAppendLocation.length - 1].items.push(modification);
            } else {
                itemsGroupedByAppendLocation.push({
                    items: [ modification ],
                    appendAfter: getModificationAppendLocation(modification.appliedDate.getTime())
                });
            }
        }

        //var t1 = new Date();
        //console.log(itemsGroupedByAppendLocation.length);        

        // add grouped modifications to DOM
        for (var i = 0; i < itemsGroupedByAppendLocation.length; i++) {
            var modGroup = itemsGroupedByAppendLocation[i];

            // build html of all mods in group
            var listHtml = '';
            listHtml += '<div class="modGroup">';
            for (var j = 0; j < modGroup.items.length; j++) {
                var modification = modGroup.items[j];

                // add the pretty value so we can display it better
                if (modification.type == 'conversation handler change') {
                    var inputContext = modification.inputContext;
                    var handlerName = 'unknown conversation handler';
                    if (inputContext.conversationHandlerId != null && _conversationHandlers != null) {
                        var handler = _u.find(_conversationHandlers, function (handler) { return handler.id == inputContext.conversationHandlerId; });
                        if (handler != null) {
                            handlerName = 'conversation handler "' + handler.name + '"';
                        }
                    }
                    var whatHappened = inputContext.enabled ? 'Enabling' : 'Disabling';

                    var status = modification.outputContext != null && modification.outputContext.sentOk ? 'succeeded' : 'failed';

                    var whatWillHappen = '';
                    if (modification.outputContext != null) {
                        if (!modification.outputContext.sentOk && modification.outputContext.retrying) {
                            whatWillHappen = ", will retry soon";
                        }
                    }

                    modification.prettyValue = whatHappened + ' ' + handlerName + ' ' + status + ' at '+ modification.appliedDate.toString("d") + ' ' + modification.appliedDate.toString("t") + whatWillHappen;
                }

                // add to UI
                var html = _factoryData['templateInstances' + _options.messengerStyle].modification({
                    modification: modification
                });

                listHtml += html;                

                _renderedModifications[modification.id] = modification;  
            }

            listHtml += '</div>';

            $(modGroup.appendAfter).after(listHtml);                         
        }


        // check for modifications that are rendered but no longer exist, delete them
        var removeAllMods = (currentModificationIds.length == 0);
        if (removeAllMods) {
            // special case: removing all mods from UI.  removing one-by-one is slow and this case happens enough (when user toggles annotations on/off) that we should make it fast            
            //$(options.container).find('.message[data-mod-id]').remove(); // this is still slow
            $(options.container).find('.modGroup').remove();
            _renderedModifications = {};
        } else {
            var renderedModificationIds = [];
            $.each(_renderedModifications, function (modificationId, modification) {
                renderedModificationIds.push(modification.id);
            });
            $.each(renderedModificationIds, function (index, renderedModificationId) {
                var existsInData = (currentModificationIds.indexOf(renderedModificationId) >= 0);
                if (!existsInData) {
                    // remove from ui
                    $(options.container).find('.message[data-mod-id="' + renderedModificationId + '"]').remove();
                    delete _renderedModifications[renderedModificationId];
                }
            });
        }

        //t2 = new Date();
        //console.log('mod syncing took ' + (t2.getTime() - t1.getTime()) + ' - ' + modifications.length);
    }



    function drawGoogleMap(uiMessage, locations) {
        if (_options.messengerStyle == "v1" || locations.length > 1) {
            drawGoogleMapV1(uiMessage, locations);
        } else {
            drawGoogleMapV2(uiMessage, locations);
        }
        
    }

    function drawGoogleMapV1(uiMessage, locations) {
        _factoryData.loadGoogleMapsScript(function () {
            var mapDiv = $(uiMessage).find('div').find('div');
            $(mapDiv).height(320);
            $(mapDiv).width(320);

            var lats = _u.chain(locations).pluck('lat').map(function (lat) { return parseFloat(lat); }).value();
            var totalLats = 0;
            $.each(lats, function (index, lat) {
                totalLats += lat;
            });
            var avgLat = totalLats / lats.length;

            var lons = _u.chain(locations).pluck('long').map(function (long) { return parseFloat(long); }).value();
            var totalLons = 0;
            $.each(lons, function (index, lon) {
                totalLons += lon;
            });
            var avgLon = totalLons / lons.length;

            if (isNaN(avgLat) || isNaN(avgLon)) {
                return;
            }

            var element = $(mapDiv)[0];

            var map = new google.maps.Map(element, {
                center: { lat: avgLat, lng: avgLon },
                zoom: 14
            });

            var markerInfo = [];
            var bounds = new google.maps.LatLngBounds();
            $.each(locations, function (index, location) {
                if (location.lat != null && location.long != null) {

                    var marker = new google.maps.Marker({
                        position: { lat: parseFloat(location.lat), lng: parseFloat(location.long) },
                        map: map,
                        title: location.label
                    });

                    var content = getMapMarkerContent(location, index, true, function (addressFormatted) {
                        return 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(addressFormatted);
                    });

                    if (content != null & content != '') {
                        var infoWindow = new google.maps.InfoWindow({
                            content: content
                        });
                        markerInfo.push({
                            marker: marker,
                            infoWindow: infoWindow
                        });
                    }

                    bounds.extend(new google.maps.LatLng(location.lat, location.long));
                }
            });
            // hack so the info bubbles go away when clicking on another marker
            $.each(markerInfo, function (index, info) {
                info.marker.addListener('click', function () {
                    $.each(markerInfo, function (index2, otherInfo) {
                        otherInfo.infoWindow.close();
                    });

                    info.infoWindow.open(map, info.marker);
                });
            });
            // hack so the info bubbles go away when clicking on the map itself
            map.addListener('click', function () {
                $.each(markerInfo, function (index, info) {
                    info.infoWindow.close();
                });
            });

            if (locations.length > 1) {
                map.fitBounds(bounds);
            }

            _options.callbacks.onDrawn(true);
        });
    }

    function drawGoogleMapV2(uiMessage, locations) {
        _factoryData.loadGoogleMapsScript(function () {
            var mapDiv = $(uiMessage).find('div').find('div.mapContainer');
            $(mapDiv).height(320);
            $(mapDiv).width(320);
            $(mapDiv).css({
                "border-bottom": "solid 1px #DDE1E6"
            });

            var lats = _u.chain(locations).pluck('lat').map(function (lat) { return parseFloat(lat); }).value();
            var totalLats = 0;
            $.each(lats, function (index, lat) {
                totalLats += lat;
            });
            var avgLat = totalLats / lats.length;

            var lons = _u.chain(locations).pluck('long').map(function (long) { return parseFloat(long); }).value();
            var totalLons = 0;
            $.each(lons, function (index, lon) {
                totalLons += lon;
            });
            var avgLon = totalLons / lons.length;

            if (isNaN(avgLat) || isNaN(avgLon)) {
                return;
            }

            var element = $(mapDiv)[0];

            var map = new google.maps.Map(element, {
                center: { lat: avgLat, lng: avgLon },
                zoom: 14,
                mapTypeControl: false,
                streetViewControl: false
            });

            var markerInfo = [];
            var bounds = new google.maps.LatLngBounds();
            $.each(locations, function (index, location) {
                if (location.lat != null && location.long != null) {

                    var marker = new google.maps.Marker({
                        position: { lat: parseFloat(location.lat), lng: parseFloat(location.long) },
                        map: map,
                        title: location.label
                    });

                    var content = getMapMarkerContent(location, index, true, function (addressFormatted) {
                        return 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(addressFormatted);
                    });

                    if (content != null & content != '') {
                        var infoWindow = new google.maps.InfoWindow({
                            content: content
                        });
                        markerInfo.push({
                            marker: marker,
                            infoWindow: infoWindow
                        });

                        if (locations.length == 1) {
                            addMapDetails(uiMessage, location, map);
                        }
                        

                    }

                    bounds.extend(new google.maps.LatLng(location.lat, location.long));
                }
            });
            // hack so the info bubbles go away when clicking on another marker
            $.each(markerInfo, function (index, info) {
                info.marker.addListener('click', function () {
                    $.each(markerInfo, function (index2, otherInfo) {
                        otherInfo.infoWindow.close();
                    });

                    info.infoWindow.open(map, info.marker);
                });
            });
            // hack so the info bubbles go away when clicking on the map itself
            map.addListener('click', function () {
                $.each(markerInfo, function (index, info) {
                    info.infoWindow.close();
                });
            });

            if (locations.length > 1) {
                map.fitBounds(bounds);
            }

            //_options.callbacks.onDrawn(true);
            
            setTimeout(function () { _options.callbacks.onDrawn(true); }, 3);
        });
    }

    function addMapDetails(uiMessage, location, map) {
        $(uiMessage).find('div.message_content').css({
            "border-width": "1px",
            "border-style": "solid",
            "border-color": "#DDE1E6",
            "background": "#FFFFFF",
            "padding": "0px"
        });
        //Location card div
        $(uiMessage).find('div').find('div').find('.title').text(location.label);
        var addressLine1 = location.address1;
        var addressLine2 = location.city + ", " + location.state;
        $(uiMessage).find('div').find('div').find('.addressLine1').text(addressLine1);
        $(uiMessage).find('div').find('div').find('.addressLine2').text(addressLine2);

        var mapButton = $(uiMessage).find('.mapActionButton');
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function (position) {
                //Change button
                $(mapButton).toggleClass('navigate', true);
                $(mapButton).text("Navigate");                
                $(uiMessage).find('div').find('div').find('.navigation_details').show();

                var start = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
                var end = new google.maps.LatLng(location.lat, location.long);

                //Calculate distance and travel time
                //TODO parameterize units and travel mode
                var distanceMatrixService = new google.maps.DistanceMatrixService();
                var distanceRequest = {
                    destinations: [end],
                    origins: [start],
                    unitSystem: google.maps.UnitSystem.IMPERIAL,
                    travelMode: google.maps.TravelMode.DRIVING
                }

                distanceMatrixService.getDistanceMatrix(distanceRequest, function (resp, status) {
                    if (status == google.maps.DirectionsStatus.OK) {
                        var tripStats = resp.rows[0].elements[0];

                        $(uiMessage).find('div').find('div').find('.direction_distance').text(tripStats.distance.text);
                        $(uiMessage).find('div').find('div').find('.direction_traveltime').text(tripStats.duration.text);
                    } else {
                        console.log(status);
                    }

                });

                //Add route to map
                var directionsService = new google.maps.DirectionsService()

                directionsDisplay = new google.maps.DirectionsRenderer({
                    suppressMarkers: true
                });

                //TODO parameterize units and travelmode
                var request = {
                    origin: start,
                    destination: end,
                    unitSystem: google.maps.UnitSystem.IMPERIAL,
                    travelMode: google.maps.TravelMode.DRIVING
                };

                directionsService.route(request, function (response, status) {
                    if (status == google.maps.DirectionsStatus.OK) {
                        directionsDisplay.setDirections(response);
                        directionsDisplay.setMap(map);

                        var startLocation = new Object();
                        var endLocation = new Object();
                        var waypointLocations = [];

                        // Display start and end markers for the route.
                        var legs = response.routes[0].legs;
                        for (i = 0; i < legs.length; i++) {
                            if (i == 0) {
                                startLocation.latlng = legs[i].start_location;
                                startLocation.address = legs[i].start_address;
           
                            }
                            if (i != 0 && i != legs.length - 1) {
                                var waypoint = {};
                                waypoint.latlng = legs[i].start_location;
                                waypoint.address = legs[i].start_address;
                                waypointLocations.push(waypoint);
                            }
                            if (i == legs.length - 1) {
                                endLocation.latlng = legs[i].end_location;
                                endLocation.address = legs[i].end_address;
                            }
                        }
                        createMarker(map, endLocation.latlng, "end", "http://www.google.com/mapfiles/markerB.png")
                        createMarker(map, startLocation.latlng, "start", "http://maps.google.com/mapfiles/kml/pal4/icon57.png");

                    } else {
                        alert("Directions Request from " + start.toUrlValue(6) + " to " + end.toUrlValue(6) + " failed: " + status);
                    }
                });


                $(mapButton).click(function () {
                    var origin = encodeURI(position.coords.latitude + "," + position.coords.longitude);
                    var destination = encodeURI(location.address1 + ' ' + location.address2 + ' ' + location.city + ', ' + location.state + ' ' + location.zip);
                    //var destination = encodeURI(location.address1 == null ? location.lat + "," + location.long : location.address1);
                    var mapUrl = "https://www.google.com/maps/dir/?api=1&origin=" + origin + "&destination=" + destination + "&dir_action=navigate";
                    window.open(mapUrl);
                });

            }, function (error) {
                $(mapButton).text("Open Map");
                $(mapButton).click(function () {
                    var params = encodeURI(location.address1 + ' ' + location.address2 + ' ' + location.city + ', ' + location.state + ' ' + location.zip);
                    //var params = encodeURI(location.address1 == null ? location.lat + "," + location.long : location.address1);
                    var mapUrl = "https://www.google.com/maps/search/?api=1&query=" + params;
                    window.open(mapUrl);
                });
            });
        }

    }

    function createMarker(map, latlng, label, url) {
        var marker = new google.maps.Marker({
            position: latlng,
            map: map,
            icon: url,
            title: label,
            zIndex: Math.round(latlng.lat() * -100000) << 5
        });
    }


    function drawMapQuestMap(uiMessage, locations) {
        _factoryData.loadMapQuestScript(function () {
            var mapDiv = $(uiMessage).find('div').find('div');
            $(mapDiv).height(320);
            $(mapDiv).width(320);

            var lats = _u.chain(locations).pluck('lat').map(function (lat) { return parseFloat(lat); }).value();
            var totalLats = 0;
            $.each(lats, function (index, lat) {
                totalLats += lat;
            });
            var avgLat = totalLats / lats.length;

            var lons = _u.chain(locations).pluck('long').map(function (long) { return parseFloat(long); }).value();
            var totalLons = 0;
            $.each(lons, function (index, lon) {
                totalLons += lon;
            });
            var avgLon = totalLons / lons.length;

            if (isNaN(avgLat) || isNaN(avgLon)) {
                return;
            }

            var id = $(mapDiv)[0].id;
            var map = L.mapquest.map(id, {
                center: [avgLat, avgLon],
                layers: L.mapquest.tileLayer('map'),
                zoom: 14
            });

            var bounds = L.latLngBounds();
            $.each(locations, function (index, location) {
                if (location.lat != null && location.long != null) {

                    var marker = L.marker([location.lat, location.long], {}).addTo(map);

                    var content = getMapMarkerContent(location, index, true, function (addressFormatted) {
                        return 'https://www.mapquest.com/search/result?query=' + encodeURIComponent(addressFormatted) + '&page=0&index=0';
                    });

                    if (content != null && content != '') {
                        marker.bindPopup(content, {});
                        marker.on("popupopen", function (e) {
                            var contentElement = $(_options.container).find('.marker-' + index);
                            var contentWidth = $(contentElement).width();
                            content = content.replace('display: inline-block;', 'display: inline-block; width: ' + contentWidth + 'px;');
                            e.popup.setContent(content);
                        });
                    }

                    bounds.extend([location.lat, location.long]);
                }
            });

            if (locations.length > 1) {
                map.fitBounds(bounds);
            }

            _options.callbacks.onDrawn(true);
        });
    }

    function getMapMarkerContent(location, index, attachLabelToAddressFormatted, getDirectionsUrl) {
        var lineStyle = 'style="white-space: nowrap;"';

        var info = '';
        if (location.label != null && location.label != '') {
            info += '<div ' + lineStyle + '><strong>' + location.label + '</strong></div>';
        }
        var directionsLink = null;
        if (location.address1 != null && location.address1 != '' &&
            location.city != null && location.city != '' &&
            location.state != null && location.state != '' &&
            location.zip != null && location.zip != 0) {

            info += '<div ' + lineStyle + '>' + location.address1 + '</div>';
            if (location.address2 != null && location.address2 != '') {
                info += '<div ' + lineStyle + '>' + location.address2 + '</div>';
            }
            info += '<div ' + lineStyle + '>' + location.city + ', ' + location.state + ' ' + location.zip + '</div>';

            var addressFormatted = (attachLabelToAddressFormatted ? location.label + ' ' : '') + location.address1 + ' ' + location.address2 + ' ' + location.city + ', ' + location.state + ' ' + location.zip;
            var directionsUrl = getDirectionsUrl(addressFormatted);
            directionsLink = '<div ' + lineStyle + '><a href="' + directionsUrl + '" target="_blank">Get Directions</a></div>';
        }

        if (location.phone != null && location.phone != '') {
            info += '<div ' + lineStyle + '>' + location.phone + '</div>';
        }

        var content = info;
        if (directionsLink != null) {
            content += directionsLink;
        }

        if (content == null || content == '') {
            return null;
        }

        return '<div class="marker-' + index + '" style="display: inline-block;">' + content + '</div>';
    }

    function checkIfImagesLoaded() {
        var images = $(_options.container).find('img');
        $.each(images, function (index, image) {
            if (!image.complete) {
                var onImageLoaded = function () {
                    _options.callbacks.onDrawn(true);
                }

                $(image).on('load', onImageLoaded);
                $(image).on('error', onImageLoaded);
            }
        });
    }


    function toHtml(text) {
        return text.replace(/\r\n/g, '<br/>');
    }

    function getHorizontalScrollerUsedWidth(scroller) {
        var items = $(scroller).find('.scroll_item');
        var uWidth = 0;
        $.each(items, function (index, item) {
            var includeMargin = true;
            uWidth += $(item).outerWidth(includeMargin);
        });
        return uWidth;
    }


    function syncHorizontalScrollButtons(container) {
        var scroller = $(container).find('.horizontal_scroller');
        if (_options.messengerStyle == 'v2') {
            scroller = $(container).find('.scroller');
        }
        if (scroller.length == 0) return;

        var aWidth = $(scroller).width();
        var uWidth = Math.floor(getHorizontalScrollerUsedWidth(scroller) + 0.5);

        var xPos = $(scroller).scrollLeft();

        var visibleRange = {
            left: xPos,
            right: xPos + aWidth
        };

        var rowHeight = $(scroller).height();
        $(container).find('.scroll').css('line-height', rowHeight + 'px');

        var showScrollRight = Math.floor(visibleRange.right + 0.5) < uWidth;
        var showScrollLeft = visibleRange.left > 0;
        $(container).find('.scroll[data-direction="right"]').toggleClass('visible', showScrollRight);
        $(container).find('.scroll[data-direction="left"]').toggleClass('visible', showScrollLeft);
        
        var hideOccludedElements = false;
        if (hideOccludedElements) {
            //console.log('----');
            var visiblePixelThd = 10;
            var scrollerRect = $(scroller).get(0).getBoundingClientRect();
            var clippingSpan = [ scrollerRect.left, scrollerRect.right ];
       
            var occludingRects = [];
            if (showScrollLeft) {
                var rect = $(container).find('.scroll[data-direction="left"]')[0].getBoundingClientRect();
                occludingRects.push(rect);
                clippingSpan[0] = rect.right;
            }
            if (showScrollRight) {
                var rect = $(container).find('.scroll[data-direction="right"]')[0].getBoundingClientRect();
                occludingRects.push(rect);
                clippingSpan[1] = rect.left;
            }

            //console.log(clippingSpan);

            var scrollItems = $(container).find('.scroll_item');
            $.each(scrollItems, function (si, scrollItem) {
                var scrollRect = $(scrollItem).get(0).getBoundingClientRect();               
                
                var visibilityInfo = getVisibilityInfo(scrollRect, clippingSpan);
                //console.log(visibilityInfo);

                var hide = false;
                //$.each(occludingRects, function(ri, occluder) {
                //    if (intersects(occluder, scrollRect)) {
                //        hide = true;
                //        return false;
                //    }
                //});
                if (visibilityInfo.pct < 0.3) hide = true;
                
                $(scrollItem).css('visibility', hide ? 'hidden' : 'visible');
            });
        }
    }


    function intersects(a, b) {
        var intersects =
            (a.left < b.right && a.right > b.left) &&
            (a.top < b.bottom && a.bottom > b.top);        
        return intersects;
    }


    function getVisibilityInfo(rect, clipSpan) {
        var visibleSpan = [ rect.left, rect.right ];

        // no overlap in intervals
        if (visibleSpan[1] < clipSpan[0] ||  visibleSpan[0] > clipSpan[1]) return 0;

        // clamp to clip span
        if (visibleSpan[0] < clipSpan[0]) visibleSpan[0] = clipSpan[0];
        if (visibleSpan[1] > clipSpan[1]) visibleSpan[1] = clipSpan[1];

        var visibleWidth = visibleSpan[1] - visibleSpan[0];
        var result = {
            pixels: visibleWidth,
            pct: 0
        };
        var totalWidth = rect.right - rect.left;
        if (totalWidth > 0) result.pct = visibleWidth / totalWidth;

        return result;
    }

    function getMessageAppendLocation(messageId, messageTime) {
        var defaultRow = $(options.container).find('.messages .placeholder')[0];

        var bestRow = defaultRow;
        var bestRowTime = -1;

        var existingRows = $(options.container).find('.message');
        existingRows = _u.map(existingRows, function (r) {
            var tmpId = parseInt($(r).attr('data-id'));

            return {
                row: r,
                id: tmpId > 0 ? tmpId : 0,
                time: parseInt($(r).attr('data-time'))
            }
        });

        // select the row that is nearest to the new rows time but as old or older
        for (var i = 0; i < existingRows.length; i++) {
            var row = existingRows[i];
            var rowMessageId = row.id;
            var rowMessageTime = row.time;

            if (rowMessageId > 0 && messageId > -1) {
                if (messageId >= rowMessageId) {
                    bestRow = row.row;
                }
            } else {
                if (messageTime >= rowMessageTime) {
                    bestRow = row.row;
                }
            }
        }

        var topModGroup = $(bestRow).parents('.modGroup').last();
        if (topModGroup.length > 0) {
            bestRow = topModGroup;
        }

        var topModGroup = $(bestRow).parents('.modGroup').last();
        if (topModGroup.length > 0) {
            bestRow = topModGroup;
        }

        return bestRow;
    }

    function getExistingDOMMessageRows() {
        var existingRows = $(options.container).find('.message');
        existingRows = _u.map(existingRows, function (r) {
            return {
                row: r,
                time: parseInt($(r).attr('data-time'))
            }
        });
        return existingRows;
    }

    function getModificationAppendLocation(modificationTime, existingRows) {        
        var bestRow = null;

        if (existingRows == null) {
            existingRows = getExistingDOMMessageRows();
        }

        // select the row that is nearest to the new rows time but as old or older
        for (var i = 0; i < existingRows.length; i++) {
            var row = existingRows[i];
            var rowMessageTime = row.time;

            if (modificationTime >= rowMessageTime) {
                bestRow = row.row;
            }
        }

        if (bestRow == null) {
            var defaultRow = $(options.container).find('.messages .placeholder')[0];
            bestRow = defaultRow;
        }

       return bestRow;
    }


    // Use "pretty date" for stuff that is within 12 hours or so
    function customPrettyDate(time) {
        var diff = (((new Date()).getTime() - time.getTime()) / 1000);
        var hour_diff = Math.floor(diff / 3600);

        if (hour_diff < 12) return prettyDate(time);
        return time.toString('d') + ' - ' + time.toString('t');
    }

    function needsBroken(tweet, maxLength) {
        var breakThd = 55;
        if (typeof maxLength != 'undefined') breakThd = maxLength;
        var content = (tweet.large_content != null && tweet.large_content.length > tweet.content.length) ? tweet.large_content : tweet.content;
        var tokes = content.split(' ');
        for (var i = 0; i < tokes.length; i++) {
            if (tokes[i].length > breakThd) return true;
        }
        return false;
    }
    
    function valueSort(a, b) {
        if (a == null && b != null) return -1;
        if (a != null && b == null) return 1;
        if (a == null && b == null) return 0;
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
    }

    function drawConversationWidget(container, triggerCallback) {
        var imgEl;
        var uiModel = {
            messages: [],
            dateFormatterFn: customPrettyDate,
            linkifyFn: function (text) {
                text = text.replace(/\n/g, "<br/>");
                return _options.linkifyText(text);
            },
            breakContentFn: function (content) {
                var breakThd = 35;
                var contentNeedsBroken = needsBroken({ content: content, large_content: null }, breakThd);
                return contentNeedsBroken;
            }
        };

        $(container).html(_factoryData['templateInstances' + _options.messengerStyle].authorConversations(uiModel));

        if (triggerCallback) _options.callbacks.onDrawn(true);
    }


    function tryResolveMedia(post, domElement) {
        console.log(post);
    }

    function sendTesterModification(modification) {
        var eventObj = {
            source: 'AstuteBot.WebChat',
            type: 'testerModification',
            data: {
                modification: modification
            }
        }
        var message = JSON.stringify(eventObj)
        window.parent.postMessage(message, '*');
    }

    function drilldownModification(modification) {
        console.log(modification.outputContext.dataType);

        switch (modification.outputContext.dataType) {
            case 'apiResponse':
                {
                    if (!_options.isTest) {
                        var apiResponse = JSON.parse(modification.outputContext.dataJson);
                        var modalUi = _options.callbacks.onModalRequested();
                        SimpleApiResponseWidgetFactory.createInstance({
                            container: $(modalUi),
                            apiResponse: apiResponse
                        });
                    } else {
                        sendTesterModification(modification);
                    }
                }
                break;
            
            case 'expressionError':
            case 'expressionResult':
                {
                    if (!_options.isTest) {
                        var isError = (modification.outputContext.dataType == 'expressionError');
                        var expressionInfo = JSON.parse(modification.outputContext.dataJson);
                        var modalUi = _options.callbacks.onModalRequested();
                        ExpressionWidgetFactory.createInstance({
                            container: modalUi,
                            title: {
                                enabled: true,
                                value: (isError ? 'Expression Evaluation Error' : 'Expression Evaluation Result')
                            },
                            context: {
                                enabled: true,
                                value: expressionInfo.context,
                                readOnly: true
                            },
                            expression: {
                                value: expressionInfo.expression,
                                label: 'Expression',
                                placeholder: '{{ }}'
                            },
                            test: {
                                enabled: true,
                                resultSet: (isError ? true : false),
                                currentResult: {
                                    hadError: true,
                                    errorMessage: expressionInfo.errorMessage
                                }
                            },
                            apply: {
                                enabled: false
                            },
                            cancel: {
                                enabled: true,
                                label: 'Close',
                                onClicked: function () {
                                    _options.callbacks.onModalExitRequested();
                                }
                            }
                        });
                    } else {
                        sendTesterModification(modification);
                    }
                }
                break;

            case 'contextView':
                {
                    if (!_options.isTest) {
                        var modalUi = _options.callbacks.onModalRequested();
                        JsonEditorWidgetFactory.createInstance({
                            container: modalUi,
                            json: modification.outputContext.dataJson,
                            height: $(modalUi).height(),
                            includeApplyButton: false,
                        });
                    } else {
                        sendTesterModification(modification);
                    }
                }
                break;
            case 'link':                
                var baseUrl = location.ancestorOrigins?.[0] ? `${location.ancestorOrigins[0]}/bot/bots/bots` : `${location.origin}/react_dist/#/bots`;

                if (_options.isTest) {
                    baseUrl = `${location.origin}/react_dist/#/bots`;
                }

                var url = `${baseUrl}?drilldown=authoring`;
                var params = JSON.parse(modification.outputContext.dataJson);
                $.each(Object.keys(params), function (index, key) {
                    url = url + '&' + key + '=' + params[key];
                });
                window.open(url, '_blank');
                break;
        }
    }

    function analyticsEvent(elem){
        var data = {};
        switch(elem.tagName.toLowerCase()){
            case 'a':
                data.href = elem.href;
                data.innerHTML = elem.innerHTML;
                break;
        }
        var analyticsType = elem.getAttribute('data_analyticsType');
        var message = $(elem).closest('.message');
        var messageId = parseInt($(message).attr('data-id'));
        switch(analyticsType){
            case 'QuickReplyClicked':
                data.label = _renderedMessages[messageId].content.replies[$(elem).attr('data-index')].label;
                break;
            case 'MultiReplyClicked':
                data.label = _renderedMessages[messageId].content.replies[$(elem).attr('data-index')].label;
                data.selected = !$(elem).hasClass('active');
                break;
        }
        _options.callbacks.analyticsEvent(analyticsType, data);
    }

    init();
    return _interface;
}
;
var SimpleCalendarWidgetFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    };

    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {
            container: null,
            callbacks: {
                onDateSelected: function (date) { },
            }
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="simpleCalendar">' +
                    '<table>' +
                        '<tr>' +
                            '<td class="spacerCell"></td>' +
                            '<td class="calendarCell">' +
                                '<div class="header">' +
                                    '<div class="headerLabel">Pick a date</div>' +
                                    '<div class="monthYearPicker">' +
                                        '<button class="previousMonth">' +
                                            '<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12" fill="none">' +
                                                '<path fill-rule="evenodd" clip-rule="evenodd" d="M7 10.5L5.5 12L0 6L5.5 0L7 1.5L2.69995 6L7 10.5Z" fill="#717578"/>' +
                                            '</svg>' +
                                        '</button>' +
                                        '<div class="monthYearLabel">' +

                                        '</div>' +
                                        '<button class="nextMonth">' +
                                            '<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12" fill="none">' +
                                                '<path fill-rule="evenodd" clip-rule="evenodd" d="M0 10.5L4.30005 6L0 1.5L1.5 0L7 6L1.5 12L0 10.5Z" fill="#717578"/>' +
                                            '</svg>' +
                                        '</button>' +
                                    '</div>' +
                                '</div>' +
                                '<div class="main">' +
                                    '<table>' +
                                        '<thead>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                        '</thead>' +
                                        '<tbody>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                            '<tr>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                                '<td></td>' +
                                            '</tr>' +
                                        '</tbody>' +
                                    '</table>' +
                                '</div>' +
                            '</td>' +
                            '<td class="spacerCell"></td>' +
                        '</tr>' +
                    '<table>' +
                '</div>'
        };

        // auto doT templates
        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });       

        _factoryData.isInitialized = true;
    }


    // wrap all factory/one-time data in  a single object so we only have to punch one hole in interface to instance code to use
    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {}
    };

    return _factoryInterface;
}());


// per-instance junk
SimpleCalendarWidgetFactory.createInstance = function (options) {
    var _interface = {
    };

    // ---------------

    // will trigger factory init() if needed
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);              

    var _selectedDate = new Date();
    var _displayMonth = _selectedDate.getMonth();
    var _displayYear = _selectedDate.getFullYear();

    function init() {             
        draw();
    }    

    function draw() {
        $(_options.container).html(_factoryData.templateInstances.control({}));

        drawCalendar();
    }      

    function onPreviousMonth() {
        _displayMonth = _displayMonth - 1;

        if (_displayMonth < 0) {
            _displayMonth = 11;
            _displayYear = _displayYear - 1;
        }

        drawCalendar();
    }

    function onNextMonth() {
        _displayMonth = _displayMonth + 1;

        if (_displayMonth > 11) {
            _displayMonth = 0;
            _displayYear = _displayYear + 1;
        }

        drawCalendar();
    }

    function getLocalization() {
        var language = '';

        var localizations = {
            months: {
                default: [
                    'January',
                    'February',
                    'March',
                    'April',
                    'May',
                    'June',
                    'July',
                    'August',
                    'September',
                    'October',
                    'November',
                    'December'
                ]
            },
            days: {
                default: [
                    'Su',
                    'Mo',
                    'Tu',
                    'We',
                    'Th',
                    'Fr',
                    'Sa'
                ]
            }
        };

        var localization = {
            months: null,
            days: null
        };

        localization.months = localizations.months[language];
        if (localization.months == null) {
            localization.months = localizations.months['default'];
        }

        localization.days = localizations.days[language];
        if (localization.days == null) {
            localization.days = localizations.days['default'];
        }

        return localization;
    }

    function drawCalendar() {
        cleanMonthSelector();
        cleanDaySelector();

        var localization = getLocalization();

        drawMonthSelector(localization);
        drawDaySelector(localization);
    }

    function drawMonthSelector(localization) {
        var simpleCalendarElement = document.getElementsByClassName('simpleCalendar')[0];

        var previousMonthElement = simpleCalendarElement.getElementsByClassName('previousMonth')[0];
        previousMonthElement.removeEventListener('click', onPreviousMonth);
        previousMonthElement.addEventListener('click', onPreviousMonth);

        var monthYearLabelValue = localization.months[_displayMonth] + ' ' + _displayYear;
        var monthYearLabelElement = simpleCalendarElement.getElementsByClassName('monthYearLabel')[0];
        monthYearLabelElement.innerHTML = monthYearLabelValue;

        var nextMonthElement = simpleCalendarElement.getElementsByClassName('nextMonth')[0];
        nextMonthElement.removeEventListener('click', onNextMonth);
        nextMonthElement.addEventListener('click', onNextMonth);
    }   

    function cleanMonthSelector() {
        var simpleCalendarElement = document.getElementsByClassName('simpleCalendar')[0];

        var previousMonthElement = simpleCalendarElement.getElementsByClassName('previousMonth')[0];
        previousMonthElement.removeEventListener('click', onPreviousMonth);

        var nextMonthElement = simpleCalendarElement.getElementsByClassName('nextMonth')[0];
        nextMonthElement.removeEventListener('click', onNextMonth);
    }

    function drawDaySelector(localization) {
        var simpleCalendarElement = document.getElementsByClassName('simpleCalendar')[0];
        var dayElements = simpleCalendarElement.querySelectorAll('.main tbody td');

        var headerElements = simpleCalendarElement.querySelectorAll('.main thead td');
        for (var i = 0; i < 7; i++) {
            headerElements[i].innerHTML = localization.days[i];
        }

        var selectedMonth = _selectedDate.getMonth();
        var selectedYear = _selectedDate.getFullYear();
        var selectedDay = _selectedDate.getDate();

        var firstOfMonth = new Date(_displayYear, _displayMonth, 1);
        var calendarData = calendarize(firstOfMonth, 0);
        for (var i = 0; i < calendarData.length; i++) {
            var skip = i * 7;
            for (var j = 0; j < 7; j++) {
                var dayData = calendarData[i][j];
                var dayElement = dayElements[skip + j];

                if (dayData != 0) {
                    dayElement.innerHTML = dayData;
                    dayElement.setAttribute('data-id', dayData);
                    dayElement.addEventListener('click', onDaySelected);
                }

                if (selectedYear == _displayYear && selectedMonth == _displayMonth && dayData == selectedDay) {
                    dayElement.classList.add('selected');
                }
            }
        }
    }

    function onDaySelected(event) {
        var simpleCalendarElement = document.getElementsByClassName('simpleCalendar')[0];
        var selectedDay = parseInt(event.target.getAttribute('data-id'));

        var days = simpleCalendarElement.querySelectorAll('.main tbody td');
        for (var i = 0; i < days.length; i++) {
            var dayElement = days[i];

            if (dayElement.classList.contains('selected')) {
                dayElement.classList.remove('selected');
            }

            var dayNumber = dayElement.getAttribute('data-id');
            if (dayNumber != null && dayNumber == selectedDay) {
                dayElement.classList.add('selected');
            }
        }

        _options.callbacks.onDateSelected(new Date(_displayYear, _displayMonth, selectedDay));
    }

    function cleanDaySelector() {
        var simpleCalendarElement = document.getElementsByClassName('simpleCalendar')[0];
        var dayElements = simpleCalendarElement.querySelectorAll('.main tbody td');
        for (var i = 0; i < dayElements.length; i++) {
            var dayElement = dayElements[i];

            if (dayElement.classList.contains('selected')) {
                dayElement.classList.remove('selected');
            }

            dayElement.innerHTML = '';
            dayElement.removeAttribute('data-id');
            dayElement.removeEventListener('click', onDaySelected);
        }
    }

    function calendarize(target, offset) {
        var i = 0, j = 0, week, out = [], date = new Date(target || new Date);
        var year = date.getFullYear(), month = date.getMonth();

        // day index (of week) for 1st of month
        var first = new Date(year, month, 1 - (offset | 0)).getDay();

        // how many days there are in this month
        var days = new Date(year, month + 1, 0).getDate();

        while (i < days) {
            for (j = 0, week = Array(7); j < 7;) {
                while (j < first) week[j++] = 0;
                week[j++] = ++i > days ? 0 : i;
                first = 0;
            }
            out.push(week);
        }

        return out;
    }

    init();
    return _interface;
};
var AutoCompleteWidgetFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    };

    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {
            container: null,
            delayMs: 500,
            log: false,
            callbacks: {
                onGetData: function (utterance) { setData([]); },
                onSuggestionTaken: function (suggestion) { },
                onSelectionChanged: function (selection) { }
            }
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="auto_complete_widget">' +
                    '<div class="content">' +
                        '<table class="auto_complete_table" width=100%>' +
                        '</table>' +
                    '</div>' +
                '</div>',
            suggestion:
                '<tr>' +
                    '<td class="clickable suggestion">{{=it.suggestion}}</td>' +
                '</tr>'
        };

        // auto doT templates
        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });

        _factoryData.isInitialized = true;
    }


    // wrap all factory/one-time data in  a single object so we only have to punch one hole in interface to instance code to use
    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {},
        getDefaultExample: function (id) { }
    };

    return _factoryInterface;
}());


// per-instance junk
AutoCompleteWidgetFactory.createInstance = function (options) {
    var _interface = {
        userTyped: function (utterance) { userTyped(utterance); },
        setData: function (suggestions) { setData(suggestions); },
        selectUp: function () { selectUp(); },
        selectDown: function () { selectDown(); },
        selectNone: function () { selectNone(); }
    };

    // ---------------

    // will trigger factory init() if needed
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);


    var _autoCompleteQueued = false;
    var _queuedUtterance = null;

    var _autoCompleteInProgress = false;
    var _lastUtterance = null;
    var _lastUtteranceChange = Date.now();

    function init() {
        draw();
    }

    function log(message) {
        if (!_options.log) {
            return;
        }

        console.log(message);
    }

    function draw() {
        $(_options.container).html(_factoryData.templateInstances.control(_options));

        $(_options.container).find('.auto_complete_widget').hide();
    }

    function userTyped(utterance) {
        log('auto complete: user typed ' + utterance);
        _queuedUtterance = utterance;
        _lastUtteranceChange = Date.now();

        if (_lastUtterance == _queuedUtterance) {
            log('auto complete: last utterance was same as this');
            return;
        }

        if (_autoCompleteQueued) {
            log('auto complete: queued already ' + _queuedUtterance);
            return;
        }

        if (_autoCompleteInProgress) {
            _autoCompleteQueued = true;
            log('auto complete: queued ' + _queuedUtterance);
            return;
        }

        if (_queuedUtterance == null || _queuedUtterance == '') {
            clearData();
            log('auto complete: utterance was empty, cleared data');
            return;
        }

        queueAutoComplete();
    }

    function doAutoComplete() {
        _lastUtterance = _queuedUtterance;

        _autoCompleteInProgress = true;
        log('auto complete: getting data for ' + _lastUtterance);
        _options.callbacks.onGetData(_lastUtterance);
    }

    function queueAutoComplete() {
        if (_options.delayMs == null) {
            doAutoComplete();
            return;
        }

        _autoCompleteQueued = true;

        setTimeout(function () {
            var now = Date.now();
            if (now - _lastUtteranceChange < (_options.delayMs - 1)) {
                log('auto complete: user typed within 1/2 second, queueing ' + _queuedUtterance);
                queueAutoComplete();
                return;
            }

            _autoCompleteInProgress = false;
            _autoCompleteQueued = false;
            doAutoComplete();
        }, _options.delayMs);
        log('auto complete: queued for 1/2 second from now...');
    }

    function setData(suggestions) {
        suggestions = suggestions.reverse();

        log('auto complete: data set for ' + _lastUtterance + ':');
        log(suggestions);

        clearData();

        var autoCompleteTable = $(_options.container).find('.auto_complete_table');
        if (suggestions != null) {
            $.each(suggestions, function (index, suggestion) {
                var html = _factoryData.templateInstances.suggestion({ suggestion: suggestion });
                $(autoCompleteTable).append(html);
            });

            if (suggestions.length > 0) {
                $(_options.container).find('.suggestion').click(function () {
                    var suggestion = $(this).html();
                    _options.callbacks.onSuggestionTaken(suggestion);                    
                });

                var autoCompleteWidget = $(_options.container).find('.auto_complete_widget');
                $(autoCompleteWidget).show();

                var height = $(autoCompleteTable).height();
                if (height > 150) {
                    height = 150;
                    $(autoCompleteWidget).addClass('nano');
                } else {
                    $(autoCompleteWidget).removeClass('nano');
                }

                $(autoCompleteWidget).css('bottom', (height + 1) + 'px');

                $(_options.container).find('.nano').nanoScroller();
                $(_options.container).find('.nano').nanoScroller({ scroll: 'bottom' });
            }
        }

        if (_autoCompleteQueued) {
            queueAutoComplete();
        } else {
            _autoCompleteInProgress = false;
            log('auto complete: finished.');
        }
    }

    function selectUp() {
        var selected = $(_options.container).find('.suggestion.selected');
        if (selected.length < 1) {
            var last = $(_options.container).find('.suggestion').last();
            $(last).addClass('selected');
            $(_options.container).find('.nano').nanoScroller({ scrollTo: $(last) });
            var suggestion = $(last).html();
            log('auto complete: selection changed to ' + suggestion);
            _options.callbacks.onSelectionChanged(suggestion);
        } else {
            var previous = $(selected).parent().prev().find('.suggestion');
            $(selected).removeClass('selected');            
            if (previous.length < 1) {
                log('auto complete: selection changed to ' + null);
                _options.callbacks.onSelectionChanged(null);
            } else {
                $(previous).addClass('selected')
                $(_options.container).find('.nano').nanoScroller({ scrollTo: $(previous) });
                var suggestion = $(previous).html();
                log('auto complete: selection changed to ' + suggestion);
                _options.callbacks.onSelectionChanged(suggestion);
            }
        }
    }

    function selectDown() {
        var selected = $(_options.container).find('.suggestion.selected');
        if (selected.length < 1) {
            var first = $(_options.container).find('.suggestion').first();
            $(first).addClass('selected');
            $(_options.container).find('.nano').nanoScroller({ scrollTo: $(first) });
            var suggestion = $(first).html();
            log('auto complete: selection changed to ' + suggestion);
            _options.callbacks.onSelectionChanged(suggestion);
        } else {
            var next = $(selected).parent().next().find('.suggestion');
            $(selected).removeClass('selected');            
            if (next.length < 1) {
                log('auto complete: selection changed to ' + null);
                _options.callbacks.onSelectionChanged(null);
            } else {                
                $(next).addClass('selected')
                $(_options.container).find('.nano').nanoScroller({ scrollTo: $(next) });
                var suggestion = $(next).html();
                log('auto complete: selection changed to ' + suggestion);
                _options.callbacks.onSelectionChanged(suggestion);
            }
        }
    }

    function selectNone() {
        var selected = $(_options.container).find('.suggestion.selected')
        if(selected.length > 0){
            selected.removeClass('selected');
        }
        else{
            clearData();
        }
        _options.callbacks.onSelectionChanged(null);
    }

    function clearData() {
        $(_options.container).find('.auto_complete_table').empty();
        $(_options.container).find('.auto_complete_widget').hide();
    }

    init();
    return _interface;
};
var MultimediaLobbyWidgetFactory = (function() {
    var _factoryInterface = {
        createInstance: function(options) { return null; },
        getFactoryData: function() { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    }; 
    
    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {    
            container: null,
            userInfo: null,
            lobbyState: {
                audioEnabled: true,
                videoEnabled: true
            },
            videoAllowed: true, // todo: implement this
            includeDecline: false,
            includeAccept: false,
            acceptText: 'Join',
            header: '',
            callbacks: {
                onJoined: function(localAudioEnabled, localVideoEnabled) {},
                onStateChanged: function(audioEnabled, videoEnabled) {},
                onDeclinedAccepted: function(accepted, lobbyState) {}
            }
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="MultimediaLobbyWidget">' +                    
                    '<div class="lobbyLoaded" style="display: none;">' +
        
                        '<div class="TopUi">' +
                            '<div class="Title">{{=it.header}}</div>' +
                        '</div>' +

                        '<div class="cameraPreview text-center">' +
                            '<div id="local-media" class="text-center"></div>' +
                        '</div>' +
                        
                        '<div class="BottomUi"> ' +
                            '<div class="text-center actionBar noselect">' +
                                '{{?it.includeAccept || it.includeDecline}}' +
                                    '<div style="padding-bottom: 16px;">' +
                                        '{{?it.includeDecline}}<button class="btn btn-default decline">Decline</button> {{?}}' +
                                        '{{?it.includeAccept}}<button class="btn btn-primary accept">{{=it.acceptText}}</button>{{?}}' +
                                    '</div>' +
                                '{{?}}' +

                                '<div class="action video {{?!it.videoAllowed}}cannotEnable{{?}}" data-active="{{=(it.lobbyState.videoEnabled && it.videoAllowed) ? "1" : "0"}}">' +
                                    '<i class="si-videocamera toggle"></i>' +
                                '</div>' +
                                '<div class="action audio" data-active="{{=it.lobbyState.audioEnabled ? "1" : "0"}}">' +
                                    '<i class="si-mic_on toggle"></i>' +
                                '</div> ' +
                            '</div>' +
                        '</div>' +
                    '</div>' +                        
                '</div>'
        };
        
        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });

        _factoryData.isInitialized = true;
    }

    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {}
    };

    return _factoryInterface;
}());


MultimediaLobbyWidgetFactory.createInstance = function(options) {
    var _interface = {
        destroy: function() {},
        onMessageReceived: function(conversation) {},
        onShown: function() {}
    };
    
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);
    
    var _tryPreview = false;    
    var _previewTrack = null;
    var _previewTrackAttached = false;       
    var _destroyed;

    function init() {        
        //console.log('lobby init');
        _destroyed = false;
        $(_options.container).html(_factoryData.templateInstances.control(_options));
        initAudio();
        initVideo();

        $(_options.container).find('.decline').click(function () {
            _options.callbacks.onDeclinedAccepted(false);
            return false;
        });
        $(_options.container).find('.accept').click(function () {
            _options.callbacks.onDeclinedAccepted(true, _options.lobbyState);
            return false;
        });

        //$(_options.container).find('.join').click(function () {
        //    join();
        //});        
    }


    _interface.destroy = function() {
        //console.log('lobby destroy');
        _destroyed = true;

        destroyTracks(_previewTrack);
    };


    function syncUi() {
        // error getting preview video
        var errorAttachingPreview = _tryPreview && !_previewTrackAttached;
        if (errorAttachingPreview) {
            console.log('could not preview');
            addVideoStandIn();            
            $(_options.container).find('.action.video').toggleClass('cannotEnable', true);
            $(_options.container).find('.action.audio').toggleClass('cannotEnable', true);
            $(_options.container).find('.action').attr('data-active', '0');            
        }

        var readyToShowLobby = true;
        if (readyToShowLobby) {
            {
                var infoHtml = 
                    '';
                $(_options.container).find('.Info').html(infoHtml);
            }

            $(_options.container).find('.lobbyLoaded').show();

            if (!errorAttachingPreview) {
                $(_options.container).find('.action').unbind().click(function() {
                    var isAudio = $(this).hasClass('audio');
                    var isVideo = $(this).hasClass('video');
                    if (isAudio || isVideo) {
                        var isActive = ($(this).attr('data-active') == "1");
                        isActive = !isActive;
                        $(this).attr('data-active', isActive ? '1' : '0');

                        if (isAudio) {
                            $(this).find('.toggle').toggleClass('si-mic_on', isActive);
                            $(this).find('.toggle').toggleClass('si-mic_off', !isActive);
                        }
                        if (isVideo) {
                            $(this).find('.toggle').toggleClass('si-videocamera', isActive);
                            $(this).find('.toggle').toggleClass('si-disabledvideocamera', !isActive);                            
                        }

                        onStateChanged();

                        var matchingLocalTrack = _u.find(_previewTrack.getTracks(), function (o) { return o.kind == (isAudio ? 'audio' : 'video'); });

                        if (typeof matchingLocalTrack != 'undefined') {
                            if (isVideo) {
                                if (isActive) { 
                                    initVideo();                            
                                } else {                                    
                                    addVideoStandIn();
                                    detachTracks([matchingLocalTrack]);       
                                    destroyTracks(_previewTrack);
                                }
                            }
                        }
                    }                    
                });
            }
        }
    }

    function destroyTracks(stream) {
        if (stream != null) {
            var tracks = stream.getTracks();

            for (var i = 0; i < tracks.length; i++) {
                var track = tracks[i];
                track.stop();
            }

            stream = null;
        }
    }

    function onStateChanged() {
        _options.lobbyState = {
            audioEnabled: ($(_options.container).find('.action.audio').attr('data-active') == '1'),
            videoEnabled: ($(_options.container).find('.action.video').attr('data-active') == '1')
        };
        _options.callbacks.onStateChanged(_options.lobbyState);
    }

    function addVideoStandIn() {
        var videoStandIn = '';
        videoStandIn +=
            '<div class="videoDisabled">';
        
        try {
            var avatar = _options.userInfo.avatar;
            if (avatar && avatar.length > 4) {
                videoStandIn += 
                '<div class="avatarContainer">' +
                    '<img src="' + avatar + '" class="avatar"></img>' +
                '</div>';
            }
        } catch (e) {}


        videoStandIn +=
            '</div>';

        $(_options.container).find('#local-media').html(videoStandIn);        
    } 

    function initAudio() {
        // start up mic and then immediately tear down the track
        // we don't need a mic preview yet, but it is good to get the permission for the mic here
        if (navigator.mediaDevices.getUserMedia) {
            navigator.mediaDevices.getUserMedia({ video: false, audio: true })
                .then(function (stream) {
                    destroyTracks(stream);
                    syncUi();
                })
                .catch(function (err) {
                    console.log(err);
                });
        }
    }

    function initVideo() {
        if (!options.videoAllowed) {
            return;
        }

        if (navigator.mediaDevices.getUserMedia) {
            navigator.mediaDevices.getUserMedia({ video: true })
                .then(function (stream) {
                    if (_destroyed) {
                        destroyTracks(stream);
                        return;
                    }

                    _tryPreview = true;
                    _previewTrack = stream;

                    var previewContainer = document.getElementById('local-media');
                    if (!previewContainer.querySelector('video')) {
                        attachTracks(stream, previewContainer);
                    } else {
                        syncUi();
                    }  
                })
                .catch(function (err) {
                    console.log(err);
                });
        }
    }
           
   
    // Attach the Tracks to theDOM.
    function attachTracks(tracks, container) {        
        $(container).html(
            '<video autoplay="" muted="" playsinline=""></video>'
        );
        $(container).find('video').unbind().on('loadeddata', function (e) {
            _previewTrackAttached = true;                    
            syncUi();
        });          

        $(container).find('video')[0].srcObject = tracks;
        var playPromise = $(container).find('video')[0].play();
        if (playPromise !== undefined) {
            playPromise.then(function() {
                
            }).catch(function(error) {
                
            });
        }
    }

    // Detach the Tracks from the DOM.
    function detachTracks(tracks) {
        $(_options.container).find('video').remove();
    }  
   
    init();
    return _interface;
}
;
var MultimediaCallWidgetFactory = (function() {
    var _factoryInterface = {
        createInstance: function(options) { return null; },
        getFactoryData: function() { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    }; 
    
    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {    
            container: null,
            userInfo: null,
            lobbyState: null,
            videoAllowed: true,
            isUser: true,
            api: null,

            // need 1 of these: (try to make it just use calls started event in future)
            conversation: null,
            multimediaProviderId: null,
            callStartedEvent: null,

            sendMessageFn: function(conversationId, type, content) { },

            callbacks: {                
                onStateChanged: function(state) { },
                onCallStateChanged: function (isActive) { },
                onCallEnded: function (callId) { },
                onUserConnectedStateChanged: function (callId, isConnected) { }                
            }
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="MultimediaCallWidget MultimediaLobbyWidget">' +   // piggyback off lobby stlyes for most stuff like overlay buttons
                    
                    '<div class="TopUi">' +
                        '<div class="Title"></div>' +
                    '</div>' +
                    
                    '<div class="VideoContainer Primary"></div>' +                    
                    '<div class="VideoContainer Secondary"></div>' +                
                
                    '<div class="BottomUi"> ' +
                        '<div class="text-center actionBar noselect">' +
                            '<div class="action video {{?!it.videoAllowed}}cannotEnable{{?}}" data-active="{{=(it.lobbyState.videoEnabled && it.videoAllowed) ? "1" : "0"}}">' +
                                '<i class="si-videocamera toggle"></i>' +
                            '</div>' +
                            '<div class="action audio" data-active="{{=it.lobbyState.audioEnabled ? "1" : "0"}}">' +
                                '<i class="si-mic_on toggle"></i>' +
                            '</div>' +
                            
                            // end call button
                            //'{{?!it.isUser}}' +
                                '<div class="action CloseCall" data-active="0" title="End call">' +
                                    '<i class="si-disapprove"></i>' +
                                '</div> ' +
                            //'{{?}}' +
                        '</div>' +
                    '</div>' +
                '</div>'
        };
        
        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });

        _factoryData.isInitialized = true;
    }

    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {}
    };

    return _factoryInterface;
}());


MultimediaCallWidgetFactory.createInstance = function(options) {
    var _interface = {
        destroy: function() {},
        onMessageReceived: function(conversation, message) {},
        reset: function() {},
        onShown: function() {}
    };
    
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);
    var _lobbyWidget = null;
    var _providerWidget = null;
    var _callId = null;


    _interface.reset = function() {
        _options.lobbyState = null;
        _options.callStartedEvent = null;
        if (_lobbyWidget != null) {
            _lobbyWidget.destroy();
            _lobbyWidget = null;
        }
        init();
    };

    _interface.onShown = function() {
        syncSecondaryVideoDimensions();
    };

    function init() {        
        //console.log('call init');
        var needsLobbied = (_options.lobbyState == null || typeof _options.lobbyState == 'undefined');
        //console.log('needs lobbied = '+ needsLobbied);
        if (needsLobbied) {
            _options.lobbyState = {
                audioEnabled: true,
                videoEnabled: _options.videoAllowed,
            };

            // check to see if a multimedia call on this conversation already exists.  if it doesn't, allow the user (agent) to create a new call after pressing join/accept
            if (_options.callStartedEvent == null) _options.callStartedEvent = getActiveCallStartedEvent(_options.conversation.messages);  
            var callAlreadyExists = (_options.callStartedEvent != null);
            //console.log('call exists = ' + callAlreadyExists);
            
            var acceptText = '';
            var header = '';
            if (_options.isUser) {
                acceptText = 'Accept';
                if (_options.callStartedEvent != null) {
                    header = 'Agent "' + _options.callStartedEvent.content.context.createdBy.displayName + '" is calling you';
                }
            } else {
                acceptText = callAlreadyExists ? 'Join' : 'Call';
            }

            _lobbyWidget = MultimediaLobbyWidgetFactory.createInstance({
                container: _options.container,
                userInfo: _options.userInfo,
                lobbyState: _options.lobbyState,
                videoAllowed: _options.videoAllowed,
                includeDecline: _options.isUser,
                includeAccept: true,
                header: header,
                acceptText: acceptText,
                callbacks: {
                    onStateChanged: function(state) {         
                        _options.callbacks.onStateChanged(state);
                    },
                    onDeclinedAccepted: function(accepted, lobbyState) {
                        if (accepted) {
                            // accepted call
                            _options.lobbyState = lobbyState;
                            if (_lobbyWidget != null) {
                                _lobbyWidget.destroy();
                                _lobbyWidget = null;
                            }

                            if (!callAlreadyExists) {
                                // agent is intiating a call to the user
                                var multimediaType = _options.lobbyState.videoEnabled ? 2 : 1;
                                console.log(_options.userInfo);
                                _options.api.conversation.multimedia.createCall(
                                    _options.conversation.id,
                                    _options.multimediaProviderId,
                                    multimediaType,               
                                    _options.userInfo,
                                    _options.conversation.realtimeToken
                                );

                                // creating a call should cause a "call started" event to rattle in, which should cause the UI to update correctly via "onMessageReceived"

                            } else {
                                drawCallUi();
                            }
                        } else {
                            // user declined call (probably from agent)
                            sendMessage({
                                type: 'event',
                                content: JSON.stringify({
                                    type: 26, // call declined
                                    context: JSON.stringify({
                                        callId: _callId,
                                        agentInfo: _options.userInfo
                                    })
                                })
                            });
                        }
                    }
                }
            });
        } else {
            drawCallUi();
        }

        //$(_options.container).find('.join').click(function () {
        //    join();
        //});        
    }


    //  when we receive a "call started" event, draw call ui.  this is used in case where agent is calling an agent from the lobby (i.e. the agent is initiating a call frmo an already-existing
    // agent conversation, as opposed to an agent queue ring)
    _interface.onMessageReceived = function(conversation, message) {
        _options.conversation = conversation;
        
        var isCallEndedEvent = (message.contentType == 10) && (message.content.type == 25);
        var isCallStartedEvent = (message.contentType == 10) && (message.content.type == 22);
        var isCallDeclinedEvent = (message.contentType == 10) && (message.content.type == 26);
        var isUserDisconnectedEvent = (message.contentType == 10) && (message.content.type == 14);

        if (isCallStartedEvent) {
            drawCallUi();            
        }
        if (isCallEndedEvent) {
            _options.callbacks.onCallStateChanged(false);
            _options.callbacks.onCallEnded();
        }
        if (isCallDeclinedEvent || 
            isUserDisconnectedEvent) {
            _options.callbacks.onCallEnded();
        }
    };


    function onUserConnected() {
        if (_callId == null) return;

        _options.callbacks.onUserConnectedStateChanged(_callId, true);
    }

    function onUserDisconnected() {
        if (_callId == null) return;

        _options.callbacks.onUserConnectedStateChanged(_callId, false);
    }


    function sendMessage(message) {
        var conversationId = null;
        if (_options.conversation != null) conversationId = _options.conversation.id;
        if (_options.callStartedEvent != null) conversationId = _options.callStartedEvent.conversationId;
        if (conversationId == null) return;

        _options.sendMessageFn(conversationId, message.type, message.content);
    }

    function drawCallUi() {
        _options.callbacks.onCallStateChanged(true);

        $(_options.container).html(_factoryData.templateInstances.control(_options));

        if (_options.callStartedEvent == null) _options.callStartedEvent = getActiveCallStartedEvent(_options.conversation.messages);

        //var callExists = (_options.callStartedEvent != null);

        //if (!callExists) {
        //    if (!_options.isUser) {
        //        // call is not active on conversation, client is agent.
        //    } else {
        //        // call is not active, and client is the end-user.  this shouldnt be possible
        //        console.log('big error');
        //    }
        //}


        var roomCreatedEvent = _options.callStartedEvent;        

        if (roomCreatedEvent != null) {
            _callId = _options.callStartedEvent.content.context.callId;
            
            if (!_options.videoAllowed) _options.lobbyState.videoEnabled = false;

            var eventContext = roomCreatedEvent.content.context;
            switch (eventContext.type) {
                // twilio
                case 0:                
                    {
                        var token = eventContext.twilioContext.userJwt;
                        if (!_options.isUser) {
                            token = eventContext.twilioContext.agentJwt;
                        }

                        // TODO: create a "TwilioCallProvider" widget that is instantiated here:
                        _providerWidget = TwilioMultimediaProviderWidgetFactory.createInstance({
                            roomName: eventContext.twilioContext.roomName,
                            token: token,
                            userMediaState: _options.lobbyState, // object that defined is local users audio, video enabled? 
                            primaryContainer: $(_options.container).find('.VideoContainer.Primary'),
                            secondaryContainer: $(_options.container).find('.VideoContainer.Secondary'),
                            callbacks: {
                                setTitle: function(title) {
                                    $(_options.container).find('.Title').html(title);
                                },
                                syncSecondaryVideoDimensions: function () {
                                    syncSecondaryVideoDimensions();
                                }
                            }
                        });     

                        onUserConnected();
                    }
                    break;
            }
        }        


        $(_options.container).find('.action').unbind().click(function() {
            var isAudio = $(this).hasClass('audio');
            var isVideo = $(this).hasClass('video');
            if (isAudio || isVideo) {
                var isActive = ($(this).attr('data-active') == "1");
                isActive = !isActive;
                $(this).attr('data-active', isActive ? '1' : '0');

                if (isAudio) {
                    $(this).find('.toggle').toggleClass('si-mic_on', isActive);
                    $(this).find('.toggle').toggleClass('si-mic_off', !isActive);
                }
                if (isVideo) {
                    $(this).find('.toggle').toggleClass('si-videocamera', isActive);
                    $(this).find('.toggle').toggleClass('si-disabledvideocamera', !isActive);                            
                }                

                var enabledState = {
                    audioEnabled: ($(_options.container).find('.action.audio').attr('data-active') == '1'),
                    videoEnabled: ($(_options.container).find('.action.video').attr('data-active') == '1')
                };

                if (_providerWidget != null) {
                    _providerWidget.setAVEnabled(enabledState);
                }

                //var matchingLocalTrack = _u.find(_previewTracks.getTracks(), function (o) { return o.kind == (isAudio ? 'audio' : 'video'); });

                //if (typeof matchingLocalTrack != 'undefined') {
                //    if (isVideo) {
                //        matchingLocalTrack.enabled = isActive;
                //    } else {
                //        // always disable audio track from actually playing because its very distracting
                //        matchingLocalTrack.enabled = false;
                //    }

                //    if (isVideo) {
                //        if (isActive) { 
                //            // re-add to dom (overkill?)
                //            attachTracks(_previewTracks, document.getElementById('local-media'));                                
                //        } else {                                    
                //            addVideoStandIn();
                //            // remove from dom (overkill?)
                //            detachTracks([matchingLocalTrack]);       
                //        }
                //    }
                //}
            }                    


            // close call
            if ($(this).hasClass('CloseCall')) {
                closeCall();
            }
        });
    }


    function syncSecondaryVideoDimensions() {
        var secondaryContainer = $(_options.container).find('.VideoContainer.Secondary');
        var video = $(secondaryContainer).find('video');
        if (video.length == 0) return;
        video = $(video)[0];

        width = video.videoWidth;
        height = video.videoHeight;
                

        if (height == 0) {
            $(secondaryContainer).hide();
            return;
        }

        var aspectRatio = width / height;

        // secondary container is set to have a fixed width of 20%.  adjust height based off of that
        $(secondaryContainer).show();
        var containerWidth = $(secondaryContainer).outerWidth();
        var containerHeight = Math.floor(containerWidth / aspectRatio);
        $(secondaryContainer).css('height', containerHeight + 'px');                
    }


    function closeCall() {
        var doClose = function() {
            if (_providerWidget != null) {
                _providerWidget.destroy();
                _providerWidget = null;

                onUserDisconnected();
            }                  
        };

        if (_options.isUser) {
            doClose();
        } else {
            ShowConfirmDialog({
                body: 'Are you sure you want to end this call?',
                title: 'End call?',
                onClickedOk: function () {
                    doClose();
			    },
                onClickedCancel: function () {
                },
            });
        }
    }



    function getActiveCallStartedEvent(messages) {        
        var callStartedEvent = null;
        if (typeof messages != 'undefined') {
            for (var i = messages.length - 1; i >= 0; i--) {
                var message = messages[i];                            
                var isCallStartedEvent = (message.contentType == 10) && (message.content.type == 22);
                var isCallEndedEvent = (message.contentType == 10) && (message.content.type == 25);
                var isCallDeclinedEvent = (message.contentType == 10) && (message.content.type == 26);

                // if a call ended event happened after the most recent call started event, then there should not be an active call because
                // there is only one active call per conversation hopefully
                if (isCallEndedEvent) break;

                // same applies for call declined:
                if (isCallDeclinedEvent) break;

                if (isCallStartedEvent) {
                    callStartedEvent = message;
                    break;
                }

            }
        }
        return callStartedEvent;
    };


    _interface.destroy = function() {        
        //console.log('call destroy');
        if (_lobbyWidget != null) {
            _lobbyWidget.destroy();
            _lobbyWidget = null;
        }
        if (_providerWidget != null) {
            _providerWidget.destroy();
            _providerWidget = null;

            onUserDisconnected();
        }
    };

   
    init();
    return _interface;
}
;
// one-time junk
var JsonEditorWidgetFactory = (function() {
    var _factoryInterface = {
        createInstance: function(options) { return null; },
        getFactoryData: function() { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    }; 
    
    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {    
            container: null,                        

            obj: null,
            json: null, // fallback json is filled in

            includeApplyButton: true,
            hideApplyWhenSame: false,
            alertIfInvalid: true,          // if true, will display an icon if current json is not valid 
            displayFormatOption: true,     // if true, will display a button users can click to "format" json after making edits
            height: 200,
            heightOverride: null,
            callbacks: {
                onUpdated: function() {},
                onCanceled: function() {},
                onChange: function() {}
            }
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="json_editor_widget" {{?it.heightOverride != null}}style="max-height:{{=it.heightOverride}}px;"{{?}} >' +
                    '<div>' +
                        '<span class="ei-jsonEditor-statusIcons">' +
                            '<button class="ei-button ei-button--medium ei-button--invisible ei-button--square format" title="Format current JSON">' +
                                '<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 3L13 1.5V6H8.5l1.9-1.9c-.8-1-2-1.6-3.4-1.6-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5c2.3 0 4.2-1.8 4.5-4H13c-.3 3.1-2.8 5.5-6 5.5-3.3 0-6-2.7-6-6s2.7-6 6-6c1.8 0 3.4.8 4.5 2z"/></svg>' +
                            '</button>' +
                        '</span>' +

                        '<textarea class="ei-jsonEditor-textarea ei-textarea" spellcheck=false {{?it.heightOverride != null}}style="min-height:{{=it.heightOverride}}px!important"{{?}} ></textarea>' +
                        '<div class="ei-input-error invalid_json">Current JSON is not valid</div>' +
                    '</div>' +
                    '{{?it.includeApplyButton}}' +
                        '<div class="ei-modal-footer">' +
                            '<button type="button" class="ei-button ei-button--medium ei-button--ghost cancel">Cancel</button>' +
                            '<button type="button" class="ei-button ei-button--medium ei-button--primary apply">Apply</button>' +
                        '</div>' +
                    '{{?}}' +
                '</div>',

        };

        // auto doT templates
        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });


        _factoryData.isInitialized = true;
    }


    // wrap all factory/one-time data in  a single object so we only have to punch one hole in interface to instance code to use
    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {}
    };

    return _factoryInterface;
}());





// per-instance junk
JsonEditorWidgetFactory.createInstance = function(options) {
    var _interface =
    {
        read: function() {},
        destroy: function() {}
    };
    

    var _api = null;

    // ---------------

    // will trigger factory init() if needed
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);


    var _jsonIsFormatted;
    var _jsonIsValid;

    var _prettyJson = null;

    var _destroyed = false;

    function init() {
        draw();
    }   

    _interface.read = function() {
        return {
            content: $(_options.container).find('textarea').val(),
            isValid: _jsonIsValid
        }
    };

    function draw() {
        if (_options.json != null) {
            try {
                _options.obj = JSON.parse(_options.json);
                _prettyJson = JSON.stringify(_options.obj, null, 2);
                _jsonIsFormatted = true;
                _jsonIsValid = true;
            } catch (e) {
                _prettyJson = _options.json;
                _jsonIsFormatted = false;
                _jsonIsValid = false;
            }
        } else {
            _prettyJson = JSON.stringify(_options.obj, null, 2);
            _jsonIsFormatted = true;
            _jsonIsValid = true;
        }

        var uiModel = {
            height: getTextHeight(),
            heightOverride: _options.heightOverride,
            value: _prettyJson,
            includeApplyButton: _options.includeApplyButton
        };
        $(_options.container).html(_factoryData.templateInstances.control(uiModel));

        if (_options.hideApplyWhenSame) {
            $(_options.container).find('.apply').hide();
        }

        $(_options.container).find('textarea').val(uiModel.value);

        $(_options.container).find('.apply').click(function(e) {
            try {
                _options.json = $(_options.container).find('textarea').val();
                _options.callbacks.onUpdated(JSON.parse(_options.json));
                draw();
            } catch (e) {
                alert(e);
            }
            e.stopPropagation();
        });

        $(_options.container).find('.cancel').click(function(e) {
            _options.callbacks.onCanceled(e);
            e.stopPropagation();
        });

        $(_options.container).find('.format').click(function() {
            var objVal = JSON.parse($(_options.container).find('textarea').val());
            var prettyJson = JSON.stringify(objVal, null, 2);
            $(_options.container).find('textarea').val(prettyJson);
            _jsonIsFormatted = true;
            syncIcons();
        });

        // alow tab funcitonality inside textarea
        var textarea = $(_options.container).find('textarea')[0];
        textarea.onkeydown = function(e){
            if(e.keyCode==9 || e.which==9){
                e.preventDefault();
                var s = this.selectionStart;
                this.value = this.value.substring(0,this.selectionStart) + "  " + this.value.substring(this.selectionEnd); // spaces instead of tabs (2 of them to match pretty print above)
                this.selectionEnd = s+2; 
            }
        }

        $(_options.container).find('textarea').on('change keyup paste', function () {
            var value = $(_options.container).find('textarea').val();

            // hide apply when same
            if (_options.hideApplyWhenSame) {
                if (value != _prettyJson) {
                    $(_options.container).find('.apply').show();
                } else {
                    $(_options.container).find('.hide').show();
                }
            }

            _jsonIsFormatted = false;
            _jsonIsValid = currentIsValidJson();            
            syncIcons();
            _options.callbacks.onChange();            
        });

        $(options.container).parent().click(function (e) {
            e.stopPropagation();
        });

        syncIcons();
    }

    function syncIcons() {
        $(_options.container).find('.format').css('display', (_jsonIsFormatted || !_options.displayFormatOption || !_jsonIsValid) ? 'none' : 'flex');
        $(_options.container).find('.invalid_json').css('display', (_jsonIsValid || !_options.alertIfInvalid) ? 'none' : 'block');
    }

    function currentIsValidJson() {
        var current = $(_options.container).find('textarea').val();
        try {
            var obj = JSON.parse(current);
            return true;
        } catch (e) {            
        }  
        return false;
    }

    _interface.destroy = function() {

    }

    function getTextHeight() {    
        var heights = {
            textAreaPadding: 2 * 2,
            textAreaBorder: 2 * 1
        };
        return _options.height - _u.reduce(_u.values(heights), function (c, height) { return c + height; }, 0);
    }


    init();
    return _interface;
};
// one-time junk
var SimpleApiResponseWidgetFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    };

    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {
            container: null,
            apiResponse: null,
        };

        _factoryData.templateDefinitions = {
            control:
                '<div class="simple_api_response_widget nano">' +
                    '<div class="content">' +
                        '<div class="response_section_main">' +
                            '<h4>' +
                                'Request:' +
                            '</h4>' +                            
                            '<table class="response_section">' +
                                '<tr>' +
                                    '<td class="section_label">Verb:</td>' +
                                    '<td class="section_value">{{=it.request.verb}}</td>' +
                                '</tr>' +
                                '<tr>' +
                                    '<td class="section_label">Endpoint:</td>' + 
                                    '<td class="section_value">{{=it.request.endpoint}}</td>' +
                                '</tr>' +
                                '<tr>' +
                                    '<td class="section_label">Headers:</td>' +
                                    '<td class="section_value">' +           
                                        '<table class="response_section">' +
                                        '{{~it.request.headers :header:index}}' +
                                            '<tr><td class="section_label">{{=header.key}}</td><td class="section_label">{{=header.value}}</td></tr>' +
                                        '{{~}}' +
                                        '</table>' +
                                    '</td>' +
                                '</tr>' +
                                '<tr>' +
                                    '<td class="section_label">Body:</td>' +
                                    '<td class="section_value"><textarea readonly>{{=it.request.body}}</textarea></td>' +
                                '</tr>' +
                            '</table>' +
                        '</div>' +
                        '<div class="response_section_main">' +
                            '<h4>' +
                                'Response:' +
                            '<h4>' +
                            '<table class="response_section">' +
                                '<tr>' +
                                    '<td class="section_label">Status Code:</td>' + 
                                    '<td class="section_value">{{=it.response.statusCode}}</td>' +
                                '</tr>' +
                                '<tr>' +
                                    '<td class="section_label">Headers:</td>' +
                                    '<td class="section_value">' +
                                        '<table class="response_section">' +
                                        '{{~it.response.headers :header:index}}' +
                                            '<tr><td class="section_label">{{=header.key}}</td><td class="section_label">{{=header.value}}</td></tr>' +
                                        '{{~}}' +
                                        '</table>' +
                                    '</td>' +
                                '</tr>' +
                                '<tr>' +
                                    '<td class="section_label">Body:</td>' +
                                    '<td class="section_value"><textarea readonly>{{=it.response.body}}</textarea></td>' +
                                '</tr>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                '<div>'
        };

        $.each(_factoryData.templateDefinitions, function (key, templateDefinition) {
            _factoryData.templateInstances[key] = doT.template(templateDefinition);
        });

        _factoryData.isInitialized = true;
    }


    // wrap all factory/one-time data in  a single object so we only have to punch one hole in interface to instance code to use
    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstances: {}
    };

    return _factoryInterface;
}());





// per-instance junk
SimpleApiResponseWidgetFactory.createInstance = function (options) {
    var _interface = {};

    // ---------------

    // will trigger factory init() if needed
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);    

    function init() {        
        draw();
    }

    function draw() {       
        $(_options.container).html(_factoryData.templateInstances.control(_options.apiResponse));

        var textAreas = $(_options.container).find('textarea');
        $.each(textAreas, function (index, textArea) {
            var scrollHeight = textArea.scrollHeight;
            $(textArea).height(scrollHeight);
        });         

        $(_options.container).find('.nano').nanoScroller();
    }

    init();
    return _interface;
};
// one-time junk
var TestBotWidgetFactory = (function() {
    var _factoryInterface = {
        createInstance: function(options) { return null; },
        getFactoryData: function() { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    }; 
    
    // initialize factory/one-time data.  done only when first instance is created
    function init() {
        _factoryData.defaultInstanceOptions = {    
            container: null,
            botAccountServiceId: '',
            accountSettings: null,
            replyHeightPixels: 36,
            createContextJson: null,
            updateContextJson: null,
            autoGetStarted: false,
            conversationServiceId: '',
            persistConversationAcrossSession: false,
            forceNewSession: false,
            allowMicrophone: false,
            initialUserMessage: null,
            platform: 'Web Chat',            
            realtimeUrl: 'https://realtime.astutebot.com/signalr', // we should pass this in from a setting if we want to run locally
            postAnalyticsEvents: false
        };

        _factoryData.templateDefinitionsv1 = {
            control:
                '<div class="chat">' +
                    '<div class="header"></div>' +

                    '<div class="Conversation nano v1" style="height: {{=it.convoHeight}}px;">' +
                        '<div class="content">' +

                            '<div class="jump clickableADA" data-source="begin" data-target="end" tabindex="0" aria-label="Jump to end of conversation"><input type="Button" value="Jump to end of conversation"></input></div>' +

                            '<div class="message_content">' +
                            '</div>' +

                            '<div class="jump clickableADA" data-source="end" data-target="begin" tabindex="0" aria-label="Jump to the beginning of conversation"><input type="Button" value="Jump to the beginning of conversation"></input></div>' +

                        '</div>' +                        
                    '</div>' +

                    '<div class="AlternateContainer nano" style="height: {{=it.convoHeight}}px; display: none;">' +
                        '<div class="content"></div>' +
                    '</div>' +

                    '<div class="auto_complete_widget_container" style="height: 0px;">' +
                    '</div>' +

                    '<table style="height:{{=it.replyHeight}}px; overflow: visible;" role="presentation">' +
                        // get started row
                        '<tr {{?!it.includeGetStarted}}style="display: none;"{{?}} class="get_started_row">' +
                            '<td align=center colspan=2>' +
                                '<div class="clickable get_started" tabindex="0" role="button">{{=it.getStartedText}}</div>' +
                            '</td>' +
                        '</tr>' +

                        // reply row
                        '<tr {{?it.includeGetStarted}}style="display: none;"{{?}} class="reply_row">' +
                            // optional mode selector                            
                            '<td class="ModeSelector" valign="center"></td>' +

                            '<td><div class="persistent_menu_cell"></div></td>' +
                            '<td><div class="upload_menu_cell"></div></td>' +
                            '<td class="text_container" valign=middle>' +
                                '<textarea rows=1 class="reply_text clickableADA" placeholder="{{=it.replyTextPlaceholder}}" tabindex="0" contenteditable="true" aria-label="{{=it.replyTextPlaceholder}}"></textarea>' +
                            '</td>' +
                            '{{?it.allowMicrophone}}' +
                                '<td class="speech_container" valign=middle>' +
                                    '<i class="speech si-mic_on" title="{{= _i(\"Click and hold to enter speech directly to bot\")}}C"><i/>' +
                                '</td>' +
                            '{{?}}' +
                            '<td class="send_container" valign=middle>' +
                                '<button class="btn btn-default send" type="submit" role="button" tabindex="0">{{=it.sendMessageButtonText}}</button>' +
                            '</td>' +                            
                        '</tr>' +

                    '</table>' +                    

                    '<div class="spinner" style="display: none;"></div>' +
                '</div>',

            modeSelector: 
                '{{~it.modeSelector.modes :mode:index}}' +
                    '<span class="clickable Mode" data-index="{{=index}}" data-value="{{=mode.value}}">' +
                        '<i class="clickable {{=mode.icon}}" title="{{=mode.label}}"></i> ' +

                        // notification dot
                        '<span class="DotHighlight" style="display:none;"></span>' +

                        '<span class="SelectedBorder"></span>' +
                    '</span>' +
                '{{~}}' +
                // vertical bar between mode buttons and text actions
                '{{?it.modeSelector.modes.length > 0}}' +
                    '<span class="VerticalSeparator"></span>' +
                '{{?}}',    

            persistentMenu:
                '{{?it.persistentMenu != null && it.persistentMenu.actions.length > 0}}' +
                    '<div class="btn-group dropup caret_up persistent_menu">' +
                        '<button type="button" class="open_persistent_menu dropdown-toggle clickableADA" data-toggle="dropdown" tabindex="0" aria-label="Menu, press enter to open" role="button" aria-expanded="false">' +
                            '<i class="si-textblock" aria-hidden="true"></i>' +                                    
                        '</button>' +
                        '<ul class="dropdown_caret dropdown-menu">' +
                            '{{~it.persistentMenu.actions :menuItem:index}}' +
                               '{{?menuItem.type == "postback"}}' +
                                   '<li class="clickable"><a class="noselect persistent_menu_item postback_menu_item clickableADA" data-postback="{{=menuItem.value}}" tabindex="0" data_analyticsType="Persistant Menu Click">{{=menuItem.label}}</a></li>' +
                               '{{??}}' +
                                   '<li class="clickable"><a class="noselect persistent_menu_item clickableADA" href="{{=menuItem.value}}" target="_blank" tabindex="0"  data_analyticsType="Persistant Menu Click">{{=menuItem.label}}</a></li>' +
                               '{{?}}' +
                            '{{~}}' +
                        '</ul>' +
                    '</div>' +
                '{{?}}',

            uploadMenu:
                '<div class="btn-group dropup caret_up upload_menu">' +
                    '<button type="button" class="open_upload_menu dropdown-toggle clickableADA" data-toggle="dropdown" tabindex="0" aria-label="Upload, press enter to open" role="button" aria-expanded="false">' +
                        '<i class="si-attachment1"></i>' +                                    
                    '</button>' +
                    '<ul class="dropdown_caret dropdown-menu">' +
                        '<input type="file" class="image_input" accept=".png, .gif, .jpg, .jpeg" style="display:none;" aria-hidden="true"></input>' +                
                        '<li class="clickable"><a class="noselect upload_menu_item image_menu_item clickableADA" tabindex="0">{{=it.imageText}}</a></li>' +
                        '<input type="file" class="file_input" accept=".txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx" style="display:none;" aria-hidden="true"></input>' +                
                        '<li class="clickable"><a class="noselect upload_menu_item file_menu_item clickableADA" tabindex="0">{{=it.fileText}}</a></li>' +
                    '</ul>' +
                '</div>'
        };

        _factoryData.templateDefinitionsv2 = {
            control:
                '<div class="chat v2">' +
                    '<div class="header"></div>' +

                    '<div class="Conversation nano" style="height: {{=it.convoHeight - 20}}px;">' +
                        '<div class="content">' +

                            '<div class="jump clickableADA" data-source="begin" data-target="end" tabindex="0" aria-label="Jump to end of conversation" role="Button">Jump to end of conversation</div>' +

                            '<div class="message_content">' +
                            '</div>' +

                            '<div class="jump clickableADA" data-source="end" data-target="begin" tabindex="0" aria-label="Jump to the beginning of conversation" role="Button">Jump to the beginning of conversation</div>' +

                        '</div>' +
                    '</div>' +

                    '<div class="AlternateContainer nano" style="height: {{=it.convoHeight}}px; display: none;">' +
                        '<div class="content"></div>' +
                    '</div>' +

                '<div class="auto_complete_widget_container" style="height: 0px;">' +
                '</div>' +

                '<table class="v2 reply_table" role="presentation">' +                        

                        // get started row
                        '<tr {{?!it.includeGetStarted}}style="display: none;"{{?}} class="get_started_row">' +
                            '<td align=center colspan=2>' +
                                '<button class="get_started" tabindex="0">{{=it.getStartedText}}</button>' +
                            '</td>' +
                        '</tr>' +

                        // reply row
                        '<tr {{?it.includeGetStarted}}style="display: none;"{{?}} class="reply_row">' +
                            // optional mode selector                            
                            '<td class="ModeSelector" valign="center"></td>' +
                            '<td class="text_container" valign=middle>' +
                                '<label for="reply_text" class="srOnly">{{=it.replyTextPlaceholder}}</label>' +
                                '<textarea id="reply_text" rows=1 class="reply_text" placeholder="{{=it.replyTextPlaceholder}}" contenteditable="true"></textarea>' +
                                '<div class="attachment_container" style="display: none;">' +
                                    '<div class="attachment_name_image_container">' +
                                        '<div class="attachment_name_image_flexer">' +
                                            '<div class="attachment_image_container">' +
                                                '<img class="attachment_image" src=""></img>' +
                                                '<div class="file_image" style="display: none;"><i class="si-attachment1"></i></div>' +
                                            '</div>' +
                                            '<div class="attachment_name"></div>' +
                                        '</div>' +
                                    '</div>' +
                                    '<div class="attachment_remove clickable clickableADA"  tabindex="0">' +
                                        '<svg class="" tabindex="0" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">' +
                                            '<path d="M12 4L4 12M4.00003 4L12 12" stroke="#697077" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
                                        '</svg>' +
                                    '</div>' +
                                '</div>' +
                            '</td>' +
                            '{{?it.allowMicrophone}}' +
                            '<td class="speech_container" valign=middle>' +
                                '<i class="speech si-mic_on" title="{{= _i(\"Click and hold to enter speech directly to bot\")}}C"><i/>' +
                            '</td>' +
                            '{{?}}' +
                            '<td><div class="upload_menu_cell"></div></td>' +
                            '<td><div class="persistent_menu_cell"></div></td>' +
                            '<td class="send_container" valign=bottom>' +
                                '<button id="submit_button" class="btn btn-primary send" type="submit" style="display:none;" aria-label="{{=it.sendMessageButtonText}}">' +
                                    '<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">' +
                                        '<path fill-rule="evenodd" clip-rule="evenodd" d="M3.49576 20.8264L19.4888 11.8666C20.169 11.4855 20.1707 10.5072 19.4918 10.1237L3.49285 1.08803C2.74361 0.664878 1.84531 1.32294 2.02285 2.1649L3.72767 10.25L10.3168 10.25C10.731 10.25 11.0668 10.5858 11.0668 11C11.0668 11.4142 10.731 11.75 10.3168 11.75L3.73452 11.75L2.02924 19.7456C1.84987 20.5866 2.74553 21.2467 3.49576 20.8264Z" fill="white" />' +
                                    '</svg>' +
                                    '</i>' +
                                '</button>' +
                            '</td>' +                            
                        '</tr>' +                        
                    '</table>' +

                    '<table class="overlay_panel_table" style="display: none;">' +
                        '<tr>' +                     
                            '<td class="spacer"></td>' +
                            '<td class="overlay_panel_button_container one_button" valign=bottom width="*">' +
                                '<div class="overlay_panel_button cancel" role="button" tabindex="0">{{= _i(\"Cancel\")}}</div>' +                                    
                            '</td>' +
                            '<td class="spacer_small two_buttons"></td>' +
                            '<td class="overlay_panel_button_container two_buttons" valign=bottom width="*">' +
                                '<div class="overlay_panel_button submit" role="button" tabindex="0">{{= _i(\"Submit\")}}</div>' +
                            '</td>' +                
                            '<td class="spacer"></td>' +
                        '</tr>' +
                    '</table>' +

                    '<div class="spinner" style="display: none;"></div>' +
                '</div>',

            modeSelector:
                '{{~it.modeSelector.modes :mode:index}}' +
                '<span class="clickable Mode" data-index="{{=index}}" data-value="{{=mode.value}}">' +
                    '<i class="clickable {{=mode.icon}}" title="{{=mode.label}}"></i> ' +

                    // notification dot
                    '<span class="DotHighlight" style="display:none;"></span>' +

                    '<span class="SelectedBorder"></span>' +
                '</span>' +
                '{{~}}' +
                // vertical bar between mode buttons and text actions
                '{{?it.modeSelector.modes.length > 0}}' +
                '<span class="VerticalSeparator"></span>' +
                '{{?}}',

            persistentMenu:
                '{{?it.persistentMenu != null && it.persistentMenu.actions.length > 0}}' +
                '<div class="btn-group dropup caret_up persistent_menu">' +
                    '<button type="button" class="open_persistent_menu dropdown-toggle" data-toggle="dropdown" tabindex="0" aria-label="Menu, press enter to open" role="button" aria-expanded="false">' +
                        '<i class="si-textblock" aria-hidden="true"></i>' +
                    '</button>' +
                    '<ul class="dropdown_caret dropdown-menu pull-right">' +
                        '{{~it.persistentMenu.actions :menuItem:index}}' +
                            '{{?menuItem.type == "postback"}}' +
                                '<li class="clickable"><a class="noselect persistent_menu_item postback_menu_item clickableADA" data-postback="{{=menuItem.value}}" tabindex="0" data_analyticsType="Persistant Menu Click">{{=menuItem.label}}</a></li>' +
                            '{{??}}' +
                                '<li class="clickable"><a class="noselect persistent_menu_item clickableADA" href="{{=menuItem.value}}" target="_blank" tabindex="0"  data_analyticsType="Persistant Menu Click">{{=menuItem.label}}</a></li>' +
                            '{{?}}' +
                        '{{~}}' +
                    '</ul>' +
                '</div>' +
                '{{?}}',

            uploadMenu:
                '<div class="upload_menu">' +
                    '<button type="button" class="open_upload_dialog dropdown-toggle" data-toggle="dropdown" tabindex="0" aria-label="Upload, press enter to open" role="button" aria-expanded="false">' +
                        '<i class="si-attachment1"></i>' +
                    '</button>' +         
                    '<input type="file" class="file_image_input" accept=".png, .gif, .jpg, .jpeg, .txt, .csv, .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx" style="display:none;" aria-hidden="true"></input>' +        
                '</div>'
        };

        // auto doT templates
        $.each(_factoryData.templateDefinitionsv1, function (key, templateDefinition) {
            _factoryData.templateInstancesv1[key] = doT.template(templateDefinition);
        });

        $.each(_factoryData.templateDefinitionsv2, function (key, templateDefinition) {
            _factoryData.templateInstancesv2[key] = doT.template(templateDefinition);
        });

        _factoryData.isInitialized = true;
    }


    // wrap all factory/one-time data in  a single object so we only have to punch one hole in interface to instance code to use
    var _factoryData = {
        isInitialized: false,
        templateDefinitions: {},
        templateInstancesv1: {},
        templateInstancesv2: {}
    };

    return _factoryInterface;
}());





// per-instance junk
TestBotWidgetFactory.createInstance = function(options) {
    var _interface =
    {
        onHidden: function() {},
        onShown: function() {},
        resize: function() {}
    };
    
    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);

    var _messengerVersion = "v1";
    if (_options.accountSettings != null && _options.accountSettings.style != null) {
        _messengerVersion = _options.accountSettings.style
    }

    // the conversation service id parameter is only valid on the 'app channel' platform
    if (_options.platform != 'App Channel' && _options.platform != 'Web Chat') _options.conversationServiceId = '';

    var _isSending = false;

    var _conversationId = '';
    var _isTest = false;
    var _showAnnotations = false;
    var _conversationServiceId = _options.conversationServiceId;
    var _conversationToken = '';
    var _messages = [];

    // ---------------
    
    var _conversationWidget, _realtimeHubClient, _autoCompleteWidget = null;
    var _hubClientId = null;
    var _subscribedId = null;
    var _realtimeConnected = false;
    var _realtimeSubscribed = false;
    var _detailPoller = null;
    var _pageNewerUrl = null;

    var _annotations = [];

    var _annotationIdCtr = -1;
    var _getStartedClicked = false;
    var _lastReplyHeight = null;
    var _maxMessageIdCtr;
    var _recordingAudio = false;
    var _recorder = null;
    var _autoCompleteSelection;
    
    var _conversationRecordExists = false;
    var _conversationHasMessages = false;

    var _isTyping = false;
    var _stopTypingTimeout = null, _startTypingTimeout = null;

    var _readiedAttachment = null;

    var _possibleResizeFromInput = false;

    var _screenReaderMode = true;

    if (!_options.forceNewSession && _conversationServiceId == '' && _options.persistConversationAcrossSession) {
        _conversationServiceId = tryLoadConversationServiceId(_conversationServiceId);
    } else {
        // if we are not persisting conversation across session calls/pageloads, try to wipe whatever conv service id we have saved so
        // if client ever turns persistence back on, they'll get a fresh convo
        tryStoreConversationServiceId('');
    }
    if (_conversationServiceId == null) _conversationServiceId = '';

    var _apiBase = '';

    window.addEventListener("message", function (event) {
        if (event != null) {
            if (event.data != null) {
                if (event.data.type != null) {
                    if (event.data.type == "toggleAnnotations") {
                        _showAnnotations = !_showAnnotations;
                        setData();
                    }
                }
            }
        }
    });

    function sendConversationIdToTester(conversationId) {
        if (_isTest) {
            window.parent.postMessage({ 
                type: 'conversationId', 
                data: { conversationId: conversationId } 
            }, '*');
        }
    }

    function init() {        
        $(_options.container).on("mousemove", function (e) {
            _screenReaderMode = false;
        });

        $(_options.container).on('keydown', function (e) {
            var tabPressed = e.keyCode == 9; // tab
            if (tabPressed) {
                _screenReaderMode = true;
            }
        });

        if (_messengerVersion == "v2") {
            _options.replyHeightPixels = 49;
        }

        // figure out what our api endpoint is
        {
           _apiBase = $('#botsApiEndpoint').val();
            console.log('Api base is: ' + _apiBase);
        }

        if (_conversationServiceId != '') {
            console.log('trying to load specified conversation');
            $.when(        
                getConversation(_options.botAccountServiceId, _conversationServiceId)
            ).done(function(r0) {
                var gotValidResponse = (r0.data != '');
                if (gotValidResponse) {
                    console.log('got existing conversation');       
                    _conversationRecordExists = true;
                    _conversationId = r0.data.conversationId;
                    _isTest = r0.data.isTest;
                    sendConversationIdToTester(_conversationId);
                    analyticsEvent('ConversationCreated');
                    _conversationToken = r0.data.realtimeToken;
                    _conversationHasMessages = (r0.data.messages.length > 0);
                    _getStartedClicked = (r0.data.messages.length > 0);
                    if (_options.updateContextJson != null && _options.updateContextJson.length > 0) {
                        $.when(
                            updateConversationContext(_options.botAccountServiceId, r0.data.conversationId, _options.updateContextJson, 'merge')
                        ).done(function (r1) {
                            if (r1.success) {
                                draw();
                                drawUploadMenu();
                                processConversationMessagesResult(r0.data);

                                var language = '';
                                if (r1.data.context != null) {
                                    var context = JSON.parse(r1.data.context);
                                    if (context != null && context.customData != null && context.customData.userInfo != null && context.customData.userInfo.language != null) {
                                        language = context.customData.userInfo.language;
                                    }
                                }                                
                                onLanguageChanged({ language: language });
                            }
                        });
                    } else {
                        draw();
                        drawUploadMenu();
                        processConversationMessagesResult(r0.data);
                    }
                } else {
                    console.log('did not get existing conversation');
                    _getStartedClicked = false;
                    draw();
                }
            });
        } else {
            draw();
        }
    }   

    function getInitialLanguage() {
        var initialLanguage = '';

        if (_options.browserSettings != null && _options.browserSettings.language != null) {
            initialLanguage = _options.browserSettings.language;
        }

        if (_options.createContextJson != null && _options.createContextJson.length > 0) {
            var createContext = JSON.parse(_options.createContextJson);
            
            var match = _u.find(createContext, function (x) { return x.userInfo != null && x.userInfo.language != null; });
            if (match != null) {
                initialLanguage = match.userInfo.language; 
            }
        }

        return initialLanguage;
    }


    // -----------------
    var _alternateMode = {
        widget: null
    };
    var _messageIdThatTriggeredVideoModeSelect = null;

    function syncModeSelector() {
        var callStartedEvent = _conversationWidget.getActiveCallStartedEvent(_messages);

        var currentMode = $(_options.container).find('.ModeSelector .Mode.selected').attr('data-value');
        var chatIsNotified = $(_options.container).find('.Mode[data-value="chat"] .DotHighlight').is(':visible');        
        var callIsNotified = $(_options.container).find('.Mode[data-value="call"] .DotHighlight').is(':visible');
        if (typeof currentMode == 'undefined' || callStartedEvent == null) currentMode = 'chat';

        //var modeSelectorEnabled = (callS;
        var modeSelectorSettings = {
            enabled: (callStartedEvent != null),
            currentMode: currentMode,
            modes: [
                { value: "chat", icon: "si-messages", label: "Chat" },
                { value: "call", icon: "si-voice", label: "Call" }
            ]
        };

        $(_options.container).find('.ModeSelector').html(_factoryData['templateInstances' + _messengerVersion].modeSelector({
            modeSelector: modeSelectorSettings
        }));  
        $(_options.container).find('.ModeSelector').toggle(modeSelectorSettings.enabled);

        $(_options.container).find('.Mode[data-value="chat"] .DotHighlight').toggle(chatIsNotified);
        $(_options.container).find('.Mode[data-value="call"] .DotHighlight').toggle(callIsNotified);

        $(_options.container).find('.ModeSelector .Mode').click(function(e) {
            var index = parseInt($(this).attr('data-index'));            
            var mode = modeSelectorSettings.modes[index];
            setMode(mode.value);    
        });

        // when a new call event arrives, automatically switch user over to call UI
        if (modeSelectorSettings.enabled && (_messageIdThatTriggeredVideoModeSelect != callStartedEvent.id)) {
            modeSelectorSettings.currentMode = 'call';
            _messageIdThatTriggeredVideoModeSelect = callStartedEvent.id;
        }

        setMode(modeSelectorSettings.currentMode);        

        // tear down alternate widget
        if (!modeSelectorSettings.enabled) {
            if (_alternateMode.widget != null) {
                _alternateMode.widget.destroy();
                _alternateMode.widget = null;
            }
        }
    }

    
    function setMode(mode) {
        $(_options.container).find('.ModeSelector .Mode').removeClass('selected');
        $(_options.container).find('.ModeSelector .Mode[data-value="' + mode + '"]').addClass('selected');
        switch (mode) {
            case 'chat':
                // chat widget is standard/built-in here
                $(_options.container).find('.Conversation').show();
                $(_options.container).find('.AlternateContainer').hide();
                _conversationWidget.onShown();
                break;

            case 'call':
                // other widgets are customizable by widget. e.g. "lobby", "multimedia call"
                $(_options.container).find('.Conversation').hide();
                $(_options.container).find('.AlternateContainer').show();

                if (_alternateMode.widget == null) {
                    //var lobbyState = {
                    //    audioEnabled: true,
                    //    videoEnabled: true
                    //};
                    var lobbyState = null;

                    var callStartedEvent = _conversationWidget.getActiveCallStartedEvent(_messages);                                        
                    var audioOnly = (callStartedEvent.content.context.multimediaType == 1);                    

                    _alternateMode.widget = MultimediaCallWidgetFactory.createInstance({
                        callStartedEvent: _conversationWidget.getActiveCallStartedEvent(_messages),
                        container: $(_options.container).find('.AlternateContainer'),
                        userInfo: null,
                        videoAllowed: !audioOnly,
                        isUser: true,
                        lobbyState: lobbyState,
                        sendMessageFn: function(conversationId, type, content) { 
                            sendMessage(type, content);
                        },
                        callbacks: {
                            onCallStateChanged: function(isActive) {
                                $(_options.container).find('.Mode[data-value="call"] .DotHighlight').toggle(isActive);
                            },
                            onCallEnded: function (callId) {
                                syncModeSelector();
                            },
                            onUserConnectedStateChanged: function (callId, isConnected) {
                                setConversationMultimediaCallConnected(callId, isConnected);
                            },
                        }
                    });
                }                
                break;
        }
        $(_options.container).find('.nano').nanoScroller();
    }

    function onUnviewedMessagesChanged(unviewedMessages) {
        // ignore all events.  definitely need to at least ignore "is typing" events
        var hasUnreadMessagesFromOtherUser = typeof _u.find(unviewedMessages, function(o) { return o.fromBot && o.contentType != 10; }) != 'undefined';       
        $(_options.container).find('.Mode[data-value="chat"] .DotHighlight').toggle(hasUnreadMessagesFromOtherUser);
    }

    // ----------

    function draw() {
        // if we are re-entering a conversation dont display "get started" again, and don't apply any "initial user messages"
        if (_conversationHasMessages) {
            _getStartedClicked = true;
            _options.initialUserMessage = null;
        }        


        // map "auto get started" to an "initial user message", if get started button is enabled and "initial user message" is not set to anything
        var includeGetStarted = _options.channelSettings != null && _options.channelSettings.getStarted != null && _options.channelSettings.getStarted.enabled;

        var autoGetStartedFromLauncher = _options.autoGetStarted;
        var autoGetStartedFromChannel = _options.accountSettings != null && _options.accountSettings.autoGetStarted != null && _options.accountSettings.autoGetStarted;
        if ((autoGetStartedFromLauncher || autoGetStartedFromChannel) && !_getStartedClicked && _options.initialUserMessage == null) {
            _options.initialUserMessage = { type: 'event', content: { type: 0 } };
        }

        _options.height = $(_options.container).height();

        var initialLanguage = getInitialLanguage();

       
        var getStartedText = _i('Get Started');
        if (includeGetStarted && _options.channelSettings.getStarted.texts != null) {
            var getStartedTextInfo = null;

            var match = _u.find(_options.channelSettings.getStarted.texts, function (text) { return text.language.toLowerCase() == (initialLanguage || '').toLowerCase(); });
            if (match != null) {
                getStartedTextInfo = match;
            } else {
                match = _u.find(_options.channelSettings.getStarted.texts, function (text) { return text.language == ''; });
                if (match != null) {
                    getStartedTextInfo = match;
                }
            }

            if (getStartedTextInfo != null) {
                getStartedText = getStartedTextInfo.text;
            }
        }        

        var replyTextPlaceholder = _i('Type message here...');
        if (_options.accountSettings != null && typeof _options.accountSettings.replyTextPlaceholder != 'undefined' && _options.accountSettings.replyTextPlaceholder != '' && _options.accountSettings.replyTextPlaceholder != null) {
            replyTextPlaceholder = _options.accountSettings.replyTextPlaceholder;
        }

        var sendMessageButtonText = _i('Send');
        if (_options.accountSettings != null && typeof _options.accountSettings.sendMessageButtonText != 'undefined' && _options.accountSettings.sendMessageButtonText != '' && _options.accountSettings.sendMessageButtonText != null) {
            sendMessageButtonText = _options.accountSettings.sendMessageButtonText;
        }

        var uiModel = {
            fullHeight: _options.height,
            convoHeight: getConvoHeight(),
            replyHeight: getReplyHeight(),            
            includeGetStarted: includeGetStarted && !_getStartedClicked,
            getStartedText: getStartedText,
            replyTextPlaceholder: replyTextPlaceholder,
            sendMessageButtonText: sendMessageButtonText,
            allowMicrophone: _options.allowMicrophone
        };

        // note: it seems like its important to leave the "form" element inside the "body" in the DOM on android, else the keyboard wont work properly (obviously)
        $(_options.container).append(_factoryData['templateInstances' + _messengerVersion].control(uiModel));

        var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);            
        if (isMobile) {
            var conversationScroller = $(_options.container).find('.Conversation.nano');
            var conversationScrollerContent = $(_options.container).find('.Conversation.nano > .content');
            var messageContent = $(_options.container).find('.message_content');

            $(conversationScrollerContent).addClass('pushDown');
            var checkScrollerHeight = function () {
                var conversationScrollerHeight = $(conversationScroller).height();
                var messageContentHeight = messageContent.height();

                if (conversationScrollerHeight > messageContentHeight) {
                    setTimeout(checkScrollerHeight, 100);
                } else {
                    $(conversationScrollerContent).removeClass('pushDown');
                    $(conversationScroller).nanoScroller();
                    $(conversationScroller).nanoScroller({ scroll: 'bottom' });
                }
            };
            setTimeout(checkScrollerHeight, 100);
        }
        

        onLanguageChanged(initialLanguage);
        drawUploadMenu();

        // reply text keyhandling nonsense
        $(_options.container).find('.reply_text')
            .on('input', function (e) {
                this.style.height = "5px";
                this.style.height = (this.scrollHeight) + "px";
                onHeightMaybeChanged();

                var value = $(this).val();
                if (value != null && value != '') {
                    $(_options.container).find('.send').prop('disabled', false);
                    if (_messengerVersion == "v2") {
                        $(_options.container).find('.send').show();
                        $(_options.container).find('.upload_menu_cell').hide();
                        $(_options.container).find('.persistent_menu_cell').hide();
                    }

                } else {
                    $(_options.container).find('.send').prop('disabled', true);
                    if (_messengerVersion == "v2") {
                        $(_options.container).find('.send').hide();
                        $(_options.container).find('.upload_menu_cell').show();
                        if ($(_options.container).find('.persistent_menu').length > 0) {
                            $(_options.container).find('.persistent_menu_cell').show();
                        }
                    }

                }
            })
            .on('keydown', function (e) {
                var selectUp = e.keyCode == 38;
                if (selectUp) {
                    _autoCompleteWidget.selectUp();
                    e.preventDefault();
                    return false;
                }

                var selectDown = e.keyCode == 40;
                if (selectDown) {
                    _autoCompleteWidget.selectDown();
                    e.preventDefault();
                    return false;
                }

                var selectNone = e.keyCode == 27;
                if (selectNone) {
                    _autoCompleteWidget.selectNone();
                    e.preventDefault();
                    return false;
                }

                var sendReply = (e.keyCode == 13 && !e.shiftKey);
                if (sendReply) return false;
            })
            .on('keyup', function (e) {
                var replyText = $(_options.container).find('.reply_text').val();
                var sendReply = (e.keyCode == 13 && !e.shiftKey);
                if (sendReply && !_isSending) {
                    $(_options.container).find('.send').prop('disabled', true);
                    if (_messengerVersion == "v2") {
                        $(_options.container).find('.attachment_container').hide();
                        $(_options.container).find('.reply_text').show();

                        $(_options.container).find('.send').hide();
                        $(_options.container).find('.upload_menu_cell').show();
                        if ($(_options.container).find('.persistent_menu').length > 0) {
                            $(_options.container).find('.persistent_menu_cell').show();
                        }
                    }

                    if (_autoCompleteSelection != null) {
                        replyText = _autoCompleteSelection;
                        _autoCompleteSelection = null;
                    }
                    var postback = '';
                    $(_options.container).find('.reply_text')
                        .val('')
                        .css('height', '');
                    _autoCompleteWidget.userTyped('');
                    if (replyText.trim() != '') {
                        sendMessage('text', JSON.stringify({ speech: replyText, postback: postback }));
                        stopTyping();
                    }
                    return false;
                } else {
                    _autoCompleteWidget.userTyped(replyText);

                    var typingNow = replyText != '';
                    if (typingNow) {
                        // wait a little bit before sending a "is typing" message in case user is typing message rapidly
                        if (_startTypingTimeout == null) {
                            _startTypingTimeout = setTimeout(startTyping, 1000);
                        }

                        // clear any pending "stop typing" events from happening
                        if (_stopTypingTimeout != null) {
                            clearTimeout(_stopTypingTimeout);
                            _stopTypingTimeout = null;
                        }
                    }
                }
            })
            .on('focus', function (e) {
                _possibleResizeFromInput = true;
                $(this).closest('table.v2').toggleClass('input_focused', true);
            })
            .on('blur', function (e) {
                $(this).closest('table.v2').toggleClass('input_focused', false);
            });

        onHeightMaybeChanged();

        // IE hack - if using IE, placeholder text disappears when input has the focus, so its better usability to not have initial focus in that case
        var isIE = (window.navigator.userAgent.indexOf("MSIE ") >= 0);        
        var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
        if (!isIE && !isIE11) $(_options.container).find('.reply_text').focus();

        $(_options.container).find('.speech').on('mousedown', function (e) {
            var target = $(e.target);

            if (_recorder == null) {
                _recorder = MonoWAVRecorderFactory.createInstance({
                    maxRecordingSeconds: 10,
                    callbacks: {
                        onBlobRecorded: function (blob) {
                            var reader = new window.FileReader();
                            reader.readAsDataURL(blob);
                            reader.onloadend = function () {
                                var base64 = reader.result;
                                var base64 = base64.split(',')[1];
                                var postback = '';
                                sendMessage('audio', JSON.stringify({ speech: base64, postback: postback }));
                                stopTyping();
                            }
                        },
                        onError: function () {
                            _recordingAudio = false;
                            $(e.target).removeClass('recording');
                        }
                    }
                });
            }
            _recorder.start();
            _recordingAudio = true;
            $(e.target).addClass('recording');

            e.preventDefault();
        }).on('mouseup', function (e) {
            var target = $(e.target);

            _recordingAudio = false;
            $(e.target).removeClass('recording');
            if (_recorder == null) {
                return;
            }
            _recorder.stop();

            e.preventDefault();
        }).on('mouseleave', function (e) {
            var target = $(e.target);

            _recordingAudio = false;
            $(e.target).removeClass('recording');
            if (_recorder == null) {
                return;                
            }
            _recorder.cancel();

            e.preventDefault();
        });

        $(_options.container).find('.send').click(function() {
            var replyText = $(_options.container).find('.reply_text').val();
            var postback = '';
            if (_messengerVersion == "v2") {
                $(_options.container).find('.attachment_container').hide();
                $(_options.container).find('.reply_text').show();

                $(_options.container).find('.send').hide();
                $(_options.container).find('.upload_menu_cell').show();
                if ($(_options.container).find('.persistent_menu').length > 0) {
                    $(_options.container).find('.persistent_menu_cell').show();
                }
            }
            
            $(_options.container).find('.reply_text')
                .val('')
                .css('height', '');
            _autoCompleteWidget.userTyped('');
            if (replyText.trim() != '') {
                if (replyText == '[attachment]' && _readiedAttachment != null) {
                    sendAttachment(_readiedAttachment.filename, _readiedAttachment.base64, _readiedAttachment.mimeType, _readiedAttachment.sendMessageFn);
                    _readiedAttachment = null;
                } else {
                    sendMessage('text', JSON.stringify({ speech: replyText, postback: postback }));
                    if ($(this).is(":focus")) {
                        console.log("button is focued!");
                        $(_options.container).find('.reply_text').focus();
                    }
                }
            }
            onHeightMaybeChanged();

            stopTyping();
            
        });

        $(_options.container).find('.get_started')
            .click(function() {
                _getStartedClicked = true;
                sendMessage('event', JSON.stringify({ type: 0 })); // event type 0 = GET_STARTED
                stopTyping();

                removeGetStartedButton();
            })
            .on('keydown', function (e) {
                // pressing enter or space = click button
                if (e.keyCode == 13 || e.keyCode == 32) {
                    e.preventDefault();
                    $(e.target).click();
                }
            })
            .focus();


        $(_options.container).find('.jump')
            .click(function(e) {
                var target = $(e.target).attr('data-target');
                $(_options.container).find('.jump[data-source="' + target + '"]').focus();
            })
            .on('keyup', function(e) {
                if (e.keyCode === 13 || e.keyCode == 32) { 
                    e.preventDefault(); 
                    $(e.target).click();
                }
            });                    

        drawConversationWidget();

        // send initial user message (if any).  this could be a "get started" event        
        if (_options.initialUserMessage != null) {
            sendMessage(_options.initialUserMessage.type, JSON.stringify(_options.initialUserMessage.content));
            
            // remove get started button from ui (if any)
            removeGetStartedButton();
        }     

        var textAreaHeight = $(_options.container).find('.chat > table').height() + 1;
        if (_messengerVersion == "v2") {
            textAreaHeight = $(_options.container).find('.chat .v2 .reply_table').height() + 1;
        }

        _autoCompleteWidget = AutoCompleteWidgetFactory.createInstance({
            container: $(_options.container).find('.auto_complete_widget_container'),
            textAreaHeight: textAreaHeight,
            callbacks: {
                onGetData: function (utterance) {
                    if (_conversationId == null || _conversationId == '' || _options.botAccountServiceId == '') {
                        _autoCompleteWidget.setData([]);
                        return;
                    }
                    $.when(
                        getAutoComplete(_options.botAccountServiceId, _conversationId, utterance)
                    ).done(function (r0) {
                        if (r0.success) {
                            if (r0.data != null) {
                                _autoCompleteWidget.setData(r0.data.suggestions);
                            } else {
                                _autoCompleteWidget.setData([]);
                            }
                        }
                    });
                },
                onSuggestionTaken: function (suggestion) {
                    var postback = '';
                    $(_options.container).find('.reply_text').val('');
                    sendMessage('text', JSON.stringify({ speech: suggestion, postback: postback }));
                    stopTyping();
                    _autoCompleteWidget.userTyped('');                    
                },
                onSelectionChanged: function (selection) {
                    _autoCompleteSelection = selection;
                }
            }
        });

        syncModeSelector();                
    }    


    function removeGetStartedButton() {
        $(_options.container).find('.get_started_row').hide();
        $(_options.container).find('.reply_row').show();
        $(_options.container).find('.reply_text').focus();
    }


    function stopTyping() {
        if (_isTyping) {
            _isTyping = false;
            sendMessage('event', JSON.stringify({ type: 13, context: JSON.stringify({ on: false }) }));
        }

        // clear any pending "start typing" requests
        if (_startTypingTimeout != null) {
            clearTimeout(_startTypingTimeout);
            _startTypingTimeout = null;
        }
    }

    function startTyping() {
        if (!_isTyping) {
            _isTyping = true;
            sendMessage('event', JSON.stringify({ type: 13, context: JSON.stringify({ on: true }) }));
        }

        // reset "stop typing" event
        if (_stopTypingTimeout != null) {
            clearTimeout(_stopTypingTimeout);
        }
        _stopTypingTimeout = setTimeout(stopTyping, 2000);
        
        // clear "start typing" event
        _startTypingTimeout = null;
    }

    function onHeightMaybeChanged() {
        // if reply height changed, update convo height accordingly
        var currentReplyHeight = $(_options.container).find('.chat > table').height();
        if (currentReplyHeight == _lastReplyHeight) return;
        _lastReplyHeight = currentReplyHeight;
        $(_options.container).find('.chat > .nano').height(getConvoHeight());
    }

    function getConvoHeight() {
        var titleHeight = 0;
        var replyHeight = getReplyHeight();
        var borderHeight = 1;
        var headerHeight = $(_options.container).find('.header').height();
        if (typeof headerHeight == 'undefined') headerHeight = 0;
        return _options.height - titleHeight - replyHeight - borderHeight - headerHeight;
    }

    function getReplyHeight() {     
        var replyHeight = _options.replyHeightPixels;
        var currentReplyHeight = $(_options.container).find('.chat > table').height();

        if (typeof currentReplyHeight != 'undefined') {
            if (currentReplyHeight > replyHeight) replyHeight = currentReplyHeight;
        }

        return replyHeight;
    }

    function getContextHeight() {
        var titleHeight = 80;
        var replyHeight = 0;
        return _options.height - titleHeight - replyHeight;
    }


    _interface.onHidden = function() {
        doReset();
        if (_detailPoller != null) {
            _detailPoller.stop()
        }
    }

    _interface.onShown = function() {
        // reset view to "conversation" (if user was on context ui and quit testing then resumed)
        if ($(_options.container).find('.toggle_context').hasClass('active')) {
            $(_options.container).find('.toggle_context').click();
        }
    }

    
    _interface.resize = function() {
        // important not to do a full redraw in this case because that will cause input focus hijinx which will make mobile experience on android unuable.  plus this is better anyway
        _options.height = window.visualViewport.height;


        $(_options.container).find('.chat .nano')
            .height(getConvoHeight())
            .nanoScroller({ tabIndex: -1 });        

        if (_possibleResizeFromInput) {
            $(_options.container).find('.chat .nano')
                .nanoScroller({ scroll: 'bottom', tabIndex: -1 });
        }
    };

    function doReset() {
        _conversationServiceId = '';
        _messages = [];
        _annotations = [];
        _annotationIdCtr = -1;
        setData();
        _pageNewerUrl = null;
        drawUploadMenu();
    }


    function drawConversationWidget() {
        var container = $(_options.container).find('.nano > .content > .message_content'); 

        _conversationWidget = BotConversationFactory.createInstance({
            container: container,
            showTypingIndicator: _options.accountSettings != null ? _options.accountSettings.showTypingIndicator : false,
            showTimestamps: _options.accountSettings != null ? _options.accountSettings.showTimestamps : false,
            messengerStyle: _messengerVersion,
            drawEvents: _isTest && _showAnnotations,
            callbacks: {
                onDrawn: function (shouldScroll) {      
                    if (shouldScroll) {
                        $(_options.container).find('.nano')
                            .height(getConvoHeight())
                            .nanoScroller({ tabIndex: -1 });
                        $(_options.container).find('.nano').nanoScroller({ scroll: 'bottom', tabIndex: -1 });
                    }
                },
                onUserPostback: function(postback, label) {
                    $(_options.container).find('.reply_text').val('');
                    if (label.trim() != '') {
                        sendMessage('text', JSON.stringify({ speech: label, postback: postback }));
                        stopTyping();
                    }
                },
                onUserPostbacks: function (postbacks, label) {
                    $(_options.container).find('.reply_text').val('');
                    if (label.trim() != '') {
                        sendMessage('text', JSON.stringify({ speech: label, postbacks: postbacks }));
                        stopTyping();
                    }
                },
                onUserLocation: function (lat, long) {
                    sendMessage('location', JSON.stringify({ lat: lat, long: long }));
                    stopTyping();
                },
                onUserMessage: function (message) {
                    sendMessage('text', JSON.stringify({ speech: message, postback: '' }));
                    stopTyping();
                },
                onUserEvent: function (event) {
                    sendMessage('event', JSON.stringify(event));
                    stopTyping();
                },
                onMessageReaction: function (toMessageId, reactionType) {
                    sendMessage('reaction', JSON.stringify({ toMessageId: toMessageId, reactionType: reactionType }));
                    stopTyping();
                },        
                onOverlayPanelOpen: function (onCancel, onSubmit, buttonLabels) {
                    openOverlayPanel(onCancel, onSubmit, buttonLabels);
                },
                onOverlayPanelCanSubmitChanged: function (canSubmit) {
                    changeOverlayPanelCanSubmit(canSubmit);
                },
                onAdaLastMessagesUpdated: function () {
                    if (_screenReaderMode) {
                        var lastQuickReplyButton = $(container).find('.left_message:last() .quick_reply')[0];
                        if (lastQuickReplyButton != null) {
                            lastQuickReplyButton.focus();
                        }
                    }
                },
                onUnviewedMessagesChanged: onUnviewedMessagesChanged,
                analyticsEvent: analyticsEvent
            }
        });        
    }

    function openOverlayPanel(onCancel, onSubmit, buttonLabels) {
        $(_options.container).find('.reply_table').hide();
        $(_options.container).find('.overlay_panel_table').toggleClass('one_button', false);
        $(_options.container).find('.overlay_panel_table .one_button').attr('width', '*');

        if (buttonLabels != null) {
            if (buttonLabels.length > 0) {
                $(_options.container).find('.overlay_panel_table .cancel').html(buttonLabels[0]);
            }
            if (buttonLabels.length > 1) {
                $(_options.container).find('.overlay_panel_table .submit').html(buttonLabels[1]);
            }

            if (buttonLabels.length == 1) {
                $(_options.container).find('.overlay_panel_table .one_button').attr('width', '50%');
                $(_options.container).find('.overlay_panel_table').toggleClass('one_button', true);
            }
        }

        var overlayPanelTable = $(_options.container).find('.overlay_panel_table');
        $(overlayPanelTable).show();
        _interface.resize();

        $(_options.container).find('.cancel').off('click').on('click', function () {
            closeOverlayPanel();
            onCancel();
        });
        $(_options.container).find('.cancel').off('keydown').on('keydown', function (event) {
            if (!$(event.target).hasClass('disabled')) {
                if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                    $(event.target).click();

                    return false;
                }
            }
        });

        $(_options.container).find('.submit').off('click').on('click', function (e) {
            if (!$(e.target).hasClass('disabled')) {
                closeOverlayPanel();
                onSubmit();
            }
        });
        $(_options.container).find('.submit').off('keydown').on('keydown', function (event) {
            if (!$(event.target).hasClass('disabled')) {
                if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                    $(event.target).click();

                    return false;
                }
            }
        });
    }

    function changeOverlayPanelCanSubmit(canSubmit) {
        var submitButton = $(_options.container).find('.overlay_panel_table .submit');
        $(submitButton).toggleClass('disabled', !canSubmit);
    }

    function closeOverlayPanel() {
        $(_options.container).find('.overlay_panel_table').hide();
        $(_options.container).find('.reply_table').show();
    }

    function onMessageReceived(message) {     
        recordMessages([ message ], message.conversationId, null);      
    }

    function onModificationReceived(modification) {
        if (modification.type == 'annotation list') {
            var modifications = processAnnotationList(modification);
            $.each(modifications, function (index, annotation) {
                onModificationReceived(annotation);
            });
        } else {
            modification = processModification(modification);
            _annotations.push(modification);

            setData();
        }        
    }

    function setData() {
        if (_conversationWidget != null) {
            _conversationWidget.setIsTest(_isTest);

            _conversationWidget.setData({
                messages: _messages,
                modifications: _showAnnotations ? _annotations : []
            });
        }
    }

    function processModification(modification) {
        modification.id = _annotations.length;

        modification.submissionDate = new Date(modification.submissionDate);

        if (modification.appliedDate != null) {
            modification.appliedDate = new Date(modification.appliedDate);
        }

        modification.inputContext = JSON.parse(modification.inputContext);

        if (modification.type == 'assign') {
            var handler = _u.find(_options.handlers, function (o) { return o.id == modification.inputContext.fulfillmentHandlerId; });

            modification.inputContext.handlerLabel = 'unknown target';
            if (typeof handler != 'undefined') {
                modification.inputContext.handlerLabel = handler.type + ' target "' + handler.name + '"';
            }
        }

        if (modification.type == 'bot change') {
            // todo: look up bot name using id and refresh/sync the name (in case bot has since been renamed)
        }

        return modification;
    }

    function processAnnotationList(modification) {
        var outputContext = JSON.parse(modification.outputContext);

        var annotations = _u.map(outputContext, function (oc) {
            return {
                id: 0,
                submissionDate: new Date(oc.date),
                appliedDate: new Date(oc.date),
                inputContext: modification.inputContext,
                type: 'annotation',
                outputContext: JSON.stringify(oc)
            };
        });

        return annotations;
    }

    function processConversationMessage(message) {
        message.date = new Date(message.date);

        try {
            message.content = JSON.parse(message.content);
        } catch(err) {
            console.log("error: could not parse: " + message.content);
            message.contentType = 0;
            message.content = { speech: message.content };
        }

        try {
            message.context = JSON.parse(message.context);
        } catch (err) {
            console.log("error: could not parse: " + message.context);
        }

        if (message.context == null) {
            message.context = {
                allowableReactions: null
            };
        }

        if (message.context.allowableReactions == null) {
            message.context.allowableReactions = {
                reactionTypes: [],
                displayStyleType: 0
            };
        }
                
        // parse event contexts
        if (message.contentType == 10) {
            if (message.content.context != null) {
                try {
                    message.content.context = JSON.parse(message.content.context);
                } catch (e) {}
            }
        }

        message.fromBot = (message.fromServiceId == (_options.botAccountServiceId + ''));
    }


    function getContentType(contentType) {
        switch (contentType) {
            case 'text': return 0;
            case 'card': return 1;
            case 'quick_replies': return 2;
            case 'image': return 3;
            case 'custom_payload': return 4;
            case 'carousel': return 5;
            case 'location': return 8;
            case 'audio': return 9;
            case 'event': return 10;
        }
        return -1;
    }

    function tryStoreConversationServiceId(serviceId) {
        try {
            sessionStorage.setItem('AstuteBotConversationServiceId', serviceId);
        } catch (e) {}
    }


    function tryLoadConversationServiceId(defaultValue) {
        try {
            return sessionStorage.getItem('AstuteBotConversationServiceId');
        } catch (e) {}
        return defaultValue;
    }

    function sendMessage(contentType, content) {
        var isTypingIndicator = contentType == 'event' && JSON.parse(content).type == 13;
        var createConversationRecord = !_conversationRecordExists;
        if (createConversationRecord && isTypingIndicator) {
            return;
        }

        if (!isTypingIndicator) {
            if (_conversationWidget != null) {
                _conversationWidget.clearAdaLastMessages();
            }
        }

        // create conversation record if it does not already exist
        if (createConversationRecord) {
            // don't allow client to specify a conversation service id when creating a "web" conversation (only "app channels", which we disambiguate server-side)
            var conversationServiceIdToCreate = (_options.platform == 'App Channel') ? _conversationServiceId : null;

            $.when(
                createConversation(_options.botAccountServiceId, conversationServiceIdToCreate) // this should be "ba" + _options.botAccountServiceId, but need to support legacy
            ).done(function (r0) {
                if (r0.success) {       
                    _conversationId = r0.data.id;                    
                    _isTest = r0.data.isTest;
                    sendConversationIdToTester(_conversationId);
                    _conversationToken = r0.data.realtimeToken;
                    analyticsEvent('ConversationCreated');
                    drawUploadMenu();
                    if (_conversationServiceId == '') {
                        _conversationServiceId = r0.data.serviceId;
                        tryStoreConversationServiceId(_conversationServiceId);
                    }
                    _conversationRecordExists = true;

                    if (_realtimeHubClient == null) {
                        //_realtimeHubClient = RealtimeHubClientFactory.createInstance({
                        //    url: _options.realtimeUrl,
                        //    callbacks: {
                        //        onConnectionStateChanged: function (isConnected) {
                        //            if (_realtimeConnected != isConnected) {
                        //                _realtimeConnected = isConnected;
                        //            }
                        //        }
                        //    }
                        //});
                        _realtimeHubClient = RealtimeHubClientNewFactory.createInstance({
                            url: _options.realtimeUrlNew,
                            logToConsole: true,
                            callbacks: {
                                onConnectionStateChanged: function (isConnected) {
                                    if (_realtimeConnected != isConnected) {
                                        _realtimeConnected = isConnected;
                                    }
                                }
                            }
                        });

                        _hubClientId = _realtimeHubClient.getClientId();
                    }
                    _realtimeHubClient.conversationLanguage().subscribe(_hubClientId, _conversationId, r0.data.realtimeToken, { onUpdated: onLanguageChanged });
                    _realtimeHubClient.conversationMessages().subscribe(
                        _hubClientId, 
                        _conversationId, 
                        JSON.stringify({ isUser: true }), 
                        r0.data.realtimeToken, 
                        { 
                            onAdded: onMessageReceived,
                            onSubscribed: function(backfillData, err) {
                                console.log('subscribed: ' , backfillData, err);
                                doPoll();
                                _realtimeSubscribed = true;
                            }
                        });

                    if (_isTest) {
                        _realtimeHubClient.conversationModifications().subscribe(_hubClientId, _conversationId, r0.data.realtimeToken, {
                            onAdded: onModificationReceived
                        });
                    }

                    $.when(
                        updateConversationContext(_options.botAccountServiceId, _conversationId, _options.createContextJson, 'legacy')
                    ).done(function (r1) {
                        if (r1.success) { 
                            var language = '';
                            if (r1.data.context != null) {
                                var context = JSON.parse(r1.data.context);
                                if (context != null && context.customData != null && context.customData.userInfo != null && context.customData.userInfo.language != null) {
                                     language = context.customData.userInfo.language;                                    
                                }
                            }
                            onLanguageChanged({ language: language });

                            sendMessage(contentType, content);
                        }
                    });                   
                }                
            });
            return;
        }

        analyticsEvent('UserMessageSent');

        $.when(        
            createUserMessage(_options.botAccountServiceId, _conversationId, contentType, content)
        ).done(function(r0) {
            if (r0.success) {                          
                if (typeof r0.data != 'undefined') {              
                    if (_messages != null && _messages.length > 0) {
                        var minMessageId = _u.max(_u.map(_messages, function (message) { return message.id; }));
                        _pageNewerUrl = r0.data.paging.loadNewer.replace('min_message_id=0', 'min_message_id=' + minMessageId);
                    } else {
                        if (_pageNewerUrl == null) {
                            _pageNewerUrl = r0.data.paging.loadNewer;
                        }
                    }

                    // need to trigger the polling if needed
                    recordMessages([], r0.data.conversationId, null);
                }
            } else {
                OnError(r0.error);
            }
        });  
    }

    function onLanguageChanged(conversationLanguage) {
        if (conversationLanguage.language != null && conversationLanguage.language.length > 0) {
            $(_options.container).find('.chat').attr('lang', conversationLanguage.language);
        }

        var persistentMenu = null;
        if (_options.channelSettings.persistentMenu != null && _options.channelSettings.persistentMenu.menus != null) {
            var match = _u.find(_options.channelSettings.persistentMenu.menus, function (menu) { return menu.language.toLowerCase() == (conversationLanguage.language || '').toLowerCase(); });
            if (match != null) {
                persistentMenu = match;
            } else {
                match = _u.find(_options.channelSettings.persistentMenu.menus, function (menu) { return menu.language == ''; });
                if (match != null) {
                    persistentMenu = match;
                }
            }
        }

        var uiModel = {
            persistentMenu: persistentMenu
        };
        $(_options.container).find('.persistent_menu_cell').empty().append(_factoryData['templateInstances' + _messengerVersion].persistentMenu(uiModel));

        $(_options.container).find('.open_persistent_menu').click(function () {
            var parent = $(this).parent();
            if (!$(parent).hasClass('open')) {
                $(parent).addClass('open');
                $(this).attr('aria-expanded', 'true');
            } else {
                $(parent).removeClass('open');
                $(this).attr('aria-expanded', 'false');
            }

            $(_options.container).find('.upload_menu').removeClass('open');
            syncMenuExpandedStates();

            return false;
        });
        $(_options.container).find('.open_persistent_menu').on('keydown', function (event) {
            if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                event.preventDefault(); 
                $(this).parent().toggleClass('open');
                syncMenuExpandedStates();

                return false;
            }
        });
        $(_options.container).click(function () {
            $(_options.container).find('.persistent_menu').removeClass('open');
            syncMenuExpandedStates();
        }).on('keyup', function (event) {
            if (event.keyCode === 27) { //escape
                event.preventDefault(); 
                $(_options.container).find('.persistent_menu').removeClass('open');
                $(_options.container).find('.upload_menu').removeClass('open');
                $(_options.container).find('.reply_text').focus();
                syncMenuExpandedStates();
                return false;
            }
        });

        $(_options.container).find('.postback_menu_item').click(function () {
            var label = $(this).html();
            var postback = $(this).attr('data-postback');

            sendMessage('text', JSON.stringify({ speech: label, postback: postback }));
            stopTyping();

            $(_options.container).find('.persistent_menu').removeClass('open');
            $(_options.container).find('.open_persistent_menu').attr('aria-expanded', 'false');
            $(_options.container).find('.reply_text').focus();
            syncMenuExpandedStates();

            return false;
        });


        $(_options.container).find('.persistent_menu .dropdown-menu .persistent_menu_item')
            .on('keyup', function (event) {
                if (event.which === 27) { // escape
                    event.preventDefault();
                    $(_options.container).find('.persistent_menu').removeClass('open');
                    $(_options.container).find('.persistent_menu').children().first().focus();
                    syncMenuExpandedStates();
                    return false;
                }

                if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                    event.preventDefault();
                    $(this).click();
                    return false;
                }
            });



        $(_options.container).find('.upload_menu .dropdown-menu .upload_menu_item')
            .on('keyup', function (event) {
                if (event.keyCode === 27) { // escape
                    event.preventDefault();
                    $(_options.container).find('.upload_menu').removeClass('open');
                    $(_options.container).find('.upload_menu').children().first().focus();
                    syncMenuExpandedStates();
                    return false;
                }

                if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                    event.preventDefault();
                    $(this).click();
                    return false;
                }
            });

        $(_options.container).find('.persistent_menu .dropdown-menu .persistent_menu_item').last()
            .on('keydown', function (event) {
                if (event.which === 9 && !event.shiftKey) { // tab
                    $(_options.container).find('.persistent_menu').removeClass('open');
                    syncMenuExpandedStates();
                }
            });

        $(_options.container).find('.upload_menu .dropdown-menu .upload_menu_item').last()
            .on('keydown', function (event) {
                if (event.keyCode === 9  && !event.shiftKey) { // tab
                    $(_options.container).find('.upload_menu').removeClass('open');
                    syncMenuExpandedStates();
                }
            });
            
        $(_options.container).find('.persistent_menu .dropdown-toggle')
            .on('keydown', function (event) {
                if (event.which === 9 && event.shiftKey) { // tab
                    $(_options.container).find('.persistent_menu').removeClass('open');
                    syncMenuExpandedStates();
                }
            });

        $(_options.container).find('.upload_menu .dropdown-toggle')
            .on('keydown', function (event) {
                if (event.keyCode === 9  && event.shiftKey) { // tab
                    $(_options.container).find('.upload_menu').removeClass('open');
                    syncMenuExpandedStates();
                }
            });
    }

    function syncMenuExpandedStates() {
        var persistentMenuExpanded = $(_options.container).find('.persistent_menu').hasClass('open');
        $(_options.container).find('.persistent_menu > i').attr('aria-expanded', persistentMenuExpanded);

        var uploadMenuExpanded = $(_options.container).find('.upload_menu').hasClass('open');
        $(_options.container).find('.upload_menu > i').attr('aria-expanded', uploadMenuExpanded);        
    }

    function drawUploadMenu() {
        if (_options.accountSettings == null || !_options.accountSettings.showUploadMenu || _conversationId == null) {        
            $(_options.container).find('.upload_menu_cell').empty();
            return;
        }

        var uiModel = {
            imageText: _i('Image'),
            fileText: _i('File')
        };

        $(_options.container).find('.upload_menu_cell').empty().append(_factoryData['templateInstances' + _messengerVersion].uploadMenu(uiModel));

        $(_options.container).find('.open_upload_menu').click(function () {
            var parent = $(this).parent();
            if (!$(parent).hasClass('open')) {
                $(parent).addClass('open');
                $(this).attr('aria-expanded', 'true');
            } else {
                $(parent).removeClass('open');
                $(this).attr('aria-expanded', 'false');
            }

            $(_options.container).find('.persistent_menu').removeClass('open');

            return false;
        });
        $(_options.container).find('.open_upload_menu').on('keydown', function (event) {
            if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
                event.preventDefault(); 
                $(this).parent().toggleClass('open');

                return false;
            }
        });
        $(_options.container).click(function () {
            $(_options.container).find('.upload_menu').removeClass('open');
        });

        $(_options.container).find('.image_menu_item').click(function () {
            $(_options.container).find('.image_input').click();                        

            $(_options.container).find('.upload_menu').removeClass('open');

            return false;
        });

        $(_options.container).find('.open_upload_dialog').click(function () {
            $(_options.container).find('.file_image_input').click();              
        });

        $(_options.container).find('.file_image_input').click(function () {
            $(this).val(null);
        });

        $(_options.container).find('.file_image_input').on('change', function () {
            var input = $(this);

            var imageMimeTypes = [
                'image/png',
                'image/gif',
                'image/jpeg'
            ];

            var fileMimeTypes = [
                'text/plain',
                'text/csv',
                'application/pdf',
                'application/msword',
                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'application/vnd.ms-excel',
                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'application/vnd.ms-powerpoint',
                'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            ];         

            var sendImageFn = function (id, url, filename) {
                sendMessage('image', JSON.stringify({ name: filename, imageUrl: url, attachmentId: id }));
                stopTyping();
            };

            var sendFileFn = function (id, url, filename) {
                sendMessage('file', JSON.stringify({
                    files: [
                        {
                            name: filename,
                            fileUrl: url,
                            attachmentId: id
                        }
                    ]
                }));
                stopTyping();
            };

            beginUploadImageFile(input, imageMimeTypes, fileMimeTypes, sendImageFn, sendFileFn);
        })

        $(_options.container).find('.image_input').click(function () {
            $(this).val(null);
        });

        $(_options.container).find('.image_input').on('change', function () {
            var input = $(this);
            var allowedMimeTypes = [
                'image/png',
                'image/gif',
                'image/jpeg'
            ];
            var sendMessageFn = function (id, url, filename) {
                sendMessage('image', JSON.stringify({ name: filename, imageUrl: url, attachmentId: id }));   
                stopTyping();
            };

            beginUpload(input, allowedMimeTypes, sendMessageFn);
        })

        $(_options.container).find('.file_menu_item').click(function () {
            $(_options.container).find('.file_input').click();

            $(_options.container).find('.upload_menu').removeClass('open');

            return false;
        });

        $(_options.container).find('.file_input').click(function () {
            $(this).val(null);
        });

        $(_options.container).find('.file_input').on('change', function () {
            var input = $(this);
            //.csv, .pdf, .doc, .docx, .txt, .xls, .xlsx, .ppt, .pptx
            var allowedMimeTypes = [
                'text/plain',
                'text/csv',
                'application/pdf',
                'application/msword',
                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',                
                'application/vnd.ms-excel',
                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'application/vnd.ms-powerpoint',
                'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            ];
            var sendMessageFn = function (id, url, filename) {
                sendMessage('file', JSON.stringify({
                    files: [
                        {
                            name: filename,
                            fileUrl: url,
                            attachmentId: id
                        }
                    ]                    
                }));
                stopTyping();
            };

            beginUpload(input, allowedMimeTypes, sendMessageFn);
        })
    }

    function beginUpload(input, allowedMimeTypes, sendMessageFn) {
        if (input.length > 0 && input[0].files.length > 0) {
            var selectedFile = input[0].files[0];

            var reader = new FileReader();
            reader.onload = function () {
                var filename = selectedFile.name;
                var base64 = reader.result.split(',')[1];
                var mimeType = selectedFile.type;

                console.log(mimeType);

                if (allowedMimeTypes.indexOf(mimeType) < 0) {
                    OnError('File type is not supported.');
                    return;
                }

                $(_options.container).find('.spinner').show();
                $.when(
                    createConversationAttachment(_conversationId, filename, base64, mimeType)
                ).done(function (r0) {
                    $(_options.container).find('.spinner').hide();
                    if (r0.success) {
                        analyticsEvent('FileUpload', { fileUrl: r0.data.publicUrl });
                        sendMessageFn(r0.data.id, r0.data.publicUrl, r0.data.filename);                                                
                    }
                });                                
            };
            reader.readAsDataURL(selectedFile);
        }

        return false;
    }    

    function beginUploadImageFile(input, imageMimeTypes, fileMimeTypes, sendImageFn, sendFileFn) {
        if (input.length > 0 && input[0].files.length > 0) {
            var selectedFile = input[0].files[0];

            var reader = new FileReader();
            reader.onload = function () {
                var filename = selectedFile.name;
                var base64 = reader.result.split(',')[1];
                var mimeType = selectedFile.type;

                console.log(mimeType);

                var isImage = false;
                var isFile = false;
                if (imageMimeTypes.indexOf(mimeType) > -1) {
                    isImage = true;
                }
                if (fileMimeTypes.indexOf(mimeType) > -1) {
                    isFile = true;
                }

                if (!isImage && !isFile) {
                    OnError('File type is not supported.');
                    return;
                }

                var sendMessageFn = sendImageFn;
                if (isFile) {
                    sendMessageFn = sendFileFn;
                }

                readyAttachment(filename, base64, mimeType, sendMessageFn, !isFile);
            };
            reader.readAsDataURL(selectedFile);
        }

        return false;
    }   

    function readyAttachment(filename, base64, mimeType, sendMessageFn, isImage) {
        _readiedAttachment = { filename: filename, base64: base64, mimeType: mimeType, sendMessageFn: sendMessageFn };

        $(_options.container).find('.reply_text').val('[attachment]');
        $(_options.container).find('.reply_text').hide();

        var attachmentContainer = $(_options.container).find('.attachment_container');
        if (isImage) {
            $(attachmentContainer).find('.file_image').hide();
            $(attachmentContainer).find('.attachment_image').show();
            $(attachmentContainer).find('.attachment_image').attr('src', 'data:' + mimeType + ';base64,' + base64);
        } else {
            $(attachmentContainer).find('.file_image').show();
            $(attachmentContainer).find('.attachment_image').hide();
        }
        $(attachmentContainer).find('.attachment_name').html(filename);

        $(attachmentContainer).find('.attachment_remove').off('click').click(function (e) {
            _readiedAttachment = null;

            $(_options.container).find('.send').hide();
            $(_options.container).find('.upload_menu_cell').show();
            if ($(_options.container).find('.persistent_menu').length > 0) {
                $(_options.container).find('.persistent_menu_cell').show();
            }

            $(_options.container).find('.reply_text').val('');
            $(_options.container).find('.reply_text').show();

            $(attachmentContainer).hide();
        });
        $(attachmentContainer).find('.attachment_remove').on('keydown', function (e) {
            // pressing enter or space = click button
            if (e.keyCode == 13 || e.keyCode == 32) {
                e.preventDefault();
                $(e.target).click();
            }
        });

        $(attachmentContainer).show();
        
        $(_options.container).find('.upload_menu_cell').hide();
        $(_options.container).find('.persistent_menu_cell').hide();

        var sendButton = $(_options.container).find('.send');
        $(sendButton).prop('disabled', false);
        $(sendButton).show();
        $(sendButton).focus();
    }

    function sendAttachment(filename, base64, mimeType, sendMessageFn) {
        $(_options.container).find('.spinner').show();
        $.when(
            createConversationAttachment(_conversationId, filename, base64, mimeType)
        ).done(function (r0) {
            $(_options.container).find('.spinner').hide();
            if (r0.success) {
                analyticsEvent('FileUpload', { fileUrl: r0.data.publicUrl });
                sendMessageFn(r0.data.id, r0.data.publicUrl, r0.data.filename);
            }
        });
    }

    function recordMessages(messages, conversationId, realtimeUpdatesInfo) {
        if (messages.length > 0) _conversationHasMessages = true;

        if (_realtimeHubClient == null && realtimeUpdatesInfo != null) {
            _realtimeHubClient = RealtimeHubClientNewFactory.createInstance({
                url: _options.realtimeUrlNew,
                logToConsole: true,
                callbacks: {
                    onConnectionStateChanged: function (isConnected) {
                        if (_realtimeConnected != isConnected) {
                            _realtimeConnected = isConnected;
                        }
                    }
                }
            });
            _hubClientId = _realtimeHubClient.getClientId();
        }
        if (_realtimeHubClient != null && _hubClientId != null && conversationId > 0 & realtimeUpdatesInfo != null && conversationId != _subscribedId) {
            _realtimeHubClient.conversationMessages().subscribe(
                _hubClientId,
                conversationId,
                JSON.stringify({ isUser: true }),
                realtimeUpdatesInfo.token,
                {
                    onAdded: onMessageReceived,
                    onSubscribed: function (backfillData, err) {
                        console.log('subscribed: ', backfillData, err);
                        doPoll();
                        _realtimeSubscribed = true;
                    }
                });
            _subscribedId = conversationId;

            if (_isTest) {
                _realtimeHubClient.conversationModifications().subscribe(_hubClientId, conversationId, realtimeUpdatesInfo.token, {
                    onAdded: onModificationReceived
                });
            }

            _realtimeHubClient.conversationLanguage().subscribe(_hubClientId, conversationId, realtimeUpdatesInfo.token, { onUpdated: onLanguageChanged });
        } 

        var isFirst = (_messages.length == 0);

        // throw out custom payload messages for now
        messages = _u.filter(messages, function(o) { return o != null && o.contentType != getContentType('custom_payload'); });
        if (messages.length != 0) {

            $.each(messages, function(index, message) {
                processConversationMessage(message);
            });

            _messages = _messages.concat(messages);

            setData();          

            //console.log('now have ' + _messages.length);
        }
        
        // start polling upon receipt of first message
        if (isFirst) {                    
            doPoll(function () {
                _detailPoller = MakePoller([5, 2]);
                _detailPoller.queue(pollForNewMessages);
            });
        }    

        syncModeSelector();
    }

    function pollForNewMessages() {
        if (_realtimeConnected && _realtimeSubscribed) {
            _detailPoller.queue(pollForNewMessages);
            return;
        }

        doPoll(function () { _detailPoller.queue(pollForNewMessages); });           
    }

    function doPoll(callback) {
        if (_pageNewerUrl != null) {
            var proxyPageUrl = 'proxy.svc/poll?pollUrl=' + encodeURIComponent(_pageNewerUrl) + '&conversationServiceId=' + encodeURIComponent(_conversationServiceId);
            $.get(proxyPageUrl, function (data) {
                processConversationMessagesResult(data);                
                if (typeof callback != 'undefined') {
                    callback();
                }
            });
        }     
    }

    function processConversationMessagesResult(data) {
        var newMessages = data.messages;
        if (newMessages != null && newMessages.length > 0) {
            recordMessages(newMessages, data.conversationId, data.paging.realtimeUpdates);                    
            _pageNewerUrl = data.paging.loadNewer;
        }
    }


    function getConversation(botAccountServiceId, conversationServiceId) {
        //var requestUrl = _apiBase + 'bot/' + botId + '/user_message';
        requestUrl = 'proxy.svc/conversation?' +
            'botAccountServiceId=' + encodeURIComponent(botAccountServiceId) +
            '&conversationServiceId=' + encodeURIComponent(conversationServiceId);

        return MakeRequest(requestUrl, 'GET');       
    }

    function createConversation(botAccountServiceId, conversationServiceId) {
        requestUrl = 'proxy.svc/createConversation';

        var requestBody = {
            botAccountServiceId: botAccountServiceId,
            conversationServiceId: conversationServiceId
        };

        return MakeRequest(requestUrl, 'POST', requestBody);
    }

    function updateConversationContext(botAccountServiceId, conversationId, data, updateType) {
        requestUrl = 'proxy.svc/updateConversationContext';

        var requestBody = {
            botAccountServiceId: botAccountServiceId,
            conversationId: conversationId,
            data: data,
            updateType: updateType,
            timezone: GetBrowserTimeZone()
        };

        return MakeRequest(requestUrl, 'POST', requestBody);
    }

    function setConversationMultimediaCallConnected(callId, isConnected) {
        return MakeRequest(_apiBase + 'conversation/' + _conversationId + '/multimediaCall/connection', 'POST', {                
            roomName: callId,
            isConnected: isConnected,
            agentConversationId: null,
            accessToken: _conversationToken
        });    
    }

    function createUserMessage(botAccountServiceId, conversationId, contentType, content) {
        //var requestUrl = _apiBase + 'bot/' + botId + '/user_message';
        requestUrl = 'proxy.svc/createUserMessage';

        var requestBody = {
            botAccountServiceId: botAccountServiceId,
            conversationId: conversationId,
            contentType: contentType,
            content: content
        };

        return MakeRequest(requestUrl, 'POST', requestBody);       
    }

    function createConversationAttachment(conversationId, filename, base64, mimeType) {
        requestUrl = 'proxy.svc/createConversationAttachment';

        var requestBody = {
            conversationId: conversationId,
            filename: filename,
            base64: base64,
            mimeType: mimeType
        };

        return MakeRequest(requestUrl, 'POST', requestBody);
    }

    function getAutoComplete(botAccountServiceId, conversationId, utterance) {
        requestUrl = 'proxy.svc/getAutoComplete';

        var requestBody = {
            botAccountServiceId: botAccountServiceId,
            conversationId: conversationId,
            utterance: utterance
        };

        return MakeRequest(requestUrl, 'POST', requestBody);
    }

    function MakeRequest(url, httpVerb, requestBody) {
        var deferred = $.Deferred();

        var ajaxRequest = {
            url: url,
            type: httpVerb,
            success: function (data, status, xmlHttp) {
                deferred.resolveWith(this, [{ success: true, data: data }]);
            },
            error: function (xhr, ajaxOptions, thrownError) {
                deferred.resolveWith(this, [{ success: false, error: xhr.getResponseHeader('astute-bot-api-exception') }]);
            }
        };

        if (typeof requestBody !== 'undefined') {
            ajaxRequest.contentType = 'application/json';
            ajaxRequest.data = JSON.stringify(requestBody);
        }

        $.ajax(ajaxRequest);

        return deferred;
    };

    function MakePoller(intervalSeconds) {
        var _interface = 
        {
            queue: function(pollingFn) {
                queue(pollingFn);
            },
            reset: function() {
                reset();
            },
            stop: function() {
                stop();
            }
        };

        var _timeOut = null;
        var _intervalCounter = 0;
        var _intervalSeconds = intervalSeconds;
        var _pollingFn = null;
        var _stopped = false;

        function reset() {
            _timeOut = null;
            _intervalCounter = 0;
        }

        // queues a "poll" request
        function queue(pollingFn) {
            _pollingFn = pollingFn;            
            setTimer();
        }

        function stop() {
            _timeout = null;
            _stopped = true;
        }

        function setTimer() {
            if (_timeOut != null || _intervalSeconds == null) return;

            _timeOut = setTimeout(function () {
                _timeOut = null;
                if (_stopped) return;
                if (_pollingFn != null) _pollingFn();
                if (_intervalCounter < (_intervalSeconds.length - 1)) _intervalCounter++;
            }, _intervalSeconds[_intervalCounter] * 1000);
        }

        return _interface;
    }

    function analyticsEvent(type, data){        
        if (!_options.postAnalyticsEvents) {
            return true;
        }
        
        var eventObj = {
            source: 'AstuteBot.WebChat',
            type: type,
            data: {
                botAccountServiceId : _options.botAccountServiceId,
                conversationId : _conversationId                    
            }               
        }
        eventObj.data = data != null ? $.extend(true, {}, eventObj.data, data) : eventObj.data;
        var message = JSON.stringify(eventObj)
        window.top.postMessage(message, '*');
    }
    
    function OnError(msg) {

    }

    init();
    return _interface;
};
$(document).ready(function () {
    var _botAccountServiceId = '';
    var _conversationServiceId = '';

    function getUrlVars() {
        var vars = [], hash;
        var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1);

        hashes = hashes.split('#')[0].split('&');
        for (var i = 0; i < hashes.length; i++) {
            hash = hashes[i].split('=');
            vars.push(hash[0]);
            vars[hash[0]] = hash[1];
        }
        return vars;
    }

    function validateContext(inputContextJson) {
        try {
            var deserialized = JSON.parse(inputContextJson)

            if (deserialized instanceof Array) {
                var context = {};
                $.each(deserialized, function (index, part) {
                    context = $.extend(context, part);
                });
                inputContextJson = JSON.stringify(context);
            }
            //if (!(deserialized instanceof Array)) {
            //    // api for some reason takes input contexts as array
            //    deserialized = [deserialized];
            //    inputContextJson = JSON.stringify(deserialized);
            //}
            return inputContextJson;
        } catch (e) {
        }
        return null;
    }

    function init() {        
        var botAccountServiceId = getUrlVars()["aid"];        
        var conversationServiceId = getUrlVars()["sid"];      

        if (typeof botAccountServiceId == 'undefined' && typeof conversationServiceId == 'undefined') return;        

        if (botAccountServiceId != null) {
            _botAccountServiceId = decodeURIComponent(botAccountServiceId);
        }

        if (conversationServiceId != null) {
            _conversationServiceId = decodeURIComponent(conversationServiceId);
        }

        loadCustomStyles();
    }

    
    function loadCustomStyles() {
        $.when(        
            MakeRequest(
                'proxy.svc/botAccount/settings?botAccountServiceId=' + encodeURIComponent(_botAccountServiceId) + '&conversationServiceId=' + encodeURIComponent(_conversationServiceId), 
                'GET'
            )       
        ).done(function(r0) {
            var accountSettings = r0.data.webSettings;
            if (accountSettings != null) {
                if (accountSettings.disableUserScaling) {
                    var viewportContent = $('meta[name=viewport]').attr('content');
                    viewportContent += ', user-scalable=no';
                    $('meta[name=viewport]').attr('content', viewportContent);
                }

                if (typeof accountSettings.css != 'undefined' && accountSettings.css != '' && accountSettings.css != null) {
                    // push styles blob into document before drawing
                    var css = accountSettings.css;
                    $("<style>")
                        .prop("type", "text/css")
                        .html(css)
                        .appendTo("head");
                }
            }       

            var channelSettings = r0.data.channelSettings;
            var browserSettings = r0.data.browserSettings;

            if (_botAccountServiceId == null || _botAccountServiceId == '') {
                _botAccountServiceId = r0.data.botAccountServiceId;
            }

            draw(accountSettings, channelSettings, browserSettings, r0.data.platform);
        });
    }


    function draw(accountSettings, channelSettings, browserSettings, platform) {
        var container = $('body');

        var replyHeightPixels = 36;
        if (accountSettings != null && typeof accountSettings.replyHeightPixels != 'undefined' && accountSettings.replyHeightPixels != '' && accountSettings.replyHeightPixels != null) {
            replyHeightPixels = accountSettings.replyHeightPixels;
        }

        // launcher settings can be POSTed to us by 3d party
        var launcherSettings = {
            autoGetStarted: false,
            conversationServiceId: '',
            persistConversationAcrossSession: false,
            forceNewSession: false,
            replyHeightPixels: replyHeightPixels,
            allowMicrophone: false,
            initialUserMessage: null,
            postAnalyticsEvents: false,
        };
        try {
            var launcherSettingsJson = $('#launcherSettings').val();
            if (launcherSettingsJson != null && typeof launcherSettingsJson != 'undefined' && launcherSettingsJson != '' && launcherSettingsJson != 'null') {
                console.log('launcher settings JSON: ' + launcherSettingsJson);
                launcherSettings = JSON.parse(launcherSettingsJson);
            }        
            console.log('...parsed launcher settings ok');
        } catch (e) { 
            console.log('...unable to parse launcher settings: ' + e);
        }

        // don't overwrite the one passed in through json
        if (launcherSettings.conversationServiceId == null || launcherSettings.conversationServiceId == '') {
            launcherSettings.conversationServiceId = _conversationServiceId;
        }

        //var realtimeHubsBase = $('#realtimeHubsBase').val();
        var realtimeHubsBaseNew = $('#realtimeHubsBaseNew').val();

        var createContext = $('#createContext').val();
        var updateContext = $('#updateContext').val();
        
        // inputContext overrides create context (default legacy)
        var inputContext = $('#inputContext').val();

        // try to use input context from url if it is there
        var urlInputContext = getUrlVars()["inputContext"];
        if (typeof urlInputContext != 'undefined' && urlInputContext.length > 0) {
            inputContext = decodeURIComponent(urlInputContext);
        }

        if (inputContext != null && inputContext.length > 0) {
            createContext = inputContext;
        }

        _testWidget = TestBotWidgetFactory.createInstance({            
            container: container,
            botAccountServiceId: _botAccountServiceId,
            accountSettings: accountSettings,
            channelSettings: channelSettings,
            browserSettings: browserSettings,
            autoGetStarted: launcherSettings.autoGetStarted,
            conversationServiceId: launcherSettings.conversationServiceId,
            persistConversationAcrossSession: launcherSettings.persistConversationAcrossSession,
            forceNewSession: launcherSettings.forceNewSession,
            allowMicrophone: launcherSettings.allowMicrophone,
            replyHeightPixels: launcherSettings.replyHeightPixels,
            initialUserMessage: launcherSettings.initialUserMessage,
            createContextJson: validateContext(createContext),
            updateContextJson: validateContext(updateContext),
            handlers: [],
            conversationHandlers: [],
            platform: platform,
            //realtimeUrl: realtimeHubsBase,
            realtimeUrlNew: realtimeHubsBaseNew,
            postAnalyticsEvents: launcherSettings.postAnalyticsEvents
        });

        $(window).resize(function() {
            _testWidget.resize();
        });
    }


    function MakeRequest(url, httpVerb, requestBody) {
        var deferred = $.Deferred();

        var ajaxRequest = {
            url: url,
            type: httpVerb,
            success: function (data, status, xmlHttp) {
                deferred.resolveWith(this, [{ success: true, data: data }]);
            },
            error: function (xhr, ajaxOptions, thrownError) {
                deferred.resolveWith(this, [{ success: false, error: xhr.getResponseHeader('astute-bot-api-exception') }]);
            }
        };

        if (typeof requestBody !== 'undefined') {
            ajaxRequest.contentType = 'application/json';
            ajaxRequest.data = JSON.stringify(requestBody);
        }

        $.ajax(ajaxRequest);

        return deferred;
    };

    init();
});;
var MonoWAVRecorderFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    }; 

    function init() {
        _factoryData.defaultInstanceOptions = {
            maxRecordingSeconds: 10,
            callbacks: {
                onBlobRecorded: function (blob) { },
                onError: function () { }
            }
        };

        _factoryData.isInitialized = true;
    }

    var _factoryData = {
        isInitialized: false
    };

    return _factoryInterface;
}());

MonoWAVRecorderFactory.createInstance = function (options) {
    var _interface = {
        start: function () { startRecording(); },
        stop: function () { stopRecording(options.callbacks.onBlobRecorded) },
        cancel: function () { cancelRecording(); }
    };

    var _factoryData = this.getFactoryData();
    var _options = $.extend(true, {}, _factoryData.defaultInstanceOptions, options);

    var _initialized = false;
    var _unableToRecord = false;

    var leftchannel = [], rightchannel = [], recorder = null, recording = !1, recordingStartTime, recordingLength = 0, volume = null, audioInput = null, sampleRate = 44100, audioContext = null, context = null, outputString;

    function init() {
        if (!navigator.getUserMedia)
            navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
                navigator.mozGetUserMedia || navigator.msGetUserMedia;

        if (navigator.getUserMedia) {
            navigator.getUserMedia({ audio: true }, onCanRecord, function (e) {
                _unableToRecord = true;
                _options.callbacks.onError();
            });
        } else {
            _unableToRecord = true;
            _options.callbacks.onError();
        };

        _initialized = true;        
    }

    function onCanRecord(e) {
        // creates the audio context
        audioContext = window.AudioContext || window.webkitAudioContext;
        context = new audioContext();
        
        // creates a gain node
        volume = context.createGain();

        // creates an audio node from the microphone incoming stream
        audioInput = context.createMediaStreamSource(e);

        // connect the stream to the gain node
        audioInput.connect(volume);

        /* From the spec: This value controls how frequently the audioprocess event is 
        dispatched and how many sample-frames need to be processed each call. 
        Lower values for buffer size will result in a lower (better) latency. 
        Higher values will be necessary to avoid audio breakup and glitches */
        var bufferSize = 2048;
        recorder = context.createScriptProcessor(bufferSize, 2, 2);

        recorder.onaudioprocess = function (e) {
            if (!recording) return;
            var totalRecordingSeconds = (Date.now() - recordingStartTime) / 1000;
            if (totalRecordingSeconds > _options.maxRecordingSeconds) {
                console.log('recording too long');
                stopRecording(function (blob) { });
                _options.callbacks.onError();
                return;
            }
            var left = e.inputBuffer.getChannelData(0);
            var right = e.inputBuffer.getChannelData(1);
            // we clone the samples
            leftchannel.push(new Float32Array(left));
            rightchannel.push(new Float32Array(right));
            recordingLength += bufferSize;
            console.log('recording');
        }

        // we connect the recorder
        volume.connect(recorder);
        recorder.connect(context.destination);

        console.log('can record');
    }

    function startRecording() {
        if (_unableToRecord) {
            _options.callbacks.onError();
            return;
        }
        if (!_initialized) {
            init();
        }

        recordingStartTime = Date.now();
        recording = true;

        // reset the buffers for the new recording
        leftchannel.length = rightchannel.length = 0;
        recordingLength = 0;
    }

    function stopRecording(callback) {
        recording = false;

        // we flat the left and right channels down
        var leftBuffer = mergeBuffers(leftchannel, recordingLength);
        var rightBuffer = mergeBuffers(rightchannel, recordingLength);
        // we interleave both channels together
        var interleaved = interleave(leftBuffer, rightBuffer);

        // we create our wav file
        var buffer = new ArrayBuffer(44 + interleaved.length * 2);
        var view = new DataView(buffer);

        // RIFF chunk descriptor
        writeUTFBytes(view, 0, 'RIFF');
        view.setUint32(4, 44 + interleaved.length * 2, true);
        writeUTFBytes(view, 8, 'WAVE');
        // FMT sub-chunk
        writeUTFBytes(view, 12, 'fmt ');
        view.setUint32(16, 16, true);
        view.setUint16(20, 1, true);
        // stereo (2 channels)
        view.setUint16(22, 1, true);
        view.setUint32(24, sampleRate, true);
        view.setUint32(28, sampleRate * 2, true);
        view.setUint16(32, 4, true);
        view.setUint16(34, 16, true);
        // data sub-chunk
        writeUTFBytes(view, 36, 'data');
        view.setUint32(40, interleaved.length * 2, true);

        // write the PCM samples
        var lng = interleaved.length;
        var index = 44;
        var volume = 1;
        for (var i = 0; i < lng; i++) {
            view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
            index += 2;
        }

        // our final binary blob
        var blob = new Blob([view], { type: 'audio/wav' });
        callback(blob);
    }

    function cancelRecording() {
        stopRecording(function (blob) { });
    }

    function interleave(leftChannel, rightChannel) {
        var length = leftChannel.length + rightChannel.length / 2;
        var result = new Float32Array(length);

        for (var index = 0; index < length;) {
            result[index++] = (leftChannel[index] + rightChannel[index]) / 2;
        }
        return result;
    }

    function mergeBuffers(channelBuffer, recordingLength) {
        var result = new Float32Array(recordingLength);
        var offset = 0;
        var lng = channelBuffer.length;
        for (var i = 0; i < lng; i++) {
            var buffer = channelBuffer[i];
            result.set(buffer, offset);
            offset += buffer.length;
        }
        return result;
    }

    function writeUTFBytes(view, offset, string) {
        var lng = string.length;
        for (var i = 0; i < lng; i++) {
            view.setUint8(offset + i, string.charCodeAt(i));
        }
    }

    return _interface;
}

//var createRecorder = function () {
//    // variables
//    var leftchannel = [], rightchannel = [], recorder = null, recording = !1, recordingStartTime, recordingLength = 0, volume = null, audioInput = null, sampleRate = 44100, audioContext = null, context = null, outputElement = {}, maxRecordingSeconds = 10, errorCallback = null, outputString;

//    // feature detection 
//    if (!navigator.getUserMedia)
//        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
//            navigator.mozGetUserMedia || navigator.msGetUserMedia;

//    if (navigator.getUserMedia) {
//        navigator.getUserMedia({ audio: true }, success, function (e) {
//            alert('Error capturing audio.');
//        });
//    } else alert('getUserMedia not supported in this browser.');

//    function startRecording(maxRecordingSeconds, callback) {
//        errorCallback = callback;
//        recordingStartTime = Date.now();
//        recording = true;
//        // reset the buffers for the new recording
//        leftchannel.length = rightchannel.length = 0;
//        recordingLength = 0;
//        outputElement.innerHTML = 'Recording now...';
//        // if S is pressed, we stop the recording and package the WAV file
//    } // end startRecording()

//    function stopRecording(callBack) {
//        // we stop recording
//        recording = false;

//        outputElement.innerHTML = 'Building wav file...';

//        // we flat the left and right channels down
//        var leftBuffer = mergeBuffers(leftchannel, recordingLength);
//        var rightBuffer = mergeBuffers(rightchannel, recordingLength);
//        // we interleave both channels together
//        var interleaved = interleave(leftBuffer, rightBuffer);

//        // we create our wav file
//        var buffer = new ArrayBuffer(44 + interleaved.length * 2);
//        var view = new DataView(buffer);

//        // RIFF chunk descriptor
//        writeUTFBytes(view, 0, 'RIFF');
//        view.setUint32(4, 44 + interleaved.length * 2, true);
//        writeUTFBytes(view, 8, 'WAVE');
//        // FMT sub-chunk
//        writeUTFBytes(view, 12, 'fmt ');
//        view.setUint32(16, 16, true);
//        view.setUint16(20, 1, true);
//        // stereo (2 channels)
//        view.setUint16(22, 1, true);
//        view.setUint32(24, sampleRate, true);
//        view.setUint32(28, sampleRate * 2, true);
//        view.setUint16(32, 4, true);
//        view.setUint16(34, 16, true);
//        // data sub-chunk
//        writeUTFBytes(view, 36, 'data');
//        view.setUint32(40, interleaved.length * 2, true);

//        // write the PCM samples
//        var lng = interleaved.length;
//        var index = 44;
//        var volume = 1;
//        for (var i = 0; i < lng; i++) {
//            view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
//            index += 2;
//        }

//        // our final binary blob
//        var blob = new Blob([view], { type: 'audio/wav' });
//        //var url = (window.URL || window.webkitURL).createObjectURL(blob);
//        callBack(blob);
//    }

//    function cancel() {
//        stopRecording(function (blob) { });
//    }


//    function interleave(leftChannel, rightChannel) {
//        var length = leftChannel.length + rightChannel.length / 2;
//        var result = new Float32Array(length);

//        for (var index = 0; index < length;) {
//            result[index++] = (leftChannel[index] + rightChannel[index]) / 2;
//        }
//        return result;
//    }

//    function mergeBuffers(channelBuffer, recordingLength) {
//        var result = new Float32Array(recordingLength);
//        var offset = 0;
//        var lng = channelBuffer.length;
//        for (var i = 0; i < lng; i++) {
//            var buffer = channelBuffer[i];
//            result.set(buffer, offset);
//            offset += buffer.length;
//        }
//        return result;
//    }

//    function writeUTFBytes(view, offset, string) {
//        var lng = string.length;
//        for (var i = 0; i < lng; i++) {
//            view.setUint8(offset + i, string.charCodeAt(i));
//        }
//    }

//    function success(e) {
//        // creates the audio context
//        audioContext = window.AudioContext || window.webkitAudioContext;
//        context = new audioContext();

//        console.log('succcess');

//        // creates a gain node
//        volume = context.createGain();

//        // creates an audio node from the microphone incoming stream
//        audioInput = context.createMediaStreamSource(e);

//        // connect the stream to the gain node
//        audioInput.connect(volume);

//        /* From the spec: This value controls how frequently the audioprocess event is 
//        dispatched and how many sample-frames need to be processed each call. 
//        Lower values for buffer size will result in a lower (better) latency. 
//        Higher values will be necessary to avoid audio breakup and glitches */
//        var bufferSize = 2048;
//        recorder = context.createScriptProcessor(bufferSize, 2, 2);

//        recorder.onaudioprocess = function (e) {
//            if (!recording) return;
//            var totalRecordingSeconds = (Date.now() - recordingStartTime) / 1000;
//            if (totalRecordingSeconds > maxRecordingSeconds) {
//                console.log('recording too long');
//                stopRecording(function (blob) { });
//                if (errorCallback != null) {

//                    errorCallback();
//                }
//                return;
//            }
//            var left = e.inputBuffer.getChannelData(0);
//            var right = e.inputBuffer.getChannelData(1);
//            // we clone the samples
//            leftchannel.push(new Float32Array(left));
//            rightchannel.push(new Float32Array(right));
//            recordingLength += bufferSize;
//            console.log('recording');
//        }

//        // we connect the recorder
//        volume.connect(recorder);
//        recorder.connect(context.destination);
//    }

//    return {
//        start: startRecording,
//        stop: stopRecording,
//        cancel: cancel
//    };

//};;
/*
* JavaScript Pretty Date
* Copyright (c) 2011 John Resig (ejohn.org)
* Licensed under the MIT and GPL licenses.
*/
// takes a Date object and prints nice text
function prettyDate(time) {
    var diff = (((new Date()).getTime() - time.getTime()) / 1000);
    var day_diff = Math.floor(diff / 86400);

    if (isNaN(day_diff)) {
        return _("NaN");
    }

    //if (isNaN(day_diff) || day_diff < 0) {
    //    return _("Unknown");
    //}
    //else if (day_diff >= 4) {
    // if browser time is off from server display instead of error.
    if (day_diff >= 4 || day_diff < 0) {
        //return time.toString("F");
        //return time.toString('t') + ' ' + time.toString('d');
        return time.toString('d') + ' ' + time.toString('t');
    }

    return day_diff == 0 && (
            diff < 60 && _("Seconds ago") ||
            diff < 120 && _("1 minute ago") ||
            diff < 3600 && sprintf(_("%d minutes ago"), Math.floor(diff / 60)) ||
            diff < 7200 && _("1 hour ago") ||
            diff < 86400 && sprintf(_("%d hours ago"), Math.floor(diff / 3600))) ||
        day_diff == 1 && _("Yesterday") ||
        day_diff < 5 && sprintf(_("%d days ago"), day_diff);
}


// only show "x minutes ago", then go straight to full date
function prettyDateMinutes(time) {
    var diff = (((new Date()).getTime() - time.getTime()) / 1000);
    var day_diff = Math.floor(diff / 86400);

    if (isNaN(day_diff)) {
        return _("NaN");
    }

//     if (isNaN(day_diff) || day_diff < 0) {
//         return _("Unknown");
//     }
//     else 
// if browser time is off from server display instead of error.
    if (diff >= 3600 || day_diff < 0) {
        return time.toString('d') + ' ' + time.toString('t');
    }

    return day_diff == 0 && (
            diff < 60 && _("Seconds ago") ||
            diff < 120 && _("1 minute ago") ||
            diff < 3600 && sprintf(_("%d minutes ago"), Math.floor(diff / 60)) ||
            diff < 7200 && _("1 hour ago") ||
            diff < 86400 && sprintf(_("%d hours ago"), Math.floor(diff / 3600))) ||
        day_diff == 1 && _("Yesterday") ||
        day_diff < 5 && sprintf(_("%d days ago"), day_diff);
}



function GetUTCOffset(tzDesc) {
    // Kludge up the UTC offset from the timezone description string.  
    // NOTE: this will not work for daylight savings time and should NOT be used; when the client needs to construct a datetime and pass to the server, it needs to pass the
    // time zone textual description along with the datetime values.  Similarly, the timezone listing is wrong
    var utcOffset = tzDesc.substring(4);
    utcOffset = utcOffset.split(')')[0];
    return utcOffset;
}


// NOTE: javascript should not be making iso8601 strings...
//function MakeISO8601DateTime(year, month, day, hours, minutes, seconds, utcOffset) {
//    if (month < 10) month = '0' + month;
//    if (day < 10) day = '0' + day;
//    if (hours < 10) hours = '0' + hours;
//    if (minutes < 10) minutes = '0' + minutes;
//    if (seconds < 10) seconds = '0' + seconds;

//    return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes + ':' + seconds + utcOffset;
//}


//function MakeISO8601Date(year, month, day, utcOffset) {
//    return MakeISO8601DateTime(year, month, day, 0, 0, 0, utcOffset);
//}

// NOTE: obviated by Date.prototype.toISOSTring()? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
//...unless the ISO8601 time is UTC
function MakeISO8601UTCFromDateObject(d) {
  function pad(n) { return n < 10 ? '0' + n : n; }

  return d.getUTCFullYear()+'-'
    + pad(d.getUTCMonth()+1)+'-'
    + pad(d.getUTCDate())+'T'
    + pad(d.getUTCHours())+':'
    + pad(d.getUTCMinutes())+':'
    + pad(d.getUTCSeconds())+'Z'
}


// NOTE: when the client needs to construct a datetime and pass to the server, it needs to pass the
// time zone textual description along with the datetime values.  Thats what this function does.
// Also note that the timezone i18n UTC offsets are going to be wrong wrt DST.  Might be better to not include the UTC value in the label?
function MakePsuedoDateTimeString(year, month, day, hours, minutes, seconds, timeZoneLabel) {
    function pad(n) { return n < 10 ? '0' + n : n; }

    return year + '-'
        + pad(month) + '-'
        + pad(day) + 'T'
        + pad(hours) + ':'
        + pad(minutes) + ':'
        + pad(seconds)
        + '|' + timeZoneLabel; // this is where things get weird
}

// Day only
function MakePsuedoDateString(year, month, day, timeZoneLabel) {
    return MakePsuedoDateTimeString(year, month, day, 0, 0, 0, timeZoneLabel);
}

// time only
function MakePsuedoTimeString(hours, minutes, seconds, ampm, timeZoneLabel) {
    function pad(n) { return n < 10 ? '0' + n : n; }

    if (ampm == 'PM') {
        // 12pm = 12:mm:ss
        if (hours < 12) hours += 12;            
    } else {
        // 12am = 00:mm:ss
        if (hours == 12) hours = 0;
    }

    return pad(hours) + ':'
         + pad(minutes) + ':'
         + pad(seconds)
         + '|' + timeZoneLabel; // this is where things get weird
}

function MakeDateFromUnixTimeStamp(unixTimeStamp) {
    return new Date(unixTimeStamp*1000);
}


;
/**
 * Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
 * � 2011 Colin Snover <http://zetafleet.com>
 * Released under MIT license.
 */
(function (Date, undefined) {
    var origParse = Date.parse, numericKeys = [1, 4, 5, 6, 7, 10, 11];
    Date.parse = function (date) {        
        var timestamp, struct, minutesOffset = 0;

        // ES5 �15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
        // before falling back to any implementation-specific date parsing, so that�s what we do, even if native
        // implementations could be faster
        //              1 YYYY                2 MM       3 DD           4 HH    5 mm       6 ss        7 msec        8 Z 9 �    10 tzHH    11 tzmm
        

        if (struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date)) {

            // avoid NaN timestamps caused by �undefined� values being passed to Date.UTC
            for (var i = 0, k; (k = numericKeys[i]); ++i) {
                struct[k] = +struct[k] || 0;
            }

            // allow undefined days and months
            struct[2] = (+struct[2] || 1) - 1;
            struct[3] = +struct[3] || 1;

            if (struct[8] !== 'Z' && struct[9] !== undefined) {
                minutesOffset = struct[10] * 60 + struct[11];

                if (struct[9] === '+') {
                    minutesOffset = 0 - minutesOffset;
                }
            }

            timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]);
        }
        else {
            timestamp = origParse ? origParse(date) : NaN;
        }

        return timestamp;
    };

    Date.parseAsLocalDate = function (date) {
        var dt = new Date(Date.parse(date));
        dt.setMinutes(dt.getMinutes() + dt.getTimezoneOffset()).toString();  // unutc-time-zone the date
        return dt;
    };


    // Convert the date times user specifies from local time to UTC time, truncated the actual time values.  The idea is that when user selects
    // a day with the daterangepicker, we want the output datetimes to be midnight of that day in UTC time, not in browser/local time.
    Date.truncateToUTCDate = function(d) {
        var ud = new Date(d);
        ud.setUTCFullYear( d.getFullYear() );
        ud.setUTCMonth( d.getMonth() );
        ud.setUTCDate( d.getDate() );
        ud.setUTCHours(0, 0, 0, 0);
        return ud;
    };
} (Date));;
var olsonLUT = {

    //"Dateline Standard Time" : { utcOffset: "-12:00", olsonIds: ["Etc/GMT+12","Etc/GMT+12"] },
    //"UTC-11" : { utcOffset: "-11:00", olsonIds: ["Etc/GMT+11","Pacific/Pago_Pago","Pacific/Niue","Pacific/Midway","Etc/GMT+11"] },
    //"Hawaiian Standard Time" : { utcOffset: "-10:00", olsonIds: ["Pacific/Honolulu","Pacific/Rarotonga","Pacific/Tahiti","Pacific/Johnston","Pacific/Honolulu","Etc/GMT+10"] },
    //"Alaskan Standard Time" : { utcOffset: "-09:00", olsonIds: ["America/Anchorage","America/Anchorage America/Juneau America/Nome America/Sitka America/Yakutat"] },
    //"Pacific Standard Time (Mexico)" : { utcOffset: "-08:00", olsonIds: ["America/Santa_Isabel","America/Santa_Isabel"] },
    "US/Pacific" : { /*utcOffset: "-08:00",*/ olsonIds: ["America/Los_Angeles","America/Vancouver America/Dawson America/Whitehorse","America/Tijuana","America/Los_Angeles","PST8PDT"] },
    "US/Mountain" : { /*utcOffset: "-07:00",*/ olsonIds: ["America/Phoenix","America/Dawson_Creek America/Creston","America/Hermosillo","America/Phoenix","Etc/GMT+7"] },
    //"Mountain Standard Time (Mexico)" : { utcOffset: "-07:00", olsonIds: ["America/Chihuahua","America/Chihuahua America/Mazatlan"] },
    "US/Mountain" : { /*utcOffset: "-07:00",*/ olsonIds: ["America/Denver","America/Edmonton America/Cambridge_Bay America/Inuvik America/Yellowknife","America/Ojinaga","America/Denver America/Boise America/Shiprock","MST7MDT"] },
    //"Central America Standard Time" : { utcOffset: "-06:00", olsonIds: ["America/Guatemala","America/Belize","America/Costa_Rica","Pacific/Galapagos","America/Guatemala","America/Tegucigalpa","America/Managua","America/El_Salvador","Etc/GMT+6"] },
    "US/Central" : { /*utcOffset: "-06:00",*/ olsonIds: ["America/Chicago","America/Winnipeg America/Rainy_River America/Rankin_Inlet America/Resolute","America/Matamoros","America/Chicago America/Indiana/Knox America/Indiana/Tell_City America/Menominee America/North_Dakota/Beulah America/North_Dakota/Center America/North_Dakota/New_Salem","CST6CDT"] },
    //"Central Standard Time (Mexico)" : { utcOffset: "-06:00", olsonIds: ["America/Mexico_City","America/Mexico_City America/Bahia_Banderas America/Cancun America/Merida America/Monterrey"] },
    //"Canada Central Standard Time" : { utcOffset: "-06:00", olsonIds: ["America/Regina","America/Regina America/Swift_Current"] },
    //"SA Pacific Standard Time" : { utcOffset: "-05:00", olsonIds: ["America/Bogota","America/Coral_Harbour","America/Bogota","America/Guayaquil","America/Port-au-Prince","America/Jamaica","America/Cayman","America/Panama","America/Lima","Etc/GMT+5"] },
    "US/Eastern" : { /*utcOffset: "-05:00",*/ olsonIds: ["America/New_York","America/Nassau","America/Toronto America/Iqaluit America/Montreal America/Nipigon America/Pangnirtung America/Thunder_Bay","America/Grand_Turk","America/New_York America/Detroit America/Indiana/Petersburg America/Indiana/Vincennes America/Indiana/Winamac America/Kentucky/Monticello America/Louisville","EST5EDT"] },
    "US/Eastern" : { /*utcOffset: "-05:00",*/ olsonIds: ["America/Indianapolis","America/Indianapolis America/Indiana/Marengo America/Indiana/Vevay"] },
    //"Venezuela Standard Time" : { utcOffset: "-04:30", olsonIds: ["America/Caracas","America/Caracas"] },
    //"Paraguay Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Asuncion","America/Asuncion"] },
    //"Atlantic Standard Time" : { utcOffset: "-04:00", olsonIds: ["America/Halifax","Atlantic/Bermuda","America/Halifax America/Glace_Bay America/Goose_Bay America/Moncton","America/Thule"] },
    //"Central Brazilian Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Cuiaba","America/Cuiaba America/Campo_Grande"] },
    //"SA Western Standard Time" : { utcOffset: "-04:00", olsonIds: ["America/La_Paz","America/Antigua","America/Anguilla","America/Aruba","America/Barbados","America/St_Barthelemy","America/La_Paz","America/Kralendijk","America/Manaus America/Boa_Vista America/Eirunepe America/Porto_Velho America/Rio_Branco","America/Blanc-Sablon","America/Curacao","America/Dominica","America/Santo_Domingo","America/Grenada","America/Guadeloupe","America/Guyana","America/St_Kitts","America/St_Lucia","America/Marigot","America/Martinique","America/Montserrat","America/Puerto_Rico","America/Lower_Princes","America/Port_of_Spain","America/St_Vincent","America/Tortola","America/St_Thomas","Etc/GMT+4"] },
    //"Pacific SA Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Santiago","Antarctica/Palmer","America/Santiago"] },
    //"Newfoundland Standard Time" : { utcOffset: "-03:30", olsonIds: ["America/St_Johns","America/St_Johns"] },
    //"E. South America Standard Time" : { utcOffset: "-02:00", olsonIds: ["America/Sao_Paulo","America/Sao_Paulo"] },
    //"Argentina Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Buenos_Aires","America/Buenos_Aires America/Argentina/La_Rioja America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia America/Catamarca America/Cordoba America/Jujuy America/Mendoza"] },
    //"SA Eastern Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Cayenne","Antarctica/Rothera","America/Fortaleza America/Araguaina America/Belem America/Maceio America/Recife America/Santarem","Atlantic/Stanley","America/Cayenne","America/Paramaribo","Etc/GMT+3"] },
    //"Greenland Standard Time" : { utcOffset: "-03:00", olsonIds: ["America/Godthab","America/Godthab"] },
    //"Montevideo Standard Time" : { utcOffset: "-02:00", olsonIds: ["America/Montevideo","America/Montevideo"] },
    //"Bahia Standard Time" : { utcOffset: "-02:00", olsonIds: ["America/Bahia","America/Bahia"] },
    //"UTC-02" : { utcOffset: "-02:00", olsonIds: ["Etc/GMT+2","America/Noronha","Atlantic/South_Georgia","Etc/GMT+2"] },
    //"Azores Standard Time" : { utcOffset: "-01:00", olsonIds: ["Atlantic/Azores","America/Scoresbysund","Atlantic/Azores"] },
    //"Cape Verde Standard Time" : { utcOffset: "-01:00", olsonIds: ["Atlantic/Cape_Verde","Atlantic/Cape_Verde","Etc/GMT+1"] },
    //"Morocco Standard Time" : { utcOffset: "+00:00", olsonIds: ["Africa/Casablanca","Africa/Casablanca"] },
    "GMT" : { utcOffset: "+00:00", olsonIds: ["Etc/GMT","America/Danmarkshavn","Etc/GMT"] },
    "GMT" : { utcOffset: "+00:00", olsonIds: ["Europe/London","Atlantic/Canary","Atlantic/Faeroe","Europe/London","Europe/Guernsey","Europe/Dublin","Europe/Isle_of_Man","Europe/Jersey","Europe/Lisbon Atlantic/Madeira"] },
    "GMT" : { utcOffset: "+00:00", olsonIds: ["Atlantic/Reykjavik","Africa/Ouagadougou","Africa/Abidjan","Africa/El_Aaiun","Africa/Accra","Africa/Banjul","Africa/Conakry","Africa/Bissau","Atlantic/Reykjavik","Africa/Monrovia","Africa/Bamako","Africa/Nouakchott","Atlantic/St_Helena","Africa/Freetown","Africa/Dakar","Africa/Sao_Tome","Africa/Lome"] },
    //"W. Europe Standard Time" : { utcOffset: "+01:00", olsonIds: ["Europe/Berlin","Europe/Andorra","Europe/Vienna","Europe/Zurich","Europe/Berlin","Europe/Gibraltar","Europe/Rome","Europe/Vaduz","Europe/Luxembourg","Europe/Monaco","Europe/Malta","Europe/Amsterdam","Europe/Oslo","Europe/Stockholm","Arctic/Longyearbyen","Europe/San_Marino","Europe/Vatican"] },
    //"Central Europe Standard Time" : { utcOffset: "+01:00", olsonIds: ["Europe/Budapest","Europe/Tirane","Europe/Prague","Europe/Budapest","Europe/Podgorica","Europe/Belgrade","Europe/Ljubljana","Europe/Bratislava"] },
    //"Romance Standard Time" : { utcOffset: "+01:00", olsonIds: ["Europe/Paris","Europe/Brussels","Europe/Copenhagen","Europe/Madrid Africa/Ceuta","Europe/Paris"] },
    //"Central European Standard Time" : { utcOffset: "+01:00", olsonIds: ["Europe/Warsaw","Europe/Sarajevo","Europe/Zagreb","Europe/Skopje","Europe/Warsaw"] },
    //"W. Central Africa Standard Time" : { utcOffset: "+01:00", olsonIds: ["Africa/Lagos","Africa/Luanda","Africa/Porto-Novo","Africa/Kinshasa","Africa/Bangui","Africa/Brazzaville","Africa/Douala","Africa/Algiers","Africa/Libreville","Africa/Malabo","Africa/Niamey","Africa/Lagos","Africa/Ndjamena","Africa/Tunis","Etc/GMT-1"] },
    //"Namibia Standard Time" : { utcOffset: "+02:00", olsonIds: ["Africa/Windhoek","Africa/Windhoek"] },
    //"Jordan Standard Time" : { utcOffset: "+02:00", olsonIds: ["Asia/Amman","Asia/Amman"] },
    //"GTB Standard Time" : { utcOffset: "+02:00", olsonIds: ["Europe/Bucharest","Europe/Athens","Europe/Chisinau","Europe/Bucharest"] },
    //"Middle East Standard Time" : { utcOffset: "+02:00", olsonIds: ["Asia/Beirut","Asia/Beirut"] },
    //"Egypt Standard Time" : { utcOffset: "+02:00", olsonIds: ["Africa/Cairo","Africa/Cairo","Asia/Gaza Asia/Hebron"] },
    //"Syria Standard Time" : { utcOffset: "+02:00", olsonIds: ["Asia/Damascus","Asia/Damascus"] },
    //"South Africa Standard Time" : { utcOffset: "+02:00", olsonIds: ["Africa/Johannesburg","Africa/Bujumbura","Africa/Gaborone","Africa/Lubumbashi","Africa/Maseru","Africa/Tripoli","Africa/Blantyre","Africa/Maputo","Africa/Kigali","Africa/Mbabane","Africa/Johannesburg","Africa/Lusaka","Africa/Harare","Etc/GMT-2"] },
    //"FLE Standard Time" : { utcOffset: "+02:00", olsonIds: ["Europe/Kiev","Europe/Mariehamn","Europe/Sofia","Europe/Tallinn","Europe/Helsinki","Europe/Vilnius","Europe/Riga","Europe/Kiev Europe/Simferopol Europe/Uzhgorod Europe/Zaporozhye"] },
    //"Turkey Standard Time" : { utcOffset: "+02:00", olsonIds: ["Europe/Istanbul","Europe/Istanbul"] },
    //"Israel Standard Time" : { utcOffset: "+02:00", olsonIds: ["Asia/Jerusalem","Asia/Jerusalem"] },
    //"E. Europe Standard Time" : { utcOffset: "+02:00", olsonIds: ["Asia/Nicosia","Asia/Nicosia"] },
    //"Arabic Standard Time" : { utcOffset: "+03:00", olsonIds: ["Asia/Baghdad","Asia/Baghdad"] },
    //"Kaliningrad Standard Time" : { utcOffset: "+03:00", olsonIds: ["Europe/Kaliningrad","Europe/Minsk","Europe/Kaliningrad"] },
    //"Arab Standard Time" : { utcOffset: "+03:00", olsonIds: ["Asia/Riyadh","Asia/Bahrain","Asia/Kuwait","Asia/Qatar","Asia/Riyadh","Asia/Aden"] },
    //"E. Africa Standard Time" : { utcOffset: "+03:00", olsonIds: ["Africa/Nairobi","Antarctica/Syowa","Africa/Djibouti","Africa/Asmera","Africa/Addis_Ababa","Africa/Nairobi","Indian/Comoro","Indian/Antananarivo","Africa/Khartoum","Africa/Mogadishu","Africa/Juba","Africa/Dar_es_Salaam","Africa/Kampala","Indian/Mayotte","Etc/GMT-3"] },
    //"Iran Standard Time" : { utcOffset: "+03:30", olsonIds: ["Asia/Tehran","Asia/Tehran"] },
    //"Arabian Standard Time" : { utcOffset: "+04:00", olsonIds: ["Asia/Dubai","Asia/Dubai","Asia/Muscat","Etc/GMT-4"] },
    //"Azerbaijan Standard Time" : { utcOffset: "+04:00", olsonIds: ["Asia/Baku","Asia/Baku"] },
    //"Russian Standard Time" : { utcOffset: "+04:00", olsonIds: ["Europe/Moscow","Europe/Moscow Europe/Samara Europe/Volgograd"] },
    //"Mauritius Standard Time" : { utcOffset: "+04:00", olsonIds: ["Indian/Mauritius","Indian/Mauritius","Indian/Reunion","Indian/Mahe"] },
    //"Georgian Standard Time" : { utcOffset: "+04:00", olsonIds: ["Asia/Tbilisi","Asia/Tbilisi"] },
    //"Caucasus Standard Time" : { utcOffset: "+04:00", olsonIds: ["Asia/Yerevan","Asia/Yerevan"] },
    //"Afghanistan Standard Time" : { utcOffset: "+04:30", olsonIds: ["Asia/Kabul","Asia/Kabul"] },
    //"Pakistan Standard Time" : { utcOffset: "+05:00", olsonIds: ["Asia/Karachi","Asia/Karachi"] },
    //"West Asia Standard Time" : { utcOffset: "+05:00", olsonIds: ["Asia/Tashkent","Antarctica/Mawson","Asia/Oral Asia/Aqtau Asia/Aqtobe","Indian/Maldives","Indian/Kerguelen","Asia/Dushanbe","Asia/Ashgabat","Asia/Tashkent Asia/Samarkand","Etc/GMT-5"] },
    //"India Standard Time" : { utcOffset: "+05:30", olsonIds: ["Asia/Calcutta","Asia/Calcutta"] },
    //"Sri Lanka Standard Time" : { utcOffset: "+05:30", olsonIds: ["Asia/Colombo","Asia/Colombo"] },
    //"Nepal Standard Time" : { utcOffset: "+05:45", olsonIds: ["Asia/Katmandu","Asia/Katmandu"] },
    //"Central Asia Standard Time" : { utcOffset: "+06:00", olsonIds: ["Asia/Almaty","Antarctica/Vostok","Indian/Chagos","Asia/Bishkek","Asia/Almaty Asia/Qyzylorda","Etc/GMT-6"] },
    //"Bangladesh Standard Time" : { utcOffset: "+06:00", olsonIds: ["Asia/Dhaka","Asia/Dhaka","Asia/Thimphu"] },
    //"Ekaterinburg Standard Time" : { utcOffset: "+06:00", olsonIds: ["Asia/Yekaterinburg","Asia/Yekaterinburg"] },
    //"Myanmar Standard Time" : { utcOffset: "+06:30", olsonIds: ["Asia/Rangoon","Indian/Cocos","Asia/Rangoon"] },
    //"SE Asia Standard Time" : { utcOffset: "+07:00", olsonIds: ["Asia/Bangkok","Antarctica/Davis","Indian/Christmas","Asia/Jakarta Asia/Pontianak","Asia/Phnom_Penh","Asia/Vientiane","Asia/Hovd","Asia/Bangkok","Asia/Saigon","Etc/GMT-7"] },
    //"N. Central Asia Standard Time" : { utcOffset: "+07:00", olsonIds: ["Asia/Novosibirsk","Asia/Novosibirsk Asia/Novokuznetsk Asia/Omsk"] },
    //"China Standard Time" : { utcOffset: "+08:00", olsonIds: ["Asia/Shanghai","Asia/Shanghai Asia/Chongqing Asia/Harbin Asia/Kashgar Asia/Urumqi","Asia/Hong_Kong","Asia/Macau"] },
    //"North Asia Standard Time" : { utcOffset: "+08:00", olsonIds: ["Asia/Krasnoyarsk","Asia/Krasnoyarsk"] },
    //"Singapore Standard Time" : { utcOffset: "+08:00", olsonIds: ["Asia/Singapore","Asia/Brunei","Asia/Makassar","Asia/Kuala_Lumpur Asia/Kuching","Asia/Manila","Asia/Singapore","Etc/GMT-8"] },
    //"W. Australia Standard Time" : { utcOffset: "+08:00", olsonIds: ["Australia/Perth","Antarctica/Casey","Australia/Perth"] },
    //"Taipei Standard Time" : { utcOffset: "+08:00", olsonIds: ["Asia/Taipei","Asia/Taipei"] },
    //"Ulaanbaatar Standard Time" : { utcOffset: "+08:00", olsonIds: ["Asia/Ulaanbaatar","Asia/Ulaanbaatar Asia/Choibalsan"] },
    //"North Asia East Standard Time" : { utcOffset: "+09:00", olsonIds: ["Asia/Irkutsk","Asia/Irkutsk"] },
    //"Tokyo Standard Time" : { utcOffset: "+09:00", olsonIds: ["Asia/Tokyo","Asia/Jayapura","Asia/Tokyo","Pacific/Palau","Asia/Dili","Etc/GMT-9"] },
    //"Korea Standard Time" : { utcOffset: "+09:00", olsonIds: ["Asia/Seoul","Asia/Pyongyang","Asia/Seoul"] },
    //"Cen. Australia Standard Time" : { utcOffset: "+10:30", olsonIds: ["Australia/Adelaide","Australia/Adelaide Australia/Broken_Hill"] },
    //"AUS Central Standard Time" : { utcOffset: "+09:30", olsonIds: ["Australia/Darwin","Australia/Darwin"] },
    //"E. Australia Standard Time" : { utcOffset: "+10:00", olsonIds: ["Australia/Brisbane","Australia/Brisbane Australia/Lindeman"] },
    //"AUS Eastern Standard Time" : { utcOffset: "+11:00", olsonIds: ["Australia/Sydney","Australia/Sydney Australia/Melbourne"] },
    //"West Pacific Standard Time" : { utcOffset: "+10:00", olsonIds: ["Pacific/Port_Moresby","Antarctica/DumontDUrville","Pacific/Truk","Pacific/Guam","Pacific/Saipan","Pacific/Port_Moresby","Etc/GMT-10"] },
    //"Tasmania Standard Time" : { utcOffset: "+11:00", olsonIds: ["Australia/Hobart","Australia/Hobart Australia/Currie"] },
    //"Yakutsk Standard Time" : { utcOffset: "+10:00", olsonIds: ["Asia/Yakutsk","Asia/Yakutsk"] },
    //"Central Pacific Standard Time" : { utcOffset: "+11:00", olsonIds: ["Pacific/Guadalcanal","Antarctica/Macquarie","Pacific/Ponape Pacific/Kosrae","Pacific/Noumea","Pacific/Guadalcanal","Pacific/Efate","Etc/GMT-11"] },
    //"Vladivostok Standard Time" : { utcOffset: "+11:00", olsonIds: ["Asia/Vladivostok","Asia/Vladivostok Asia/Sakhalin"] },
    //"New Zealand Standard Time" : { utcOffset: "+13:00", olsonIds: ["Pacific/Auckland","Antarctica/South_Pole Antarctica/McMurdo","Pacific/Auckland"] },
    //"UTC+12" : { utcOffset: "+12:00", olsonIds: ["Etc/GMT-12","Pacific/Tarawa","Pacific/Majuro Pacific/Kwajalein","Pacific/Nauru","Pacific/Funafuti","Pacific/Wake","Pacific/Wallis","Etc/GMT-12"] },
    //"Fiji Standard Time" : { utcOffset: "+13:00", olsonIds: ["Pacific/Fiji","Pacific/Fiji"] },
    //"Magadan Standard Time" : { utcOffset: "+12:00", olsonIds: ["Asia/Magadan","Asia/Magadan Asia/Anadyr Asia/Kamchatka"] },
    //"Tonga Standard Time" : { utcOffset: "+13:00", olsonIds: ["Pacific/Tongatapu","Pacific/Enderbury","Pacific/Tongatapu","Etc/GMT-13"] },
    //"Samoa Standard Time" : { utcOffset: "+14:00", olsonIds: ["Pacific/Apia"] }

};
;
var RealtimeHubClientNewFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    };

    function init() {
        _factoryData.defaultInstanceOptions = {
            url: null,
            logToConsole: true,
            callbacks: {
                onConnectionStateChanged: function(isConnected) { }
            }
        };

        // script tag that includes signalr client should call this fn on error
        _factoryData.onErrorLoadingSignalRClient = function (ths, event) {
    //        this.signalRLoaded = false;
    //        var script = ths.src;

    //        var html =
    //            '<div class="signalrerror btn-group dropdown caret_down" style="position: relative; left: 160px; top: -2px; z-index: 1000;">' +
    //                 '<i class="clickable dropdown-toggle noselect si-notifications" data-toggle="dropdown" style="padding: 1px; font-size: 26px; background-color: white; color: red; border-radius: 4px;"/>'
				//'</div>',

    //        $('.navbar-brand').append(html);
    //        var firstClick = true;
    //        $('.navbar-brand').find('.signalrerror > i').click(function (e) {
    //            if (!firstClick) return;
    //            firstClick = false;
    //            $(this).parent().append(
				//	'<ul class="dropdown_caret dropdown-menu" role="menu" style="margin-left: 3px; margin-top: 5px; color: #333; text-shadow: none; padding: 8px; width: 500px; line-height: 24px;">' +
    //                    'Unable to load <a target="_blank" href="' + ths.src + '">' + ths.src + '</a>.<br/>' +
    //                    'Some features of the site are disabled because of this.' +
    //                    '<br/><br/>' +
    //                    '<center><button class="btn btn-sm btn-default diagnose">Try to get diagnostic information</button></center>' +                        
    //                    '<div class="diagnostics" style="font-family: Arial; font-size: 12px; line-height: initial; background-color: #FFA; padding: 8px; border-radius: 4px; margin-top: 8px; display: none; white-space:breakword; word-wrap: break-word;"></div>' +
				//	'</ul>'
    //            );
                
    //            $('.navbar-brand').find('.signalrerror .diagnose').click(function () {
    //                $('.navbar-brand').find('.signalrerror .diagnostics').hide();
    //                fetch(script)
    //                    .then(function (resp) {                            
    //                        $('.navbar-brand').find('.signalrerror .diagnostics').show().html('Response: ' + JSON.stringify({
    //                            url: resp.url,
    //                            type: resp.type,
    //                            status: resp.status,
    //                            statusText: resp.statusText
    //                        }));
    //                    })
    //                    .catch(function(err) {
    //                        $('.navbar-brand').find('.signalrerror .diagnostics').show().html('Error: ' + err);
    //                    });
    //                return false;
    //            });
    //        });

    //        signalRLoaded = false;
        }

        _factoryData.isInitialized = true;
    };

    var _factoryData = {
        isInitialized: false,
        signalRLoaded: true,
                
        onErrorLoadingSignalRClient: function (ths) {}
    };

    return _factoryInterface;
}());


RealtimeHubClientNewFactory.createInstance = function (options) {

    var _interface = {
        getConnectionId: function () { return _connection.hub.id; },
        getIsConnected: function () { return _isConnected; },
        getClientId: function () { return _clientIdIndex++; },        
        conversations: function () { return this.getHub('conversations'); },
        conversationMessages: function () { return this.getHub('conversationMessages'); },
        conversationModifications: function () { return this.getHub('conversationModifications'); },
        conversationLanguage: function () { return this.getHub('conversationLanguage'); },
        versionSets: function () { return this.getHub('versionSets'); },
        conversationFlowTransitions: function () { return this.getHub('ConversationFlowTransitions'); },
        exportDefinitions: function () { return this.getHub('exportDefinitions'); },
        searchIndexDocuments: function () { return this.getHub('searchIndexDocuments'); },
        notifications: function () { return this.getHub('notifications'); },
        userInfo: function () { return this.getHub('userInfo'); },
        uiViewers: function () { return this.getHub('uiViewers'); },
        agentWorkspace: function () { return this.getHub('agentWorkspace'); },
        messengerEvents: function () { return this.getHub('messengerEvents'); },
        botOperations: function () { return this.getHub('botOperations'); },
        
        // maybe use this going forward?
        getHub: function (type) { 
            //var realHub = _u.find(_hubContainers, function (o) { return o.type == type; }).hub;
            var realHub = null;
            for (var i = 0; i < _hubContainers.length; i++) {
                var o = _hubContainers[i];
                if (o.type == type) {
                    realHub = o.hub;
                    break;
                }
            }

            if (realHub != null) return realHub;

            // if hub is null (i.e. signalr didn't load properly), then return a "nop hub" that doesnt do anything.  this way client code doesn't need to do null checking everywhere
            // (on the other hand, if client code actually NEEDS to do client checking then this won't work anymore)
            var nopHub = {
                subscribe: function () {},
                unsubscribe: function () {}
            };
            return nopHub;
        }
    };

    var _factoryData = this.getFactoryData();
    var _options = Object.assign({}, _factoryData.defaultInstanceOptions, options);

    var _prettyStates = { 0: 'connecting', 1: 'connected', 2: 'reconnecting', 4: 'disconnected' };

    var _hubContainers = [
        { type: 'conversationMessages', hubFactory: ConversationMessageHubNewFactory, hub: null },
        { type: 'conversationLanguage', hubFactory: ConversationLanguageHubNewFactory, hub: null },
        { type: 'conversationModifications', hubFactory: ConversationModificationHubNewFactory, hub: null },
        { type: 'conversations', hubFactory: ConversationHubNewFactory, hub: null },
        { type: 'versionSets', hubFactory: VersionSetsHubNewFactory, hub: null },
        { type: 'ConversationFlowTransitions', hubFactory: ConversationFlowTransitionHubNewFactory, hub: null },
        { type: 'exportDefinitions', hubFactory: ExportDefinitionsHubNewFactory, hub: null },
        { type: 'searchIndexDocuments', hubFactory: SearchIndexDocumentsHubNewFactory, hub: null },
        { type: 'notifications', hubFactory: NotificationsHubNewFactory, hub: null },
        { type: 'userInfo', hubFactory: UserInfoHubNewFactory, hub: null },
        { type: 'uiViewers', hubFactory: UiViewersHubNewFactory, hub: null },
        { type: 'agentWorkspace', hubFactory: AgentWorkspaceHubNewFactory, hub: null },
        { type: 'messengerEvents', hubFactory: MessengerEventHubNewFactory, hub: null },
        { type: 'botOperations', hubFactory: BotOperationsHubNewFactory, hub: null },
    ];

    var _initializedHubCount = 0;
    var _totalHubCount = _hubContainers.length;

    var _isConnected = false;
    var _destroyed = false;

    var _clientIdIndex = 0;

    var _connection = null;

    function log(msg) {
        if (_options.logToConsole) console.log(msg);
    }

    function init() {
        _destroyed = false;

        if (!_factoryData.signalRLoaded) {
            log('Error loading SignalR client - cannot do realtime communication features!');
            return;
        }

        _connection =
            new signalR.HubConnectionBuilder()
                .withUrl(_options.url + "/eventHub")
                .withAutomaticReconnect() // use default reconnection intervals
                .configureLogging(signalR.LogLevel.Information)
                .build();        

        _connection.onreconnecting(function (error) {
            onStateChanged(2);
        });

        _connection.onreconnected(function (error) {
            onStateChanged(1);
        });

        _connection.onclose(function (error) {
            onStateChanged(4);

            start();
        });               

        _hubContainers.forEach(function (hubContainer) {
            if (_connection != null) {
                hubContainer.hub = hubContainer.hubFactory.createInstance({
                    connection: _connection,
                    logToConsole: _options.logToConsole,
                    callbacks: {
                        onInitialized: onHubInitialized
                    }
                });
            }
        });
    }

    var onStateChanged = function (state) {
        log('SignalR state changed to ' + _prettyStates[state]);

        var newIsConnected = state == 1;
        if (_isConnected == newIsConnected) {
            return;
        }

        _isConnected = newIsConnected;
        _hubContainers.forEach(function (hubContainer) {
            hubContainer.hub.onConnectionStateChanged(_isConnected);
        });
        _options.callbacks.onConnectionStateChanged(_isConnected);
    };

    function start() {
        _connection
            .start()
            .then(function() {
                onStateChanged(1);
            })
            .catch(function (error) {
                setTimeout(start, 5000);
            });
    }

    _interface.destroy = function() {
        _destroyed = true;
        _connection.stop();
    };    

    function onHubInitialized() {
        _initializedHubCount++;
        if (_initializedHubCount < _totalHubCount) {
            return;
        }

        start();
    }

    init();
    return _interface;
};
var RealtimeHubNewFactory = (function () {
    var _factoryInterface = {
        createInstance: function (options) { return null; },
        getFactoryData: function () { if (!_factoryData.isInitialized) { init(); } return _factoryData; }
    };

    function init() {
        _factoryData.defaultInstanceOptions = {
            name: 'RealTimeHub',
            connection: null, // passed in from RealtimeHubClient
            subscribeFn: function (connection, subscription, onCompletedFn) { },                         // how do we subscribe to hub?
            unsubscribeFn: function (connection, subscription) { },                                      // how do we unsubscribe from hub?
            matchUpdateFn: function (objectType, keyData, incomingData) { return false; },                  // how do we check if an incoming update applies to this subscription?
            matchSubscriptionFn: function (objectType, keyData, subscriptionKeyData) { return false; },     // how do we check if a subscription matches oa given key?
            logToConsole: true,

            callbacks: {
                onInitialized: function () { } // RealTimeHubClient needs to implement and pass in this
            }
        };

        _factoryData.isInitialized = true;
    };

    var _factoryData = {
        isInitialized: false
    };

    return _factoryInterface;
}());


RealtimeHubNewFactory.createInstance = function (options) {
    var _interface = {
        onConnectionStateChanged: function (isConnected) { onConnectionStateChanged(isConnected); },
        unsubscribe: function (clientId) { return unsubscribe(clientId); },
    };

    var _factoryData = this.getFactoryData();
    var _options = Object.assign({}, _factoryData.defaultInstanceOptions, options);
    
    var _connection = null;
    var _connected = false;
    var _subscriptions = [];

    function log(msg) {
        if (_options.logToConsole) console.log(msg);
    }

    _interface.name = _options.name;

    function init() {
        _connection = _options.connection;

        var getSubscribers = function (update) {            
            var message = JSON.parse(update.objectJson);

            var subscribers = [];
            for (var i = 0; i < _subscriptions.length; i++) {
                var s = _subscriptions[i];
                if (_options.matchUpdateFn(s.keyData, message)) {
                    subscribers.push(s);
                }
            }
            return subscribers;
        }

        _connection.on('onEvent', function (update) {
            if (update.objectType != _options.objectType) {
                return;
            }
            var eventType = update.eventType;            
            var message = JSON.parse(update.objectJson);

            switch (eventType) {
                case 'CREATE': // added                    
                    var subscribers = getSubscribers(update);
                    subscribers.forEach(function (subscriber) {
                        if (typeof subscriber.callbacks.onAdded != 'undefined') {
                            log('SignalR ' + _options.name + ' added received.');
                            subscriber.callbacks.onAdded(message);
                        }
                    });
                    break;
                case 'UPDATE': // udpated                    
                    var subscribers = getSubscribers(update);
                    subscribers.forEach(function (subscriber) {
                        if (typeof subscriber.callbacks.onUpdated != 'undefined') {
                            log('SignalR ' + _options.name + ' updated received.');
                            subscriber.callbacks.onUpdated(message);
                        }
                    });
                    break;
                case 'DELETE': // deleted                    
                    var subscribers = getSubscribers(update);
                    subscribers.forEach(function (subscriber) {
                        if (typeof subscriber.callbacks.onDeleted != 'undefined') {
                            log('SignalR ' + _options.name + ' deleted received.');
                            subscriber.callbacks.onDeleted(message);
                        }
                    });
                    break;
            }            
        });        

        _options.callbacks.onInitialized();
    }

    function onConnectionStateChanged(isConnected) {
        log('SignalR ' + _options.name + ' on connection state changed');
        _connected = isConnected
        if (_connected) {
            _subscriptions.forEach(function (subData) {
                subscribePending(subData);
            });
        }

        _subscriptions.forEach(function (subData) {
            if (typeof subData.callbacks.onConnectionStateChanged != 'undefined') {
                subData.callbacks.onConnectionStateChanged(_connected);
            }
        });
    }

    function subscribePending(subscription) {
        var onSubscribedFn = subscription.callbacks.onSubscribed;
        if (typeof onSubscribedFn == 'undefined') {
            onSubscribedFn = function () {}
        }
        _options.subscribeFn(_connection, subscription, onSubscribedFn);
        log('SignalR ' + _options.name + ' subscribing to key ' + JSON.stringify(subscription.keyData) + '.');
    }

    _interface.subscribeToKey = function(clientId, objectType, keyData, realtimeToken, callbacks) {
        if (typeof callbacks.onConnectionStateChanged != 'undefined') {
            callbacks.onConnectionStateChanged(_connected);
        }

        var existing;
        for (var i = 0; i < _subscriptions.length; i++) {
            var s = _subscriptions[i];
            if (s.clientId == clientId && _options.matchSubscriptionFn(keyData, s.keyData)) {
                existing = s;
                break;
            }
        }

        if (typeof existing != 'undefined') {
            log('SignalR ' + _options.name + ' client ' + clientId + ' already subscribed to key ' + JSON.stringify(keyData));
            return;
        }

        var newSubscription = {
            clientId: clientId,
            objectType: objectType,
            keyData: keyData,
            realtimeToken: realtimeToken,
            callbacks: callbacks
        };

        _subscriptions.push(newSubscription);        
       
        if (_connected) {
            subscribePending(newSubscription);
        } else {
            log('SignalR ' + _options.name + ' client ' + clientId + ' queued subscription to key ' + JSON.stringify(keyData));
        }
    }

    function unsubscribe(clientId) {
        var removed = [];
        var remaining = [];
        for (var i = 0; i < _subscriptions.length; i++) {
            var s = _subscriptions[i];
            if (s.clientId == clientId) {
                removed.push(s);
            } else {
                remaining.push(s);
            }
        }
        _subscriptions = remaining;            

        removed.forEach(function (s) {            
            var keyData = s.keyData;
                
            var anyOtherSubscriber = false;
            for (var i = 0; i < _subscriptions.length; i++) {
                var sub = _subscriptions[i];
                if (_options.matchSubscriptionFn(keyData, sub.keyData)) {
                    anyOtherSubscriber = true;
                    break;
                }
            }

            if (anyOtherSubscriber) {
                log('SignalR ' + _options.name + ' will not unsubscribe from key ' + JSON.stringify(keyData) + '. ' + anyOtherSubscriber.clientId + ' is subscribed still.');          
            } else {
                _options.unsubscribeFn(_connection, s);
                log('SignalR ' + _options.name + ' client ' + clientId + ' unsubscribed to key ' + JSON.stringify(keyData) + '.');
            }
        });
    }

    init();
    return _interface;
}

// --------------------------------
// implement various hubs by customizing behavior of RealtimeHub:

var ConversationMessageHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'CONVERSATION_MESSAGE';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'ConversationMessageHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription, onCompletedFn) { 
                return connection
                    .invoke('subscribe', objectType, subscription.keyData.convId, null, subscription.realtimeToken, subscription.keyData.metadataJson)
                    .then(function(backfillData) {
                        onCompletedFn(backfillData, null);
                    })
                    .catch(function (err) {
                        onCompletedFn(null, err);
                    });
            },
            unsubscribeFn: function (subscription) {
                connection.invoke('unsubscribe', objectType, subscription.keyData.convId);
            },
            matchUpdateFn: function (keyData, updateData) {
                return (keyData.convId == updateData.conversationId); 
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { 
                return (keyData.convId == subscriptionKeyData.convId); 
            }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, convId, metadataJson, realtimeToken, callbacks) {
            var keyData = { convId: convId, metadataJson: metadataJson };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var ConversationLanguageHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'CONVERSATION_LANGUAGE';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'ConversationLanguageHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) {
                connection.invoke('subscribe', objectType, subscription.keyData.convId, null, subscription.realtimeToken, null);
            },
            unsubscribeFn: function (connection, subscription) {
                connection.invoke('unsubscribe', objectType, subscription.keyData.convId);
            },
            matchUpdateFn: function (keyData, updateData) {
                return (keyData.convId == updateData.conversationId);
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) {
                return (keyData.convId == subscriptionKeyData.convId);
            }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, convId, realtimeToken, callbacks) {
            var keyData = { convId: convId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var ConversationHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'CONVERSATION';

        var getRealtimeToken = function (convo) {
            if (convo.platformId == -11) {
                var firstUserId = convo.serviceId.split(',')[0];
                return 'UMCU.' + firstUserId;
            } else {
                return 'CCU.' + convo.companyId;
            }
        }        

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'ConversationHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.key, subscription.keyData.filter, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.key); },
            
            // note: matching functions for this hub only check the company id, not the entire key (company id + filter)
            matchUpdateFn: function (keyData, updateData) { return (keyData.key == getRealtimeToken(updateData)); },            
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.key == subscriptionKeyData.key); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, key, filter, realtimeToken, callbacks) {
            var keyData = {
                key: key,
                filter: filter
            };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var ConversationModificationHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'CONVERSATION_MODIFICATION';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'ConversationModificationHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.convId, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.convId); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.convId == updateData.conversationId); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) {  return (keyData.convId == subscriptionKeyData.convId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, convId, realtimeToken, callbacks) {
            var keyData = { convId: convId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var VersionSetsHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'VERSION_SET';

        options = Object.assign({}, options, {
            name: 'VersionSetsHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.companyId, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.companyId); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.companyId == updateData.companyId); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.convId == subscriptionKeyData.convId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, companyId, realtimeToken, onAdded, onConnectionStateChanged) {
            var keyData = { companyId: companyId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, onAdded, onConnectionStateChanged);
        };

        return instance;
    }
}

// NO LONGER USED
var ConversationFlowTransitionHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'CONVERSATION_FLOW_TRANSITION';

        options = Object.assign({}, options, {
            name: 'ConversationFlowTransitionHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.flowObjectId, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.flowObjectId); },
            matchUpdateFn: function (keyData, updateData) {
                if (updateData.from != null) {
                    if (keyData.flowObjectId == updateData.from.flowObjectId ||
                        keyData.flowObjectId == updateData.from.rootFlowObjectId) {
                        return true;
                    }
                }
                if (updateData.to != null) {
                    if (keyData.flowObjectId == updateData.to.flowObjectId ||
                        keyData.flowObjectId == updateData.to.rootFlowObjectId) {
                        return true;
                    }
                }
                return false;
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.flowObjectId == subscriptionKeyData.flowObjectId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, flowObjectId, realtimeToken, callbacks) {
            var keyData = { flowObjectId: flowObjectId };
            return instance.subscribeToKey(clientId, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}


var ExportDefinitionsHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'EXPORT_DEFINITION';

        options = Object.assign({}, options, {
            name: 'ExportDefinitionsHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.companyId, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.companyId); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.companyId == updateData.companyId); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.companyId == subscriptionKeyData.companyId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, companyId, realtimeToken, onAdded, onConnectionStateChanged) {
            var keyData = { companyId: companyId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, onAdded, onConnectionStateChanged);
        };

        return instance;
    }
}

var SearchIndexDocumentsHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'SEARCH_INDEX_DOCUMENT';

        options = Object.assign({}, options, {
            name: 'SearchIndexDocumentsHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.searchIndexId, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.searchIndexId); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.searchIndexId == updateData.searchIndexId); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.searchIndexId == subscriptionKeyData.searchIndexId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, searchIndexId, realtimeToken, onAdded, onConnectionStateChanged) {
            var keyData = { searchIndexId: searchIndexId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, onAdded, onConnectionStateChanged);
        };

        return instance;
    }
}

// NO LONGER USED
var NotificationsHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'NOTIFICATION';

        options = Object.assign({}, options, {
            name: 'NotificationHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.recipientIdentifier, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.recipientIdentifier); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.recipientIdentifier == updateData.recipientIdentifier); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.recipientIdentifier == subscriptionKeyData.recipientIdentifier); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, recipientIdentifier, realtimeToken, onAdded, onConnectionStateChanged) {
            var keyData = { recipientIdentifier: recipientIdentifier };
            return instance.subscribeToKey(clientId, keyData, realtimeToken, onAdded, onConnectionStateChanged);
        };
        return instance;
    }
}

var UserInfoHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'USER_INFO';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'UserInfoHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.userid, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.userid); },
            matchUpdateFn: function (keyData, updateData) {
                return (keyData.userid == updateData.userid);
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) {
                return (keyData.userid == subscriptionKeyData.userid);
            }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, userid, realtimeToken, callbacks) {
            var keyData = { userid: userid };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var UiViewersHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'UI_VIEWERS';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'UiViewerHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.id, null, subscription.realtimeToken, subscription.keyData.metadataJson); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.id); },
            matchUpdateFn: function (keyData, updateData) {
                return (keyData.id == updateData.id);
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) {
                return (keyData.id == subscriptionKeyData.id);
            }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, id, metadataJson, realtimeToken, callbacks) {
            var keyData = { id: id, metadataJson: metadataJson };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var AgentWorkspaceHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'AGENT_WORKSPACE';

        // patch in some advanced options we hide from client:
        options = Object.assign({}, options, {
            name: 'AgentWorkspaceHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.id, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.id); },
            matchUpdateFn: function (keyData, updateData) {
                return (keyData.id == updateData.agentSetId);
            },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) {
                return (keyData.id == subscriptionKeyData.agentSetId);
            }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, id, realtimeToken, callbacks) {
            var keyData = { id: id };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, callbacks);
        };

        return instance;
    }
}

var MessengerEventHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'MESSENGER_EVENT';

        // patch in some advanced options we hide from client:
        var _options = Object.assign({}, options, {
            name: 'MessengerEventHub',
            objectType: objectType,
        });

        var _toSend = [];
        var _isConnected = false;

        var flush = function () {
            if (_isConnected) {
                if (_toSend.length > 0) {
                    _options.connection.invoke('sendEvents', JSON.stringify(_toSend));
                    _toSend = [];
                }
            }            
        };

        var instance = {};

        instance.register = function (messengerChannelServiceId) {
            _messengerChannelServiceId = messengerChannelServiceId;
        };

        instance.sendEvents = function (events) {
            _toSend = _toSend.concat(events);

            flush();
        };

        instance.onConnectionStateChanged = function(isConnected) {
            _isConnected = isConnected;

            flush();
        };        

        setTimeout(function () { _options.callbacks.onInitialized(); }, 1);        

        return instance;
    }
}

var BotOperationsHubNewFactory = {
    createInstance: function (options) {
        var objectType = 'BOT_OPERATION';

        options = Object.assign({}, options, {
            name: 'BotOperationsHub',
            objectType: objectType,
            subscribeFn: function (connection, subscription) { connection.invoke('subscribe', objectType, subscription.keyData.companyId, null, subscription.realtimeToken, null); },
            unsubscribeFn: function (connection, subscription) { connection.invoke('unsubscribe', objectType, subscription.keyData.companyId); },
            matchUpdateFn: function (keyData, updateData) { return (keyData.companyId == updateData.companyId); },
            matchSubscriptionFn: function (keyData, subscriptionKeyData) { return (keyData.companyId == subscriptionKeyData.companyId); }
        });

        // create hub instance, tack on a more convenient subscribe function:
        var instance = RealtimeHubNewFactory.createInstance(options);

        instance.subscribe = function (clientId, companyId, realtimeToken, onAdded, onConnectionStateChanged) {
            var keyData = { companyId: companyId };
            return instance.subscribeToKey(clientId, objectType, keyData, realtimeToken, onAdded, onConnectionStateChanged);
        };

        return instance;
    }
};
