吴树波 1 mese fa
commit
2ee8487bd9
100 ha cambiato i file con 14712 aggiunte e 0 eliminazioni
  1. 14 0
      .gitignore
  2. 17 0
      Build.md
  3. 358 0
      Deploy.txt
  4. 3 0
      LICENSE
  5. 168 0
      README.md
  6. 5 0
      build.bat
  7. 1840 0
      docs/ccPhoneBarSocket.js
  8. BIN
      docs/icon/extractaudio.png
  9. BIN
      docs/icon/video.png
  10. BIN
      docs/images/file.gif
  11. BIN
      docs/images/keyboard.png
  12. BIN
      docs/images/logo.jpg
  13. BIN
      docs/images/minus.gif
  14. BIN
      docs/images/mute.jpg
  15. BIN
      docs/images/mutein.ico
  16. BIN
      docs/images/network-callcenter.png
  17. BIN
      docs/images/network-tra.png
  18. BIN
      docs/images/no_video.jpg
  19. BIN
      docs/images/phone-bar.png
  20. BIN
      docs/images/phone.png
  21. BIN
      docs/images/phonebar/busy_enable.png
  22. BIN
      docs/images/phonebar/call.png
  23. BIN
      docs/images/phonebar/conference.png
  24. BIN
      docs/images/phonebar/consultation.png
  25. BIN
      docs/images/phonebar/hangup_enable.png
  26. BIN
      docs/images/phonebar/hold.png
  27. BIN
      docs/images/phonebar/online.png
  28. BIN
      docs/images/phonebar/reset.png
  29. BIN
      docs/images/phonebar/setFree.png
  30. BIN
      docs/images/phonebar/transfer.png
  31. BIN
      docs/images/phonebar/unhold.png
  32. BIN
      docs/images/plus.gif
  33. BIN
      docs/images/process-flow.png
  34. BIN
      docs/images/transparent-25-bg.png
  35. BIN
      docs/images/transparent-50-bg.png
  36. BIN
      docs/images/transparent-black-75-bg.png
  37. BIN
      docs/images/unmute.jpg
  38. BIN
      docs/images/video.jpg
  39. BIN
      docs/images/wechat.png
  40. BIN
      docs/images/wechat2.png
  41. 9800 0
      docs/jquery-1.11.0.js
  42. 8 0
      docs/kb/customerSortTips.txt
  43. 103 0
      docs/kb/faq-en.txt
  44. 63 0
      docs/kb/faq.txt
  45. 44 0
      docs/kb/llmTips-en.txt
  46. 36 0
      docs/kb/llmTips.txt
  47. 97 0
      docs/manual/faq.md
  48. BIN
      docs/manual/faq/callrecord-list.png
  49. BIN
      docs/manual/faq/calltask-list.png
  50. BIN
      docs/manual/faq/dify-app-type.png
  51. BIN
      docs/manual/faq/empty-number-detection-enabled.png
  52. BIN
      docs/manual/faq/llm-account-dify.png
  53. BIN
      docs/manual/faq/profile-list.png
  54. BIN
      docs/manual/faq/profile-menu.png
  55. BIN
      docs/manual/faq/profile-update.png
  56. BIN
      docs/manual/faq/tts-update.png
  57. BIN
      docs/manual/images/20250728084822.png
  58. BIN
      docs/manual/images/20250728090628.png
  59. BIN
      docs/manual/images/20250728090818.png
  60. BIN
      docs/manual/images/20250728091000.png
  61. BIN
      docs/manual/images/20250728092159.png
  62. BIN
      docs/manual/images/20250728092309.png
  63. BIN
      docs/manual/images/20250728092457.png
  64. BIN
      docs/manual/images/20250728092937.png
  65. BIN
      docs/manual/images/20250728093730.png
  66. BIN
      docs/manual/images/20250728094246.png
  67. BIN
      docs/manual/images/20250728094559.png
  68. BIN
      docs/manual/images/20250728094734.png
  69. BIN
      docs/manual/images/20250728095553.png
  70. BIN
      docs/manual/images/20250728095854.png
  71. BIN
      docs/manual/images/20250728100738.png
  72. BIN
      docs/manual/images/20250728100858.png
  73. BIN
      docs/manual/images/20250728101135.png
  74. BIN
      docs/manual/images/20250728102029.png
  75. BIN
      docs/manual/images/20250728102338.png
  76. BIN
      docs/manual/images/20250728102532.png
  77. BIN
      docs/manual/images/20250728102959.png
  78. BIN
      docs/manual/images/20250728103109.png
  79. BIN
      docs/manual/images/doubao_buy_voiceclone.png
  80. BIN
      docs/manual/images/doubao_buy_voiceclone20.png
  81. BIN
      docs/manual/images/doubao_create_project.png
  82. BIN
      docs/manual/images/doubao_icl_10_tts.png
  83. BIN
      docs/manual/images/doubao_icl_20_tts.png
  84. BIN
      docs/manual/images/doubao_icl_error-sample.png
  85. BIN
      docs/manual/images/doubao_icl_ok_sample_1.png
  86. 237 0
      docs/manual/manual.md
  87. 221 0
      docs/page.css
  88. 767 0
      docs/phone-bar-ex.html
  89. 561 0
      docs/phone-drop.txt
  90. BIN
      docs/shot/doubao_vcl.png
  91. BIN
      docs/shot/ivr.png
  92. BIN
      docs/shot/webgui-1.png
  93. BIN
      docs/shot/webgui-2.png
  94. BIN
      docs/shot/webgui-3.png
  95. 13 0
      docs/zh-cn/Debian12-install-docker.md
  96. 53 0
      docs/zh-cn/Debian12-install-mysql8.md
  97. 250 0
      docs/zh-cn/FreeSWITCH-install-docs.md
  98. 25 0
      docs/zh-cn/Set-up-ASR-TTS.md
  99. 29 0
      docs/zh-cn/debian12-install-jdk8.md
  100. BIN
      docs/zh-cn/vmware-ipaddr-settings.png

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+/.idea
+/target
+/call-center.iml2
+.iml
+/文档/go
+/target1
+/target2
+/call-center.iml
+/call-center-robot.iml
+/src/main/java/com/telerobot/fs/robot/DeepSeekClient2.java
+/README.en.md
+/sql/easycallcenter365-dev.sql
+/sql/easycallcenter365-.sql
+*.iml

+ 17 - 0
Build.md

@@ -0,0 +1,17 @@
+## easycallcenter365 build guides
+   
+### 设置数据库参数:
+ 
+   set mysql connection info, update src\main\resources\application-uat.properties,
+   
+   spring.datasource.username=root
+   
+   spring.datasource.password=123456
+
+
+### 打包项目:
+
+   run build.bat
+   
+   After the build is complete, you will see the easycallcenter365.jar file in the target directory.
+   

+ 358 - 0
Deploy.txt

@@ -0,0 +1,358 @@
+欢迎关注公众号: 呼叫中心之开源技术分享
+后续会陆续有视频教程发布。
+
+欢迎加入开源社区
+添加微信号: 18556579517
+
+
+系统使用及配置演示视频:https://mp.weixin.qq.com/s/05GIFKWtHt6EB0mxgL0BuA
+
+
+我们提供了预编译的二进制文件。 百度网盘下载地址: https://pan.baidu.com/s/1xFgMPCu0VKHKnG69QhyTlA 提取码: etv5
+
+
+注意:如果是采用预编译的二进制部署方式, 部署之前请把百度网盘中的文件全部下载到本地文件夹。
+然后把文件上传到 linux 服务器目录中,仅支持使用Debian12,推荐下载地址: https://mirrors.163.com/debian-cd/current/amd64/iso-dvd/
+
+一.  Freeswitch部署
+
+    注意:如果你是手动编译的FreeSWITCH,请把编译后的文件打包名为: FreeSWITCH-for-easycallcenter365-1.10.11.zip 后再上传到 linux 目录下。
+	在从源代码手动编译的情况下,如果需要docker部署,也需要从前面提到的百度网盘下载docker镜像文件 freeswitch-debian12.5-image.tar。
+	
+	环境准备:docker及mysql8的安装
+	docker 的安装参考文档: https://gitee.com/easycallcenter365/freeswitch-modules-libs/blob/master/docs/zh-cn/Debian12-install-docker.md 。
+	mysql8 的安装参考文档: https://gitee.com/easycallcenter365/freeswitch-modules-libs/blob/master/docs/zh-cn/Debian12-install-mysql8.md 。
+
+1.  创建数据库: freeswitch
+    导入  freeswitch-1.10.11.sql
+    注意这里设置数据库root密码为: easycallcenter365
+	
+2. 	导入freeswitch 的docker 镜像
+    docker load -i freeswitch-debian12.5-image.tar
+	
+3.  部署Freeswitch程序
+	mkdir /home/freeswitch/
+	unzip -d /home/freeswitch/  FreeSWITCH-for-easycallcenter365-1.10.11.zip
+	# 注意去掉多余目录,确保Freeswitch的bin目录位于 /home/freeswitch/下;
+	
+	修改Freeswitch的数据库密码: 
+	vim /home/freeswitch/etc/freeswitch/autoload_configs/switch.conf.xml
+	(<param name="core-db-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />)
+	vim /home/freeswitch/etc/freeswitch/sip_profiles/external.xml
+	( <param name="odbc-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />)
+	vim /home/freeswitch/etc/freeswitch/sip_profiles/internal.xml
+	( <param name="odbc-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />)	
+	vim /home/freeswitch/share/freeswitch/scripts/auth_user.lua
+	( local dbh = freeswitch.Dbh("mariadb://Server=127.0.0.1;Port=3306;Database=easycallcenter365;Uid=root;Pwd=easycallcenter365;"))
+
+	请注意:如果你的MySQL和FreeSWITCH不在同一台服务器上,请修改上面的 Server=127.0.0.1 为实际的IP地址。
+	这里的MySQL用户名和密码,如果和你的不一样, Uid=root;Pwd=easycallcenter365; 请修改为正确的值。
+	
+	
+	
+4. 创建docker容器
+    chmod +x /home/freeswitch/bin/*
+   
+    docker run  --log-opt max-size=100m --log-opt max-file=3  \
+    --ulimit core=-1 --security-opt seccomp=unconfined  --privileged=true  \
+    -itd --name freeswitch-debian12  \
+	-v /home/Records:/home/Records  -v /home/freeswitch:/usr/local/freeswitchvideo  \
+	--network=host  freeswitch-debian12-image   /usr/local/freeswitchvideo/bin/freeswitch_start.sh
+	
+	
+5.  设置容器的时区:  
+	 docker cp /usr/share/zoneinfo/Asia/Shanghai  freeswitch-debian12:/etc/localtime	
+	 
+	 
+6.  进入Freeswitch控制台
+    docker exec -it freeswitch-debian12 /usr/local/freeswitchvideo/bin/fs_cli	 
+	查看状态: sofia status (如果显示数量为0说明安装失败,一般是数据库用户名及密码错误导致)
+	
+
+	
+二. easycallcenter365 的部署
+
+1. 部署 easycallcenter365.jar 
+
+   首先请导入最新的数据库脚本 easycallcenter365.sql。
+   MySQL创建一个名为 easycallcenter365 的数据库,然后导入 easycallcenter365.sql 文件。
+   
+   a. 在本地文件夹,修改 easycallcenter365.jar 和 easycallcenter365-gui.jar 的配置文件;
+      如果使用的密码是 easycallcenter365 ,则跳过该步骤 b 和 c ;
+  
+   b. easycallcenter365.jar 中默认设置的MySQL的root密码是 easycallcenter365 ;
+      配置文件是:\BOOT-INF\classes\application-uat.properties ;
+	  可以使用Winrar打开jar包,鼠标拖拽拷贝出该文件,修改后再覆盖该文件。
+	  注意:修改jar包时,"压缩方式(C)",要选择"存储",否则会导致jar包损坏。
+	  
+   c. easycallcenter365-gui.jar 设置的MySQL的root密码也是 easycallcenter365 ;
+      配置文件是: \BOOT-INF\classes\application-pro.yml
+	  可以参照上一步骤c,使用Winrar打开jar包修改数据库连接密码。
+	  
+   d. 把 easycallcenter365.jar 和 easycallcenter365-gui.jar 文件上传到linux的 /home/easycallcenter365/ 目录下;	 
+      mkdir /home/easycallcenter365/   
+
+   e. 检查java版本:
+      # java -version
+	  输出	java version "1.8.0_391"
+		    Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
+		    Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
+	  如果是OpenJDK,则需要卸载并重新安装 Oracle JDK,
+	  参考文章:https://gitee.com/easycallcenter365/freeswitch-modules-libs/blob/master/docs/zh-cn/debian12-install-jdk8.md
+	  
+
+   f. 启动两个jar包程序:
+      cd /home/easycallcenter365/
+      nohup  java  -Dfile.encoding=UTF-8  -jar  easycallcenter365.jar > /dev/null 2>&1 & 
+      nohup  java  -Dfile.encoding=UTF-8  -jar  easycallcenter365-gui.jar > /dev/null 2>&1 &
+
+
+三. FunASR的部署
+     
+我们提供了 funasr-server 的一键安装包 funASR-0.1.9
+链接: https://pan.baidu.com/s/1Cg1xUcxrsLMaUv8CklFLug 提取码: 4tke 
+通过funASR官方文档进行安装,有比较大的安装失败概率。
+下载安装包到本地,然后参考网盘中的文档 "FunAsr-0.1.9-集群离线部署.txt" 进行安装。
+
+
+
+四. 运行设置
+
+   这里假定服务器的IP地址是: 192.168.14.218
+
+1. 启动服务
+    docker restart freeswitch-debian12  
+	【如果前面已经启动了 easycallcenter365.jar 和 easycallcenter365-gui.jar ,这里不需要重复启动! 】
+	nohup  java  -Dfile.encoding=UTF-8  -jar  easycallcenter365.jar > /dev/null 2>&1 & 
+    nohup  java  -Dfile.encoding=UTF-8  -jar  easycallcenter365-gui.jar > /dev/null 2>&1 &
+	
+* 注意事项
+
+  启动 easycallcenter365.jar 和  easycallcenter365-gui.jar  之前,请确保FreeSWITCH和MySQL已经启动,一定注意启动顺序。
+	
+	打开 easycallcenter365 日志:  tail -f /home/easycallcenter365/logs/easycallcenter365.log 
+	打开 FreeSWITCH 日志:         tail -f /home/freeswitch/var/log/freeswitch/freeswitch.log
+	进入 FreeSWITCH 控制台:      docker exec -it  freeswitch-debian12  /usr/local/freeswitchvideo/bin/fs_cli
+	
+* 阿里云主机设置
+
+如果当前部署是在阿里云上,需要在防火墙中设置相关端口开放。	点击主机实例,找到"安全组",然后点击"管理规则",在访问规则中,
+手动添加以下端口:
+```txt
+自定义TCP   目的: 8899          源: 所有IPV4(0.0.0.0/0)     描述: easycallcenter365-gui
+自定义TCP   目的: 1081          源: 所有IPV4(0.0.0.0/0)     描述: easycallcenter365电话工具条
+自定义UDP   目的: 21002/31002   源: 所有IPV4(0.0.0.0/0)     描述: easycallcenter365电话语音端口
+自定义UDP   目的: 5080          源: 所有IPV4(0.0.0.0/0)     描述: easycallcenter365电话语音呼入端口
+```
+注意:在阿里云上开放公网端口风险较大,社区版用户需要注意安全风险。建议设置IP来源的地址段。
+(云端部署的安全防护措施仅对商业版用户提供支持。)
+	
+
+2.  大模型参数准备
+     
+	a. 如果您需要对接 DeepSeekV3,请准备好参数 apiKey ;
+	   注意,easycallcenter365使用的是阿里云百炼平台的大模型接口: https://bailian.console.aliyun.com/ ,
+	   选择顶部菜单:模型 -> 文本模型 -> 更多模型,选择 deepseek-v3 ,设置好体验对话框之,可以进行在线调试。
+	   在左下角,"密钥管理"菜单,可以找到API Key。
+	   
+	   
+	b 如果您需要对接"扣子Coze智能体", 请准备好 coze-pat-token 和 coze-bot-id	两个参数。
+	   
+	   coze-pat-token 参数请在 https://www.coze.cn/open/oauth/pats 后台创建;
+	   点击右上角的"添加",在添加对话框中,过期时间最长可设置为30天,
+	   勾选 "Bot管理" -> "chat",接着 "选择指定工作空间",最后点击确定保存,
+	   创建成功之后,就可以看到令牌的值,复制它, 注意关闭窗口后无法再次看到!
+	   
+	   coze-bot-id 参数的值如何查看? 
+	   进入扣子主页,https://www.coze.cn/home ,最近编辑 菜单下面,可以看到最近创建的智能体,
+	   选择一个智能体并点击进入,在地址栏中就可以看到, 比如:
+	   https://www.coze.cn/space/7367249902209859647/bot/7447790819148414985
+	   这里的 coze-bot-id 就是 7447790819148414985 。
+	   
+	   特别注意:智能体发布的时候,在发布页面的最下方,一定要勾选 API,否则无法调用智能体接口!
+	   
+	
+	c. 如果是对接 MaxKB ,则需要修改三个参数: maxkb-server-url 、maxkb-api-key 、maxkb-model 。
+	   在 MaxKB 后台,点击进入你事先创建好的应用,在 "应用信息" 中找到 "API 访问凭据", 下方的 "Base URL",
+	   复制 "Base URL",在它的值后面追加字符串 /chat/completions,最终完整的示例:
+	   http://222.124.14.142:8081/api/application/abbed614-2313-11f0-b414-0242ac110002/chat/completions
+	   
+
+    d. 如果对接的是dify,需要准备好2个参数: apiKey 和 dify 的接口地址。
+	   
+	
+	
+	
+3. 修改语音合成设置
+ 
+    登录可视化web管理后台: http://192.168.14.218:8899/ (这里假定服务器的IP地址是: 192.168.14.218,请修改为实际的IP地址)
+	用户名: admin 密码: admin123  分机: 1001
+
+
+   后台菜单:  "基础配置" -> "语音合成设置" ->  "阿里云tts参数配置"
+   修改三个参数字段: access_key_id、app_key、access_key_secret 。
+   
+   阿里云 access_key_id和access_key_secret设置:
+   登录阿里云后台,鼠标悬停在页面的右上角,在弹出的个人信息中,选择 "权限与安全" -> "AccessKey",
+   在弹出的窗口中,选择 "继续使用云账号 AccessKey",最后在页面上点击按钮 "创建AccessKey"。
+   身份校验通过后,就会创建成功,请把 AccessKey ID 和 AccessKey Secret 下载到本机。
+   
+   阿里云语音 app_key的设置,参考文章: https://blog.csdn.net/HaaSTech/article/details/120217461
+   参数修改后,自动生效,无需重启FreeSWITCH程序。   
+      
+   成功获取 access_key_id和access_key_secret以及app_key后,在后台保存设置即可。
+  
+
+4.  修改语音识别设置
+    easycallcenter365 社区版,默认仅支持 FunAsr 语音识别,无需设置。
+	
+	商业版用户,可以支持把语音识别模块替换为 阿里云 或者科大讯飞。
+	如果选择阿里云语音识别,阿里云后台注意设置语音识别参数,在"项目功能配置"中,找到 "语音识别" -> "修改配置",
+	商业版语音识别模块支持8K/16K采样率,目前可以支持多种方言及东南亚小语种。
+		   
+
+5.  大模型配置
+    后台菜单:  "基础配置" -> "大模型配置",点击添加。
+	
+	DeepSeekV3配置的例子:
+	名称: 招生机器人 (举例)
+	实现类:DeepSeekChat 
+	apiKey: sk-3e88fb8159dxxxxxxxxeaf34246e94 (填写为自己的)
+	服务地址:https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
+	模型名称:deepseek-v3
+	大模型提示词:参考百度网盘同目录下的 "提示词/llmTips.txt"
+	FAQ内容:参考百度网盘同目录下的 "提示词/faq.txt"
+	转人工提示:请稍后,正在为您转接人工坐席。
+	挂机提示:欢迎您再次来电咨询,祝您生活愉快,再见!
+	客户不说话提示:您好,请问在听吗? 能听到到吗
+	开场白:您好,欢迎致电未来科技大学,请问有什么可以帮到您?
+	打断开关:打开
+	打断关键词:学费 专业 住宿 就业 前景 失业 专科 本科 奖学金 助学金 贷款 助学贷款  等一下  等下 等等 等我一下 先停下 听我说 先听我说
+	忽略打断关键词:呃 哦 哦哦 嗯 嗯嗯 嗯好的 好的 对 对对 是的 明白 啊 这样啊 是这样啊 这样的 您好 你好
+	客户意向提示词: 参考百度网盘同目录下的 "提示词/customerSortTips.txt"
+	
+	说明: "客户意向提示词" 是用于客户意向分类的,适用于外呼场景下对客户分类的需求。目前仅支持 DeepSeekV3。
+	
+	Coze扣子智能体配置的例子:
+	名称:招生客服测试
+	实现类:Coze
+	服务地址:https://api.coze.cn/v3/chat/
+	botId:7447790819148414985
+	token类型:pat
+	patToken:pat_DDFWN3cHvLW3zjcaQsJxxxxxxxvtjgaspyKZNz4CUwMil
+	转人工提示:请稍后,正在为您转接人工坐席。
+	挂机提示:欢迎您再次来电咨询,祝您生活愉快,再见!
+	客户不说话提示:您好,请问在听吗? 能听到到吗
+	开场白:您好,欢迎致电未来科技大学,请问有什么可以帮到您?
+	打断开关:关闭
+	
+	
+6.  呼入客服配置
+    后台菜单:  "呼叫中心" -> "呼入配置",点击添加。	
+	
+	添加呼入配置号码
+	大模型底座: 选择在第6步中设置的大模型
+	音色:  选择语音播报的发音人
+	被叫号码:654321  (就是呼入号)
+	服务方式:选择ai或者acd
+	转人工业务组: 转人工坐席时的业务组
+	
+	系统支持配置多个呼入号码,不同的呼入号码可以支持使用不同的大模型底座。
+		   
+	
+7. 呼入客服的测试
+
+   在百度网盘的同目录下载 "软电话.zip" 安装包,
+   使用软电话之前请先安装vc++运行环境 vcredist_x64_2015-2022.exe。
+   
+   打开软电话注册并测试:
+   分机号及密码: 1001  123456
+   注册地址: 192.168.14.218:5080   (请把 192.168.14.218 改为你自己的虚拟机地址)
+   注册成功后,拨号: 654321
+   就可以进行智能客服的呼入电话测试了。 在菜单 "呼入记录查询"中,可以查询对话记录和听录音。
+   
+   一切正常的话,此时应该可以听到语音播报。
+   如果无法听到语音播报请查看日志进行问题排查。
+   一般需要查看 easycallcenter365 和 FreeSWITCH 日志。
+   查看 easycallcenter365 日志:   /home/easycallcenter365/logs/easycallcenter365.log 
+   查看 FreeSWITCH 日志:           /home/freeswitch/var/log/freeswitch/freeswitch.log
+	
+   对于linux新手建议把 easycallcenter365.log 和 freeswitch.log 下载到本机方便查看。
+	
+   
+8. 机器人外呼配置
+
+   首先需要配置外呼线路。
+   后台菜单:  "基础配置" -> "线路配置",点击添加。
+   
+   a. 添加线路配置
+   网关名称: test (不支持汉字,请使用英文或者字母)
+   网关用途:  不限制
+   profile名称: external
+   主叫号码:007  
+   网关地址: 192.168.14.201:5080
+   外呼语音编码: pcma  (或者pcmu)
+   网关名称描述:测试线路
+   网关最大并发数:1
+   是否认证注册: 对接模式
+   使用优先级:1
+   
+   b. 创建外呼任务
+   后台菜单:  "AI外呼" -> "外呼任务管理",点击添加。
+   
+   添加外呼任务
+   任务名称: 测试外呼
+   任务类型: AI外呼
+   最大并发: 1
+   外呼线路: test
+   大模型底座:招生机器人 
+   音色:艾夏-普通话女生
+   
+   下载数据导入模板: 在任务类型,一栏的末尾,点击下载模板。 下载AI外呼任务的 Excel 模板。
+   
+   任务保存后,打开 Excel 模板,编辑待呼叫的号码列表,保存。
+   在任务列表中,找到刚刚创建的外呼任务,点击"导入数据"按钮, 选择刚编辑的Excel。
+   
+   注意:如果服务器配置是4c/8g,单次导入的Excel数据不要超过5万条,否则会报错。
+   如果配置是8c/16g以上,单次正常可以导入10万条数据。
+   
+   最后:点击 "启动外呼" 按钮,启动任务。
+   
+   
+   c. 问题排查
+   日志及问题排查,参考第8步中提到的日志文件路径。
+   通过菜单 "AI外呼记录",可以查询外呼记录,如果是线路原因导致外呼失败,可以查看 "挂机原因"字段。 
+   
+   d. 数据导出
+   通过菜单 "AI外呼记录",可以选择外呼任务,导出指定任务的外呼结果。
+    
+	
+   
+9. 对接大模型及智能体注意事项
+   无论是对接Coze还是MaxKB或Dify,请先在网页界面的对话窗口中,把文字对话的流程跑通。
+   Coze扣子需要充值,请保持有一定账户余额。
+   
+   由于DeepSeek官方API速度太慢,这里默认使用的是阿里云百炼平台的大模型接口。
+
+
+10. 其他说明
+   本一键安装包用于体验测试,实际投产建议部署在阿里云上。
+   原因如下:
+   a. 语音合成调用的是阿里云,语音播报延时受网络波动影响;
+   b. 语音识别,一般也是使用云端,语音识别结果返回的延时也受到网络波动影响;
+      FunAsr仅限测试使用,语音识别效果比云服务要差,不推荐用于商用;
+   c. 无论是调用扣子Coze智能体还是DeepSeekV3,使用的都是云端大模型,也会受到网络波动影响。
+   	
+
+   
+
+11. 其他问题汇总
+   
+   a. 关于电话工具条的设置问题
+      如果是想把工具条代码集成到自己的业务系统中,请参考easycallcenter365项目主页文档: https://gitee.com/easycallcenter365/easycallcenter365#%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E8%BD%AC%E5%86%85%E7%BD%AE%E4%BA%BA%E5%B7%A5%E5%9D%90%E5%B8%AD
+      如果是想测试 easycallcenter365-gui 管理后台的电话工具条,在管理后台找到,参数管理,修改参数: call-center-server-ip-addr ,改为当前服务器的对外IP地址即可。
+	  
+
+
+   
+   

+ 3 - 0
LICENSE

@@ -0,0 +1,3 @@
+This is not a free software. Commercial use requires payment for authorization.
+
+这不是一个免费软件,商用使用需要支付费用以获得授权。

+ 168 - 0
README.md

@@ -0,0 +1,168 @@
+# easycallcenter365
+
+![easycallcenter365](logo.jpg) 
+
+基于FreeSWITCH和大模型的智能电话客服系统。
+
+### 功能列表
+
+* 支持对接大模型/ `coze` 智能体 / Dify /MaxKB
+* 支持AI客服说话时被打断/打断关键词设置
+* 提供网页管理系统,支持在线配置
+* 实时流式语音合成/FunAsr开源语音识别/阿里云语音识别
+* 支持豆包声音复刻和语音合成 [New]
+* 支持IVR外呼 [New]
+* 支持AI通话无缝转接人工坐席
+* 支持电话工具条/支持acd话务排队
+* 支持多个AI呼入客服/独立配置呼入号码及大模型底座
+* 支持创建AI语音外呼任务
+* 压力测试: 商业版支持200外呼或呼入并发
+* 支持多方通话/电话会议
+ 
+更新日期: 2026/01/03
+
+**修复了已发现的bug,本次更新重点是:支持豆包声音复刻。**
+
+**已经下载老版本测试体验的用户,需要立即更新到最新版本,老版本已过期且无法使用。**
+
+### 商业咨询
+
+ 本项目当前仅对商业用户提供技术支持服务,这不是一个免费软件, 商业使用需付费获得授权。
+
+![联系方式](/docs/images/wechat2.png) 
+  
+### 系统功能演示视频
+
+[系统使用及配置演示视频](https://mp.weixin.qq.com/s/05GIFKWtHt6EB0mxgL0BuA)
+
+此外我们还编写了 [图文手册](/docs/manual/manual.md) 供参考。
+
+[常见问题手册](docs/manual/faq.md) 提供了对常见问题的排查的指导。
+
+### 一键安装包
+
+一键安装体验包的地址在百度网盘。
+链接: https://pan.baidu.com/s/1ZnQ64KIJWn1p-iJr-b9f4A 提取码: z2qn 
+一键安装包内置了FreeSWITCH-1.10.11、funasr-0.1.9、easycallcenter365.jar、easycallcenter365-gui.jar、mysql-8。
+下载到本地后,按照目录中的"使用说明.txt" 导入VmWare虚拟机并启动,最后调整相关参数即可体验测试。
+该部署方案可以省去从源代码编译的繁琐步骤,适合用户快速体验产品特性。
+
+### 二进制安装包
+
+我们提供了预编译的二进制文件。 下载地址: https://pan.baidu.com/s/1xFgMPCu0VKHKnG69QhyTlA 提取码: etv5
+部署文档参考目录下的文件 "部署文档.txt"。 该部署方案可以省去从源代码编译的繁琐步骤。
+
+### 系统截图
+
+![系统截图](/docs/shot/webgui-1.png)
+![系统截图](/docs/shot/webgui-2.png)
+![系统截图](/docs/shot/webgui-3.png)
+![IVR功能](/docs/shot/ivr.png)
+![豆包声音复刻](/docs/shot/doubao_vcl.png)
+
+
+
+### 呼入电话的处理流程
+
+   ![呼入电话的处理流程](docs/images/process-flow.png) 
+   
+* 客户来电时,电话一般进入 public context 的拨号计划,然后在拨号计划中调用 curl 指令,
+curl 请求 easycallcenter365 的一个 api 接口,把通话uuid、主叫被叫等信息发送过去。通话被 easycallcenter365 接管。
+
+* `easycallcenter365` 启动录音/录像。`easycallcenter365` 尝试和`机器人大脑`(deepseek/Coze智能体/MaxKB) 建立连接 。
+* `机器人大脑` 以流式http响应的返回开场白,easycallcenter365 一边接收文本,一边调用 speak 指令发送文本进行语音合成。
+* FreeSWITCH 的 mod_aliyun_tts 收到语音合成指令后,提取参数中的文本,然后连接语音合成服务器,发送语音合成请求。
+* 由于整个过程中,文本是不断产生的,mod_aliyun_tts 一边发送语音合成文本,一边接收合成后的语音流数据,同时进行解码语音并播放。
+* 播放完毕后,FreeSWITCH 启动语音识别的检测。通过 mod_funasr 或者 unimrcp 模块实现语音流的发送及语音识别结果文本的接收。
+* 通过 event socket 消息,FreeSWITCH 把收到的语音识别结果文本,发送给 easycallcenter365。
+* easycallcenter365 把语音识别结果文本,附带前面交互的消息,一起打包,发送给  `机器人大脑` 。
+* 接下来继续循环,直到电话通话结束。
+
+### 运行环境
+
+   该项目目前仅在 debian-12 环境下编译测试通过。其他操作系统环境尚未测试。 
+
+
+### 设置Debian12的中文支持
+
+* 解决乱码问题
+
+ vim ~/.profile  追加配置:
+
+```bash
+LANG=zh_CN.UTF-8
+LANGUAGE=zh_CN.UTF-8    
+```	
+
+如果不设置,可能会导致语音合成异常。让配置立即生效:
+
+```bash
+source ~/.profile
+```	
+
+* 后台菜单乱码问题
+
+  请使用`navicat`或者`SQLYog`管理工具创建数据库,并指定数据库的编码为utf8mb4,最后使用工具导入数据库脚本。
+  注意:如果使用source命令,需要特别注意,请参考下面的命令。
+  
+```sql
+CREATE DATABASE `easycallcenter365` 
+  CHARACTER SET utf8mb4 
+  COLLATE utf8mb4_general_ci;  
+  
+SET NAMES utf8mb4;
+
+USE easycallcenter365;
+
+SOURCE /home/easycallcenter365.sql;
+```  
+   
+### 详细部署及配置
+
+参考文件 [Deploy.txt](https://github.com/easycallcenter365/easycallcenter365/blob/master/Deploy.txt)。
+   
+### 目前支持哪些语音识别方式?   
+
+目前支持 websocket、mrcp 语音识别方式。目前 mod_funasr 支持 websocket 方式对接funasr语音识别。 
+mrcp 语音识别方式,支持阿里云语音识别。  
+注意:目前mrcp语音识别方式,无法实现对机器人语音的打断功能。  
+
+  
+### 如何设置转内置人工坐席
+
+  在AI通话中,如果用户明确表达了转人工的诉求,系统会自动转人工坐席。
+
+  转人工坐席的流程是,先自动排队,然后转接给空闲坐席处理,坐席需要通过电话工具条登录。  
+  
+  ![电话工具条](docs/images/phone-bar.png) 
+  
+  坐席接听测试方法:请用记事本打开 docs\phone-bar.html 文件,
+  修改 scriptServer 的地址为 easycallcenter365 所在服务器的IP地址,保存后重新使用浏览器打开 phone-bar.html 文件。
+  点击 "签入" 按钮,登录上线。 如果无法登录,请检查 easycallcenter365.jar 是否启动。
+  然后点击 "置闲" 按钮,同时注册软电话分机 1018 , 最后等待电话接入。
+
+### 如何设置电话工具条的分机号及工号
+
+  用记事本打开 docs\phone-bar.html 文件。
+  
+  找到   
+```java     
+var scriptServer = "192.168.14.218";		
+var  extnum = '1018'; //分机号		
+var opnum = '8001'; //工号		
+var skillLevel = 9; //技能等级		
+var groupId = 1; // 业务组id  
+```		
+		
+  按照提示修改即可。
+  
+### 传统电话接入后的网络结构图
+
+![传统电话接入后的网络结构图](docs/images/network-tra.png) 
+  
+### 呼叫中心接入后的网络结构图
+
+![传统电话接入后的网络结构图](docs/images/network-callcenter.png) 
+
+  
+  

+ 5 - 0
build.bat

@@ -0,0 +1,5 @@
+pushd   %~dp0
+mvn  clean package -Dmaven.test.skip=true
+ pause
+ pause
+ pause

+ 1840 - 0
docs/ccPhoneBarSocket.js

@@ -0,0 +1,1840 @@
+/*
+ * 呼叫中心电话工具条
+ * author: easycallcenter365@126.com
+ * 2025-04-08
+*/
+
+function _phoneBarObserver(){
+	if(!(this instanceof _phoneBarObserver)){
+		return new _phoneBarObserver();
+	}
+	this.listeners = {}
+};
+_phoneBarObserver.prototype = {
+	on: function (key, callback) { this.addListener(key, callback); },
+	off: function (key, callback) { this.removeListener(key, callback); },
+	addListener: function (key, callback) {
+		if(!this.listeners[key]) {
+			this.listeners[key] = [];
+		}
+		this.listeners[key].push(callback);
+	},
+	removeListener: function (key, callback) {
+		if(this.listeners[key]) {
+			if(callback) {
+				for (var i = 0; i < this.listeners[key].length; i++) {
+					if (this.listeners[key][i] === callback) {
+						delete this.listeners[key][i];
+					}
+				}
+			}else {
+				this.listeners[key] = [];
+			}
+		}
+	},
+	notifyAll: function (key, info) {
+		if(!this.listeners[key]) return;
+		for (var i = 0; i < this.listeners[key].length; i++) {
+			if (typeof this.listeners[key][i] === 'function') {
+				this.listeners[key][i](info);
+			}
+		}
+	},
+	make: function (o) {
+		for (var i in this) {
+			o[i] = this[i];
+			o.listeners = {}
+		}
+	}
+};
+
+//呼叫中心websocket通信对象
+"use strict";
+function ccPhoneBarSocket() {
+	var observer = new _phoneBarObserver(); 
+	observer.make(this);
+	var _cc = this;
+	var ws = null; 
+	var wsuri = null;
+	var isConnected = false;
+	/* 通话已建立 */
+	var callConnected = false;
+	/*
+	 *  是否可以发送视频通话邀请;
+	 */
+	var canSendVideoReInvite = false;
+	/**
+	 * 是否开启了坐席状态列表订阅
+	 * @type {boolean}
+	 */
+	this.subscribeAgentListStarted = false;
+	this.iframe = null;
+	this.callConfig = {
+		 // 使用默认的UI,还是使用自定义的UI
+		'useDefaultUi': false,
+		//呼叫控制服务器地址
+		'ipccServer': '127.0.0.1:8443',
+		//是否启用websocket安全连接
+		'enableWss' : false,
+		//语音编码
+		'callCodec' : 'pcma',
+		//是否发送心跳数据
+		'enableHeartBeat' : true,
+		//送心跳数据的时间间隔; 秒;
+		'heartBeatIntervalSecs' : 16,
+		// 工具条外呼时: 软电话和外呼通话,使用相同的语音编码,避免转码,从而提高性能;
+		// 但是!: 如果客户端是网页电话,且外呼线路使用g729编码时,该参数需要设置为false,
+		// 因为网页电话不支持g729编码; [此时会产生语音编码的转码]
+		'useSameAudioCodeForOutbound' : true,
+		// 令牌
+		'loginToken' : '',
+
+		 // 网关列表, 如果是注册模式: 网关地址参数则填写为网关名称;
+		 // 安全起见,生产环境,需要把该参数加密为base64格式;
+		'gatewayList' : [
+			{
+				uuid         :  '01',
+				updateTime         :  1675953810492,
+				gatewayAddr  : '192.168.3.111:5080;external', // 网关地址 + 外呼环境[可选参数,默认为external];
+				callerNumber : '007',    // 主叫号码
+				calleePrefix : '',       // 被叫前缀
+				priority     : 1,        // 优先级
+				concurrency  : 10,       // 并发数
+				register     : false,    // 是否注册模式
+				audioCodec   : 'g711'    // 语音编码,可用选择项 g711、g729
+			}
+		],
+
+	    'gatewayEncrypted' : false,
+
+		 // 分机注册配置;
+		 //Freeswitch服务器地址
+		 'fsServer' : '192.168.3.111:5060',
+		 // 分机账户
+		 'extnum' : '',
+		 // 工号
+		 'opnum' : '',
+		 //业务组编号
+		 'groupId' : '',
+		 //全部业务组列表
+		 'groups' : null,
+		 //全部坐席人员列表
+		 'agentList' : null,
+		 'extPassword': 'zfAn1l2mjx86lyX9U33xNf%2FKx15dOf6ucnDDK9nfnkA%3D',  // 分机密码
+		 'phoneType': 'EyeBeam',         // 电话类型
+		 'webPhoneUrl' : 'None',
+
+		 // 客户端软电话代理配置信息
+		 'localHostProxyVersion' : 'v20221130_1736',  // 本地代理软件的版本信息
+		 'localHostProxyPort' : 8888    // 本地代理软件端口;
+	};
+
+	/**
+	 * 在指定html对象后追加新元素
+	 * @param newElement
+	 * @param targetElement
+	 */
+	this.insertAfter = function(newElement,targetElement)
+	{
+		var parent = targetElement.parentNode;
+		if(parent.lastChild === targetElement)
+		{
+			parent.appendChild(newElement);
+		}else{
+			parent.insertBefore(newElement,targetElement.nextSibling);
+		}
+	};
+
+	this.trim = function (str) {
+		return str.replace(/^\s\s*/,'').replace(/\s\s*$/, '');
+	};
+    this.getIsConnected = function(){
+    	return isConnected;
+    };
+    /**
+     *  获取通话状态
+     * @returns {boolean} true通话中,false通话未建立;
+     */
+    this.getCallConnected = function(){
+        return callConnected;
+    };
+    /**
+     *  设置通话状态;
+     * @param value true通话中,false通话未建立
+     * @returns {*}
+     */
+    this.setCallConnected = function(value){
+        callConnected = value;
+    };
+
+	/**
+	 *  设置一个标志,指示是否可以发起视频通话;
+	 * 仅限通话为音频通话且通话已经接通时方可允许发起视频邀请;
+	 * @param value
+	 */
+	this.setCanSendVideoReInvite = function(value){
+		canSendVideoReInvite = value;
+	};
+
+	/**
+	 *  设置一个标志,指示是否可以发起视频通话;
+	 * @param value
+	 */
+	this.getCanSendVideoReInvite = function(){
+	    return 	canSendVideoReInvite;
+	};
+
+	this.setHeartbeat = function()
+	{
+		setInterval(function(){
+			//如果启用了心跳,而且用户已经登录上线,则发送心跳数据
+			if(_cc.callConfig.enableHeartBeat && _cc.getIsConnected()){
+				console.debug("try to send heartbeat.");
+				var heartBeat = {};
+				heartBeat.action="setHearBeat";
+				heartBeat.body = "{}";
+				_cc.sendMsg(heartBeat);
+			}
+		}, _cc.callConfig.heartBeatIntervalSecs * 1000);
+	};
+
+	this.loadScript = function(destUrl, callbackFunc){
+		var script = document.createElement("script");   
+        script.type = "text/javascript";
+        script.src = destUrl;
+		if(null != callbackFunc){
+		   script.onload=function(){callbackFunc();}
+		}
+		document.getElementsByTagName('head')[0].appendChild(script);		 
+	};
+
+    // 初始化websocket连接参数
+	this.initConfig = function(config) {
+		//把config中的属性全部拷贝到callConfig中;
+		for(var element in config) {
+			this.callConfig[element] = config[element];
+		}
+
+		var _loginToken = this.callConfig["loginToken"];
+		if(typeof(_loginToken) == "undefined" && _loginToken === "") {
+			alert("电话工具条:无法获取 loginToken!");
+			return;
+		} 
+ 
+	    console.log("callConfig:", this.callConfig);
+	    wsuri = 'ws://' + this.callConfig.ipccServer +
+	    			'/call-center/websocketServer?' +
+			'&loginToken=' + this.callConfig.loginToken;
+			 
+	    console.log("ipccServer ws url: " + wsuri);
+	    var ipccServerIpAddr = this.callConfig.ipccServer.split(":");
+	    if(this.callConfig.enableWss &&  this.checkIP(ipccServerIpAddr)){
+	    	var tipsError = "ERROR! 启用了wss之后,必须使用域名访问websocketServer! " + this.callConfig.ipccServer;
+	    	console.log(tipsError);
+	    	alert(tipsError);
+		}
+	    if(this.callConfig.enableHeartBeat) {
+			_cc.setHeartbeat();
+		}  
+		 
+		//var downLoadUrl = "http://192.168.66.71:81/soft/callcenter-soft/LocalHostProxy/LocalHostProxy-" +
+							  _cc.callConfig["localHostProxyVersion"] + ".zip";
+		//console.log("客户端代理软件的下载地址:", downLoadUrl);
+        //this.createIframe("http://127.0.0.1:8888/getVersion");
+
+        if(_cc.callConfig["useDefaultUi"]){
+			this.initPhoneBarUI();
+		}
+	};
+
+	//检测ip地址是否合法;
+	this.checkIP = function (ip)   
+	{   
+	    var re =  /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ ;  
+	    return re.test(ip);   
+	};
+
+	//断开到呼叫控制服务器的连接 
+	this.disconnect = function(){
+		var cmdInfo = {};
+		cmdInfo.action="setAgentStatus";
+		cmdInfo.body = {"cmd" : "disconnect", "args" : { "msg" : "disconnection opt triggered by js client." } };
+		ws.send(JSON.stringify(cmdInfo));
+	};
+
+	/**
+	 *  获取当前坐席登录的分机号码
+	 * @returns {string|*}
+	 */
+	this.getExtNum = function(){
+		return this.callConfig.extnum;
+	};
+
+	/**
+	 *  获取当前坐席登录的工号
+	 * @returns {string|*}
+	 */
+	this.getOpNum = function(){
+		return this.callConfig.opnum;
+	};
+
+	/**
+	 *  获取当前坐席的业务组编号
+	 * @returns {string|*}
+	 */
+	this.getGroupId = function(){
+		return this.callConfig.groupId;
+	};
+
+
+	/**
+	 *  获取全部业务组列表
+	 * @returns {string|*}
+	 */
+	this.getGroups = function(){
+		return  JSON.parse(JSON.stringify(this.callConfig.groups));
+	};
+
+    this.findAgentByOpNum  = function (opnum){
+    	if(_cc.callConfig.agentList == null) return null;
+
+		for (var key in _cc.callConfig.agentList) {
+			let item = _cc.callConfig.agentList[key];
+            if(item["opnum"] === opnum){
+            	//add _arrayIndex property to record index
+            	item["_arrayIndex"] = key;
+            	return item;
+			}
+		}
+		return null;
+	};
+
+	this.updateAgentList = function (agentList) {
+		for (var key in agentList) {
+			let item = agentList[key];
+            var existItem = this.findAgentByOpNum(item["opnum"]);
+            if(existItem != null){
+				var newStatus = item["agentStatus"];
+				var oldStatus = existItem["agentStatus"];
+				var logoutTime = item["logoutTime"];
+				if(logoutTime > 0){
+					//删除元素
+					console.info("delete offline user",item["opnum"], "index=", key);
+					_cc.callConfig.agentList.splice(existItem["_arrayIndex"], 1);
+					continue;
+				}
+				if(newStatus != oldStatus){
+					existItem["agentStatus"] = newStatus;
+				}
+			}else{
+				if(_cc.callConfig.agentList != null) {
+					_cc.callConfig.agentList[_cc.callConfig.agentList.length + 1] = item;
+				}
+			}
+		}
+	};
+
+	//连接到呼叫控制服务器
+	this.connect = function() {
+		if ('WebSocket' in window)
+			ws = new WebSocket(wsuri);
+		else {
+			console.log('您的浏览器不支持websocket,您无法使用本页面的功能!');
+			return;
+		}
+		//收到消息
+		ws.onmessage = function(evt) {
+			console.log("recv msg from websocket server: ", evt.data);
+			var msg = JSON.parse(evt.data);
+			console.log("parsed json data:", msg);
+			var resp_status = msg["status"];
+			switch(resp_status) {
+				case 200:
+					isConnected = true;
+					_cc.callConfig.extnum = msg.object["extnum"];
+					_cc.callConfig.opnum = msg.object["opnum"];
+					_cc.callConfig.groupId = msg.object["groupId"];
+					_cc.callConfig.groups = msg.object["groups"];
+					console.log('ipcc连接成功.', 'connected_ipcc_server');
+					_cc.notifyAll(ccPhoneBarSocket.eventList.ws_connected, msg);
+					_cc.setStatus(ccPhoneBarSocket.agentStatusEnum.busy);
+					break;
+				default:
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.caller_hangup) ||
+						parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.CONFERENCE_MODERATOR_HANGUP)) {
+						_cc.setCallConnected(false);
+						_cc.setCanSendVideoReInvite(false);
+						_cc.callConfig.agentList = null;
+						_cc.unSubscribeAgentList();
+						console.log("caller_hangup")
+					}
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_answered)) {
+						_cc.setCallConnected(true);
+						console.log("callee_answered")
+					}
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.new_inbound_call)) {
+						_cc.setCallConnected(true);
+						console.log("new_inbound_call")
+					}
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.caller_answered) ||
+						parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.callee_answered)) {
+						if (msg["object"]["callType"] === "audio") {
+							_cc.setCanSendVideoReInvite(true);
+							_cc.notifyAll(ccPhoneBarSocket.eventList.on_audio_call_connected, "audio call connected.")
+							console.log("current callType is audio.")
+						}
+						if (msg["object"]["callType"] === "video") {
+							_cc.setCanSendVideoReInvite(false);
+							_cc.notifyAll(ccPhoneBarSocket.eventList.on_video_call_connected, "video call connected.")
+							console.log("current callType is video.")
+						}
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_hold)) {
+						_cc.changeUiOnHold();
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_unhold)) {
+						_cc.changeUiOnUnHold();
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_on_hold_hangup)) {
+						_cc.changeUiOnUnHold();
+						$("#holdBtn").removeClass('on');
+						$("#callStatus").text("保持的通话已挂机.");
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_call_wait)) {
+						$("#stopCallWait").show();
+						$("#doConsultationBtn").hide();
+						$("#callStatus").text("客户电话等待中.");
+						_cc.showTransferAreaUI();
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_channel_off_call_wait)) {
+						$("#stopCallWait").hide();
+                        $("#transferCallWait").hide();
+						_cc.hideTransferAreaUI();
+						$("#callStatus").text("等待的电话已接回.");
+					}
+
+                    if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_start)) {
+                        $("#callStatus").text("咨询已开始.");
+                        $("#transferCallWait").show();
+                    }
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_stop)) {
+						$("#callStatus").text("咨询已结束.");
+						$("#transferCallWait").hide();
+					}
+
+                    if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.customer_on_call_wait_hangup)) {
+						$("#stopCallWait").hide();
+                        $("#transferCallWait").hide();
+						$("#callStatus").text("等待的客户已挂机.");
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.inner_consultation_start)) {
+						$("#holdBtn").removeClass('on');
+						$("#hangUpBtn").removeClass('on');
+						$("#transferBtn").removeClass('on');
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.transfer_call_success)) {
+						var extNum = msg["object"]["callee"];
+						if(extNum === _cc.getExtNum()) {
+							$("#holdBtn").addClass('on');
+							$("#hangUpBtn").addClass('on');
+							$("#transferBtn").addClass('on');
+						}
+
+					}
+
+					if (parseInt(resp_status) === parseInt(ccPhoneBarSocket.eventList.agent_status_data_changed)) {
+						if(_cc.subscribeAgentListStarted) {
+							if (_cc.callConfig.agentList == null) {
+								_cc.callConfig.agentList = JSON.parse(msg.object);
+							}else{
+								// 更新列表;
+                                _cc.updateAgentList(JSON.parse(msg.object));
+							}
+						}
+					}
+					_cc.notifyAll(resp_status, msg);
+					break;
+			}
+		};
+		//关闭连接时触发  
+		ws.onclose = function(evt) {
+			isConnected = false;
+			_cc.notifyAll(ccPhoneBarSocket.eventList.ws_disconnected, "ipccserver 连接断开.");
+			console.log("ipcc连接断开.", "disconnected");
+			console.log(evt);
+			ws.close();
+		};
+		ws.onopen = function(evt) {
+			console.log("ipccserver websocket onopen...");
+		};
+	};
+	
+	//发送消息给呼叫控制服务器
+	this.sendMsg = function(jsonObject) {
+		console.debug("ws.send:", jsonObject);
+		ws.send(JSON.stringify(jsonObject));
+	};
+
+	this.changeUiOnHold = function() {
+		$("#holdBtnLi").hide();
+		$("#unHoldBtnLi").show();
+		$("#unHoldBtn").addClass('on');
+	};
+
+	this.changeUiOnUnHold = function() {
+		$("#holdBtnLi").show();
+		$("#holdBtn").addClass('on');
+		$("#unHoldBtnLi").hide();
+	};
+
+	this.hideTransferAreaUI = function(){
+		var transferArea = document.getElementById("transfer_area");
+		transferArea.style.display = "none";
+	};
+
+	this.showTransferAreaUI = function(){
+		var transferArea = document.getElementById("transfer_area");
+		transferArea.style.display = "block";
+	};
+
+	/**
+	 *  设置状态为空闲
+	 */
+	this.setStatusFree = function(){
+		this.setStatus(ccPhoneBarSocket.agentStatusEnum.free);
+	};
+
+	/**
+	 *  设置状态为忙碌
+	 */
+	this.setStatusBusy = function(){
+		this.setStatus(ccPhoneBarSocket.agentStatusEnum.busy);
+	};
+
+	ccPhoneBarSocket.utils = {
+
+		/**
+		 * 读取 URL 中的指定查询参数值
+		 * @param {string} paramName - 要获取的参数名称
+		 * @returns {string|null} 返回参数值,如果不存在则返回 null
+		 */
+		getQueryParam :	function (paramName) {
+			const url = window.location.href; // 获取当前页面 URL
+			const params = new URLSearchParams(new URL(url).search); // 创建 URLSearchParams 对象
+			return params.get(paramName); // 获取指定参数值
+		}
+	};
+
+	/**
+	 *  座席状态枚举;
+	 * @type {{rest: number, calling: number, busy: number, free: number, justLogin: number, meeting: number, train: number}}
+	 */
+	ccPhoneBarSocket.agentStatusEnum = {
+
+		/**
+		 *  刚刚上线,尚未就绪中;
+		 */
+		"justLogin" : 1,
+
+		/**
+		 * 空闲
+		 */
+		"free"  :  2,
+
+		/**
+		* 忙碌
+		*/
+		"busy"  :  3,
+
+		/**
+		 * 小休
+		 */
+		"busy_rest"  :  31,
+
+		/**
+		 * 线下会议
+		 */
+		"busy_meeting"  :  32,
+
+		/**
+		 * 培训
+		 */
+		"busy_training"  :  33,
+
+		/**
+		 * 通话中
+		 */
+		"incall" : 4,
+
+		/**
+		 * 话后处理,填写表单中
+		 */
+		"fill_form" : 5,
+
+		/**
+		 * 电话会议中
+		 */
+		"conference"  :  6
+	};
+
+	//定义视频level-id
+	ccPhoneBarSocket.videoLevels = {
+		"Smooth" :  { "levelId" : "42e00b",  "description" : "流畅"  },
+		"Smooth2" :  { "levelId" : "42e00c",  "description" : "流畅+"  },
+		"Smooth3" :  { "levelId" : "42e00d",  "description" : "流畅++"  },
+		"Clear" : { "levelId" : "42e014",  "description" : "清晰"  },
+		"Clear2" :  { "levelId" : "42e015",  "description" : "清晰+"  },
+		"Clear3" :  { "levelId" : "42e016",  "description" : "清晰++"  },
+		"HD" :   { "levelId" : "42e01e",  "description" : "高清"  },
+		"HD2" :  { "levelId" : "42e01f",  "description" : "高清+"  }
+	};
+
+
+	/**
+	 * 系统事件列表;
+	 * @type {{}}
+	 */
+	ccPhoneBarSocket.eventList = {
+		// 当音频通话已建立
+		"on_audio_call_connected" : "100",
+		"on_video_call_connected" : "101",
+
+		// websocketServer连接成功
+		"ws_connected": "200",
+		// 当前用户已在其他设备登录;
+		"user_login_on_other_device" : "201",
+		// 用户下线
+		"ws_disconnected" : "202",
+		// 通话状态发生改变 [监听的数据]
+		"call_session_status_data_changed" : "203",
+        // 坐席状态列表发生改变
+        "agent_status_data_changed" : "205",
+		//请求参数错误
+		"request_args_error" : "400",
+		
+		// unauthorized
+		"unauthorized" : "401",
+		
+		//服务器内部错误
+		"server_error" : "500",
+		// 语音编码不匹配
+		"server_error_audio_codec_not_match" : "501",
+		// 主叫接通
+		"caller_answered" : "600",
+		//  主叫挂断
+		"caller_hangup" : "601",
+		// 主叫忙; 上一通电话未挂机
+		"caller_busy" : "602",
+		//主叫未登录
+        "caller_not_login" : "603",
+		//主叫应答超时
+		"caller_respond_timeout" : "604",
+		// 被叫接通
+		"callee_answered" : "605",
+        // 被叫挂断
+		"callee_hangup" : "606",
+		//被叫振铃
+        "callee_ringing" : "607",
+		// 座席状态改变
+		"status_changed" : "608",
+		// 一个完整的外呼任务结束: [可能尝试了一个或多个网关]
+		"outbound_finished" : "611",
+
+		// 预测外呼,分配的来电;
+		"PREDICTIVE_CALL_INBOUND" : "612",
+
+         // ACD队列分配的新来电
+		"new_inbound_call" : "613",
+
+         // 当前业务组实时排队人数
+		"acd_group_queue_number" : "615",
+
+
+		/**
+		 * 收到转接的来电请求
+		 */
+		"transfer_call_recv" :  616,
+
+		/**
+		 * 锁定坐席失败
+		 */
+		"lock_agent_fail" :  617,
+
+		/**
+		 * 通话已经转接成功
+		 */
+		"transfer_call_success" :  618,
+
+		/**
+		 * 产生asr语音识别结果
+		 */
+		"asr_result_generate" :  619,
+
+		/**
+		 * ASR语音识别流程结束(坐席侧)
+		 */
+		"asr_process_end_agent" :  620,
+
+		/**
+		 * ASR语音识别流程结束(客户侧)
+		 */
+		"asr_process_end_customer" :  621,
+
+		"asr_process_started" : 622,
+
+		/**
+		 * customer call session hold.
+		 */
+		"customer_channel_hold" : 623,
+
+		/**
+		 * customer call session unHold.
+		 */
+		"customer_channel_unhold" : 624,
+
+		/**
+		 * customer call session on hold is hangup.
+		 */
+		"customer_on_hold_hangup" : 625,
+
+		"inner_consultation_request" : 626,
+
+		/**
+		 * customer call session on call-wait.
+		 */
+		"customer_channel_call_wait" : 627,
+
+		/**
+		 * customer call session off call-wait.
+		 */
+		"customer_channel_off_call_wait" : 628,
+
+		/**
+		 * customer call session on call-wait is hangup.
+		 */
+		"customer_on_call_wait_hangup" : 629,
+
+		/**
+		 *  extension on line event
+		 */
+		"extension_on_line" : 630,
+
+		/**
+		 * extension off line event
+		 */
+		"extension_off_line" : 631,
+
+        /**
+         * Notify the agent that the call consultation has started.
+         */
+        "inner_consultation_start" : 632,
+
+        /**
+         *  Notify the agent that the call consultation has stopped.
+         */
+        "inner_consultation_stop" : 633,
+
+
+	    /**
+		* 多人电话会议,重复的被叫 ,
+		*/
+		"conference_repeat_callee"  :  660 ,
+
+		 /**
+		 * 多人电话会议,呼叫成员超时 ,
+		 */
+		 "CONFERENCE_CALL_MODERATOR_TIMEOUT"  :  661 ,
+
+		/**
+		 * 多人电话会议,成员接通 ,
+		 */
+		"CONFERENCE_MEMBER_ANSWERED"  :  662 ,
+
+
+		/**
+		 * 多人电话会议,成员挂机 ,
+		 */
+		"CONFERENCE_MEMBER_HANGUP"  :  663 ,
+
+		/**
+		 * 多人电话会议,成员禁言成功 ,
+		 */
+		"CONFERENCE_MEMBER_MUTED_SUCCESS"  :  666 ,
+
+
+		/**
+		 * 多人电话会议,成员禁言失败 ,
+		 */
+		"CONFERENCE_MEMBER_MUTED_FAILED"  : 665  ,
+
+		/**
+		 * 多人电话会议,成员解除禁言成功 ,
+		 */
+		"CONFERENCE_MEMBER_UNMUTED_SUCCESS"  :  667 ,
+
+
+		/**
+		 * 多人电话会议,成员解除禁言失败 ,
+		 */
+		"CONFERENCE_MEMBER_UNMUTED_FAILED"  : 668  ,
+
+		/**
+		 * 多人电话会议,会议成员不存在,无法执行相关操作:
+		 */
+		"CONFERENCE_MEMBER_NOT_EXISTS"  : "669"  ,
+
+		/**
+		 * 多人电话会议,主持人重置会议 ,
+		 */
+		"CONFERENCE_MODERATOR_RESET"  : "670"  ,
+
+		/**
+		 * 多人电话会议,主持人接通 ,
+		 */
+		"CONFERENCE_MODERATOR_ANSWERED"  : "671"  ,
+
+
+		/**
+		 * 多人电话会议,主持人挂机,会议结束 ,
+		 */
+		"CONFERENCE_MODERATOR_HANGUP"  : "672",
+
+		/*
+		 * 成员视频禁用成功
+		 */
+		"CONFERENCE_MEMBER_VMUTED_SUCCESS" : "674",
+
+		/*
+		 * 成员解除视频禁用成功
+		 */
+		"CONFERENCE_MEMBER_UnVMUTED_SUCCESS" : "676",
+
+		/**
+		 * 多人电话会议,成员解除视频禁用失败;
+		 */
+		"CONFERENCE_MEMBER_UnVMUTED_FAILED"  : "677",
+
+		/**
+		 * 成功把通话转接到多人视频会议
+		 */
+		"CONFERENCE_TRANSFER_SUCCESS_FROM_EXISTED_CALL"  :  "678",
+
+		/**
+		 *  outbound start event
+		 */
+		"OUTBOUND_START" : "679"
+	};
+
+	this.createIframe = function(src){
+        var _iframe = document.createElement("iframe");
+        _iframe.style.width = '0';
+        _iframe.style.height = '0';
+        _iframe.style.margin = '0';
+        _iframe.style.padding = '0';
+        _iframe.style.overflow = 'hidden';
+        _iframe.style.border = 'none';
+        _iframe.src = src;
+        document.body.appendChild(_iframe);
+        _cc.iframe = _iframe;
+    };
+
+    this.openSoftPhone = function (){		
+		//打开软电话
+		var softPhoneUrl = "http://127.0.0.1:" + _cc.callConfig["localHostProxyPort"] + 
+		                   "/autoSetExtension?server=" + encodeURIComponent(_cc.callConfig["fsServer"].split(':')[0]) +
+						   "&port=" + _cc.callConfig["fsServer"].split(':')[1] +
+						   "&extnum=" + _cc.callConfig["extnum"] +
+						   "&pass=" + encodeURIComponent(_cc.callConfig["extPassword"]) +
+						   "&phoneType=" + _cc.callConfig["phoneType"] +
+						   "&version=" + encodeURIComponent(_cc.callConfig["localHostProxyVersion"]) +
+						   "&webPhoneUrl=" + encodeURIComponent(_cc.callConfig["webPhoneUrl"]) +
+						   "";
+	  console.log("softPhoneUrl:", softPhoneUrl);					   
+      _cc.iframe.src = softPhoneUrl;
+	};
+
+	/**
+	 * 设置座席状态
+	 * @param status  agentStatusEnum
+	 */
+	this.setStatus = function (status) {
+		var cmdInfo = {};
+		cmdInfo.action="setAgentStatus";
+		cmdInfo.body = {"cmd" : "setStatus", "args" : { "status" : status } };
+		ws.send(JSON.stringify(cmdInfo));
+	};
+
+	//注销登录;
+	this.logOff = function () {
+		var cmdInfo = {};
+		cmdInfo.action="setAgentStatus";
+		cmdInfo.body = {"cmd" : "disconnect", "args" : { "cause": "disconnect request from js client." }  };
+		ws.send(JSON.stringify(cmdInfo));
+	};
+
+	/**
+	 *  在咨询失败的情况下使用该按钮,接回处于等待中的电话
+	 */
+	this.stopCallWaitBtnClickUI = function () {
+		var cmd = {};
+		cmd.action="callWait";
+		cmd.body = {"cmd" : "stop", "args" : {} };
+		ws.send(JSON.stringify(cmd));
+	};
+
+    /**
+     * 在咨询成功的情况下使用该按钮,把电话转接给专家坐席。
+     */
+	this.transferCallWaitBtnClickUI = function () {
+        this.callControl("transferCallWait", {})
+    };
+
+	this.consultationBtnClickUI = function () {
+        let transferType = "outer";
+        let phoneNumber = $("#externalPhoneNumber").val().trim();
+        if (phoneNumber === "") {
+			transferType = "inner";
+            var groupId = $("#transfer_to_groupIds").val();
+            if ($.trim(groupId) == "") {
+                alert("请选择业务组!");
+                $("#transfer_to_groupIds").focus();
+                return;
+            }
+            var member = $("#transfer_to_member").val();
+            if ($.trim(member) == "") {
+                alert("请选择要咨询的坐席成员!");
+                $("#transfer_to_member").focus();
+                return;
+            }
+
+            var selectText = $('#transfer_to_member option:selected').text();
+            if (selectText.indexOf("空闲") == -1) {
+                alert("请选择空闲的坐席成员!");
+                $("#transfer_to_member").focus();
+                return;
+            }
+
+            if (member == this.getOpNum()) {
+                alert("不能咨询自己,请选择其他坐席成员!");
+                return;
+            }
+            phoneNumber = member;
+        }
+        this.callControl("consultation", {"to": phoneNumber, "transferType": transferType});
+
+    };
+
+	/**
+	 *  处理通话转接按钮点击事件
+	 */
+	this.transferBtnClickUI = function() {
+	    let transferType = "outer";
+	    let phoneNumber = $("#externalPhoneNumber").val().trim();
+        if(phoneNumber === "") {
+            transferType = "inner";
+            var groupId = $("#transfer_to_groupIds").val();
+            if ($.trim(groupId) === "") {
+                alert("请选择转接的业务组!");
+                $("#transfer_to_groupIds").focus();
+                return;
+            }
+            var member = $("#transfer_to_member").val();
+            if ($.trim(member) === "") {
+                alert("请选择转接的坐席成员!");
+                $("#transfer_to_member").focus();
+                return;
+            }
+
+            var selectText = $('#transfer_to_member option:selected').text();
+            if (selectText.indexOf("空闲") === -1) {
+                alert("请选择空闲的坐席成员!");
+                $("#transfer_to_member").focus();
+                return;
+            }
+            if (member === this.getOpNum()) {
+                alert("不能转给自己,请选择其他坐席成员!");
+                return;
+            }
+            phoneNumber = member;
+        }
+        this.transferCall(phoneNumber, transferType);
+	};
+
+	/**
+	 *  处理通话转接
+     *  @param userCodeOrPhone 工号或者电话号码
+     *  @param transferType 转接类型:工号还是外部号码(inner or outer)
+	 */
+	this.transferCall = function(userCodeOrPhone, transferType) {
+	    if(transferType === "inner") {
+            if (userCodeOrPhone !== this.getOpNum()) {
+                this.callControl("transferCall", {"to": userCodeOrPhone, "transferType" : "inner" })
+            } else {
+                console.error("cant not transfer call to yourself.")
+            }
+        }else{
+            this.callControl("transferCall", {"to": userCodeOrPhone, "transferType" : "outer" })
+        }
+	};
+
+	//挂机
+	this.hangup = function() {
+		this.callControl("endSession", {})
+	};
+
+	// 呼叫控制相关操作;
+	this.callControl = function(action, argsObject){
+		var sessionControl = {};
+		sessionControl.action="call";
+		sessionControl.body = {"cmd" : action, "args" : argsObject };
+		ws.send(JSON.stringify(sessionControl));
+	};
+
+	this.checkCallConfirmed = function () {
+		if(!_cc.getIsConnected()){
+			console.log('请先上线.');
+			return false;
+		}
+		if(!_cc.getCallConnected()){
+			console.log('当前没有通话.');
+			return false;
+		}
+		return true;
+	};
+
+	/**
+	 *  send and play mp4 video file.
+	 */
+	this.sendVideoFile = function (mp4FilePath) {
+		if(!_cc.checkCallConfirmed()){
+			return false;
+		}
+		if(typeof(mp4FilePath) == "undefined" || mp4FilePath.trim().length === 0){
+			console.log("Parameter mp4FilePath is missing!")
+			return false;
+		}
+		this.callControl(
+			"playMp4File",
+			{ "mp4FilePath" : mp4FilePath }
+		);
+		return true;
+	};
+
+	/**
+	 *  发起视频通话邀请
+	 */
+	this.reInviteVideoCall = function(){
+        if(!_cc.checkCallConfirmed()){
+        	return false;
+		}
+		if(!_cc.getCanSendVideoReInvite()){
+			console.log('cant not send video reInvite. ',
+				'Precondition is:  Call is connected and  callType is audio.');
+			return false;
+		}
+		this.callControl(
+			"reInviteVideo",
+			{}
+		);
+		return true;
+	};
+
+	/**
+	 *  发起外呼
+	 * @param phoneNumber 被叫号码
+	 * @param callType 通话类型:视频通话、音频通话
+	 * @param videoLevel 视频通话的profile-level-id
+	 */
+	this.call = function(phoneNumber, callType, videoLevel){
+		if(typeof(videoLevel) == "undefined" || videoLevel.trim().length === 0){
+			videoLevel = ccPhoneBarSocket.videoLevels.HD.levelId;
+			console.log("auto default set videoLevel=", videoLevel);
+		}
+		if(typeof(callType) == "undefined" || callType.trim().length === 0){
+			callType = "audio";
+			console.log("auto default set callType=", callType);
+		}
+
+		console.log("call config videoLevel=" + videoLevel + ", callType=" + callType);
+
+		if(phoneNumber==null || phoneNumber.length===0) {
+			console.log('请输入外呼号码!');
+			return;
+		}
+		if(phoneNumber.trim().length < 3){
+			alert('请输入正确格式的外呼号码!');
+			return;
+		}
+		if(!_cc.getIsConnected()){
+			console.log('请先上线.');
+			return;
+		}
+		let outboundInfo = {
+			"gatewayList": _cc.callConfig.gatewayList,
+			'destPhone': phoneNumber,
+			'gatewayEncrypted' : _cc.callConfig.gatewayEncrypted,
+			'useSameAudioCodeForOutbound' : _cc.callConfig.useSameAudioCodeForOutbound,
+			'callType' :  callType,
+			'videoLevel' : videoLevel
+		};
+		this.callControl(
+			"startSession",
+			outboundInfo
+		);
+	};
+	
+	this.callEx = function(phoneNumber){
+		if(phoneNumber == null || phoneNumber.length === 0) {
+			console.log('请输入外呼号码!');
+			return;
+		}
+		if(!_cc.getIsConnected()){
+			_cc.connect();
+			_cc.on(ccPhoneBarSocket.eventList.ws_connected, function(){
+				_cc.off(ccPhoneBarSocket.eventList.ws_connected); //取消事件订阅
+				_cc.call(phoneNumber);
+			});
+			return;
+		}
+		_cc.call(phoneNumber);
+	 };
+
+
+	/************************  以下是网页工具条ui代码   ************************/
+
+	/**
+	 *  根据服务器响应状态码去查找action
+	 * @param code
+	 * @returns {string}
+	 */
+	ccPhoneBarSocket.findItemByCode = function(code){
+		for(var item in ccPhoneBarSocket.eventListWithTextInfo ){
+			if(ccPhoneBarSocket.eventListWithTextInfo[item].code === code){
+				return ccPhoneBarSocket.eventListWithTextInfo[item];
+			}
+		}
+	};
+
+	/**
+	 *  服务器响应状态枚举值;
+	 */
+	  ccPhoneBarSocket.eventListWithTextInfo = {
+		"ws_connected": { "code": 200,  msg:"已签入",
+			btn_text:[{id:"#onLineBtn",name:"签出"}],
+			enabled_btn:['#setFree','#callBtn','#onLineBtn', '#consultationBtn']
+		},
+		"ws_disconnected": { "code" : 202, msg:"服务器连接断开",
+			btn_text:[{id:"#onLineBtn",name:"签入"}],
+			enabled_btn:['#onLineBtn']
+		},
+		"user_login_on_other_device": { "code" : 201, msg:"用户已在其他设备登录",
+			btn_text:[{id:"#onLineBtn",name:"签入"}],
+			enabled_btn:['#onLineBtn']
+		},
+		"request_args_error":{ "code" : 400, msg:"客户端请求参数错误",
+			btn_text:[],
+			enabled_btn:[]
+		},
+		"server_error":{ "code" : 500, msg:"服务器内部错误",
+			btn_text:[],
+			enabled_btn:[]
+		},
+		"caller_answered":{ "code" : 600, msg:"分机已接通",
+			btn_text:[],
+			enabled_btn:['#resetStatus', '#hangUpBtn', '#transferBtn', '#holdBtn', '#consultationBtn']
+		},
+		"caller_hangup":{ "code" : 601, msg:"分机已挂断",
+			btn_text:[],
+			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn' ]
+		},
+		"caller_busy":{ "code" : 602, msg:"分机忙,上一通电话未挂断",
+			btn_text:[],
+			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn']
+		},
+		"caller_not_login":{ "code" : 603, msg:"分机未登录,请检查",
+			btn_text:[],
+			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn']
+		},
+		"caller_respond_timeout":{ "code" : 604, msg:"分机未应答超时,请重新打开分机",
+			btn_text:[],
+			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree', '#consultationBtn']
+		},
+		"callee_answered":{ "code" : 605, msg:"被叫已接通",
+			btn_text:[],
+			enabled_btn:['#resetStatus', '#hangUpBtn', '#transferBtn', '#holdBtn', '#consultationBtn' ]
+		},
+		"callee_hangup":{ "code" : 606, msg:"通话结束",
+			btn_text:[],
+			enabled_btn:['#onLineBtn', '#resetStatus', '#callBtn', '#setFree' , '#consultationBtn']
+		},
+		"callee_ringing":{ "code" : 607, msg:"被叫振铃中",
+			btn_text:[],
+			enabled_btn:['#resetStatus', '#hangUpBtn', '#transferBtn', '#consultationBtn']
+		},
+		"status_changed":{ "code" : 608, msg:"状态已改变",
+			btn_text:[],
+			enabled_btn:[ ]
+		},
+		"free":{ "code" : 0, msg:"空闲中",
+			btn_text:[],
+			enabled_btn:['#setBusy','#onLineBtn', '#consultationBtn']
+		},
+		"busy":{ "code" : 1, msg:"忙碌",
+			btn_text:[],
+			enabled_btn:['#setFree', '#onLineBtn',  '#callBtn', '#consultationBtn']  //  '#transferBtn'
+		},
+		"customer_channel_hold" : { "code" : 623, msg:"通话已保持.",
+			btn_text:[],
+			enabled_btn:['#setFree',  '#callBtn', '#unHoldBtn', '#consultationBtn' ]
+		},
+	   "customer_channel_unhold" : { "code" : 624, msg:"通话已接回.",
+			  btn_text:[],
+			  enabled_btn:[ '#hangUpBtn', '#holdBtn' ]
+		}
+	};
+
+	ccPhoneBarSocket.phone_buttons = ['#setFree', '#setBusy', '#callBtn','#hangUpBtn' , '#resetStatus' ,'#onLineBtn', '#transferBtn', '#holdBtn', '#unHoldBtn', '#consultationBtn'];
+
+	// 更新状态显示
+	this.updatePhoneBar = function (msg, status_key) {
+		if(!_cc.callConfig.useDefaultUi){
+			console.log("callConfig.useDefaultUi = false , 已禁用默认ui工具条按钮.");
+			return;
+		}
+
+		if (msg) {
+			$("#callStatus").text(msg.msg);
+		}
+		var status_info = ccPhoneBarSocket.findItemByCode(status_key);
+		if (!status_info) {
+			return;
+		}
+
+		if(status_info.code === ccPhoneBarSocket.eventListWithTextInfo.status_changed.code){
+			if(msg.object.status === ccPhoneBarSocket.agentStatusEnum.free){
+				status_info = ccPhoneBarSocket.eventListWithTextInfo.free;
+			}else{
+				status_info = ccPhoneBarSocket.eventListWithTextInfo.busy;
+			}
+		}
+		// 判断当前是否为状态改变的事件;
+		// 显示预设的消息;
+		var msgSet = status_info.msg;
+
+		if(msgSet && msgSet.length > 0){
+			$("#callStatus").text(msgSet);
+		}
+
+		var btn_text = status_info.btn_text;
+		var enabled_btn = status_info.enabled_btn;
+		if (btn_text) {
+			$.each(btn_text, function (i, d) {
+				$(d.id).next().text(d.name);
+			});
+		}
+
+		if (enabled_btn.length === 0) {
+			return;
+		}
+
+		var all_btn = ccPhoneBarSocket.phone_buttons;
+		for (var i in all_btn) {
+			var idx = $.inArray(all_btn[i], enabled_btn);
+			if (idx < 0) {
+				$(all_btn[i]).removeClass('on');
+			} else {
+				$(enabled_btn[idx]).addClass('on');
+			}
+		}
+	};
+
+	/**
+	 *  初始化电话工具条ui按钮;
+	 */
+	this.initPhoneBarUI = function () {
+
+		window.onbeforeunload = function () {
+			if (!confirm('关闭网页将导致您无法接听电话,确定要关闭吗 ?')) return false;
+		};
+
+		$("#unHoldBtnLi").hide();
+
+		if(!_cc.callConfig.useDefaultUi){
+			console.log("callConfig.useDefaultUi = false , 已禁用默认ui工具条按钮.");
+			return;
+		}
+
+        $('#conferenceBtn').on('click', function () {
+                if(!_cc.getIsConnected()){
+                    console.log('请先上线.');
+                    return;
+                }
+                var confObjId = document.getElementById("conference_area");
+                if(confObjId.style.display === "block"){
+					confObjId.style.display = "none";
+				}else{
+					confObjId.style.display = "block";
+				}
+
+        });
+
+		$('#callBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				var destPhone = $.trim($("#ccphoneNumber").val());
+				var videoLevel = document.getElementById("videoLevelSelect").value;
+				var callType = document.forms[0].callType.value;
+				_cc.call(destPhone, callType,  videoLevel);
+			}
+		});
+		$('#setFree').on('click', function () {
+			if ($(this).hasClass('on')) {
+				_cc.setStatus(ccPhoneBarSocket.agentStatusEnum.free);
+			}
+		});
+		$('#setBusy').on('click', function () {
+			if ($(this).hasClass('on')) {
+				_cc.setStatus(ccPhoneBarSocket.agentStatusEnum.busy);
+			}
+		});
+
+		$('#setBusySubList').on('change', function () {
+			 let itemValue = $('#setBusySubList').val();
+			 console.log('set busy subStatus', itemValue);
+			 _cc.setStatus(itemValue);
+		});
+
+		$('#hangUpBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				_cc.hangup();
+			}
+		});
+
+		$('#holdBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				_cc.holdCall();
+			}
+		});
+
+		$('#unHoldBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				_cc.unHoldCall();
+			}
+		});
+
+		$("#doTransferBtn").hide();
+		$('#transferBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				if(!_cc.getIsConnected()){
+					console.log('请先上线.');
+					return;
+				}
+				var transferArea = document.getElementById("transfer_area");
+				if(transferArea.style.display === "block"){
+					transferArea.style.display = "none";
+					_phoneBar.unSubscribeAgentList();
+					$("#doTransferBtn").hide();
+					$("#doConsultationBtn").hide();
+				}else{
+					transferArea.style.display = "block";
+					populateGroupIdOptions();
+					_phoneBar.subscribeAgentList();
+					$("#doTransferBtn").show();
+					$("#doConsultationBtn").hide();
+				}
+			}
+		});
+
+        $("#stopCallWait").hide();
+        $("#transferCallWait").hide();
+		$("#doConsultationBtn").hide();
+		$('#consultationBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				if(!_cc.getIsConnected()){
+					console.log('请先上线.');
+					return;
+				}
+				var transferArea = document.getElementById("transfer_area");
+				if(transferArea.style.display === "block"){
+					transferArea.style.display = "none";
+					_phoneBar.unSubscribeAgentList();
+					$("#doConsultationBtn").hide();
+					$("#doTransferBtn").hide();
+				}else{
+					transferArea.style.display = "block";
+					populateGroupIdOptions();
+					_phoneBar.subscribeAgentList();
+					$("#doConsultationBtn").show();
+					$("#doTransferBtn").hide();
+				}
+			}
+		});
+
+		$('#onLineBtn').on('click', function () {
+			if ($(this).hasClass('on')) {
+				if (_cc.getIsConnected()) {
+					_cc.disconnect();
+				} else {
+					_cc.connect();
+				}
+			}else {
+				alert('当前不允许签出!');
+			}
+		});
+
+		$('#resetStatus').on('click', function () {
+			window.onbeforeunload = null;
+			location.reload();
+		});
+
+		//拨号文本框;收到键盘回车事件之后立即拨号
+		$("#ccphoneNumber").keydown(function (e) {
+			var curKey = e.which;
+			if (curKey === 13) {
+				var destPhone = $.trim($("#ccphoneNumber").val());
+				var videoLevel = document.getElementById("videoLevelSelect").value;
+				var callType = document.forms[0].callType.value;
+				_cc.call(destPhone,callType, videoLevel);
+				return false;
+			}
+		});
+
+		//ESC按键挂机功能支持
+		$(document).keyup(function (e) {
+			var key = e.which;
+			if (key === 27) {
+				console.log('按下了ESC键, 即将发送挂机指令.');
+				if(_cc.getIsConnected()){
+					if(_cc.callConfig["useDefaultUi"]) {
+						if ($('#hangUpBtn').hasClass('on')) {
+							_cc.hangup();
+						}
+					}else{
+						_cc.hangup();
+					}
+				}
+			}
+		});
+	};
+
+	/**
+	 *  保持通话
+	 */
+	this.holdCall = function(){
+		var cmd = {};
+		cmd.action="callHold";
+		cmd.body = {"cmd" : "hold", "args" : {} };
+		ws.send(JSON.stringify(cmd));
+	};
+
+	/**
+	 *  接回保持的通话
+	 */
+	this.unHoldCall = function(){
+		var cmd = {};
+		cmd.action="callHold";
+		cmd.body = {"cmd" : "unhold", "args" : {} };
+		ws.send(JSON.stringify(cmd));
+	};
+
+	/**
+	 *  订阅坐席状态列表
+	 */
+	this.subscribeAgentList = function(){
+		var cmd = {};
+		cmd.action="pollAgentList";
+		cmd.body = {"cmd" : "subscribe", "args" : {} };
+		ws.send(JSON.stringify(cmd));
+		_cc.subscribeAgentListStarted = true;
+	};
+
+	/**
+	 *  取消订阅坐席状态列表
+	 */
+	this.unSubscribeAgentList = function(){
+		if(_cc.subscribeAgentListStarted) {
+			_cc.subscribeAgentListStarted = false;
+			_cc.callConfig.agentList = null;
+			var cmd = {};
+			cmd.action = "pollAgentList";
+			cmd.body = {"cmd": "unSubscribe", "args": {}};
+			ws.send(JSON.stringify(cmd));
+		}
+	};
+
+	/*************************  以下是电话会议相关  ***************************/
+
+	/**
+	 *  启动会议; 仅限使用默认UI场景下使用;
+	 */
+	this.conferenceStartBtnUI = function(customerName){
+		var callType = document.getElementById("conf_call_type").value;
+		var confTemplate = document.getElementById("conf_template").value;
+		var layOut = document.getElementById("conf_layout").value;
+
+		_cc.setStatusBusy();
+
+		// 禁用外呼按钮
+		$("#callBtn").removeClass('on');
+
+		// 禁用置闲按钮
+		$("#setFree").removeClass('on');
+
+		// 禁用签出按钮
+		$("#onLineBtn").removeClass('on');
+
+		document.getElementById("startConference").setAttribute("disabled", "true");
+
+		if(_cc.getCallConnected()) {
+			let tips = "是否把当前通话转换为会议 ?";
+			console.log(tips);
+			if(confirm(tips)) {
+				_cc.transferToConference(layOut, confTemplate, callType, customerName);
+			}else{
+				document.getElementById("startConference").removeAttribute("disabled");
+			}
+		}else {
+			_cc.conferenceStart(layOut, confTemplate, callType);
+		}
+	};
+
+	/**
+	 *  添加会议成员; 仅限使用默认UI场景下使用;
+	 */
+	this.conferenceAddMemberBtnUI = function (reInvite, memberPhoneParam,  memberNameParam) {
+		var memberName = "";
+		var memberPhone = "";
+		var memberCallType = $.trim(document.getElementById("member_call_type").value);
+		var memberVideoLevel = $.trim(document.getElementById("member_video_level").value);
+
+		if(reInvite === 0) {
+			memberName = $("#member_name").val();
+			memberPhone = $("#member_phone").val();
+			if (memberName.length === 0 || $.trim(memberName) === "") {
+				alert('请填写参会者姓名!');
+				return;
+			}
+			if (memberPhone.length === 0 || $.trim(memberPhone) === "") {
+				alert('请填写参会者手机号!');
+				return;
+			}
+
+			memberName = $.trim(memberName);
+			memberPhone = $.trim(memberPhone);
+			// 使用会员成员html模版添加新成员
+			var templateObj = document.getElementById("conf_member_template");
+
+			var existMember = document.getElementById("conf_member_" + memberPhone) != null;
+			if (existMember) {
+				alert('会议成员已经存在,请不要重复添加!');
+				return;
+			}
+
+			var memberHtmlItem = templateObj.innerHTML;
+			memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_name}", "gm"), memberName);
+			memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_phone}", "gm"), memberPhone);
+			memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_status}", "gm"), "即将呼叫");
+
+			var li = document.createElement("li");
+			li.setAttribute("id", "conf_member_" + memberPhone);
+			li.setAttribute("class", "conf_member_item_row");
+			li.innerHTML = memberHtmlItem;
+
+			_cc.insertAfter(li, templateObj);
+
+			$("#member_name").val('');
+			$("#member_phone").val('');
+		}else{
+			memberName = memberNameParam;
+			memberPhone = memberPhoneParam;
+		}
+
+		// 隐藏 mute及 vmute按钮
+		var memberItemId = "#conf_member_" + memberPhone;
+		$(".conf_mute", $(memberItemId)).find("img").hide();
+		$(".conf_vmute", $(memberItemId)).find("img").hide();
+		$(".conf_re_invite", $(memberItemId)).hide();
+		$(".conf_status", $(memberItemId)).html("即将呼叫");
+
+		_cc.conferenceAddMembers( [
+			{"name": memberName, "phone": memberPhone, "callType" : memberCallType,  "videoLevel": memberVideoLevel}
+		]);
+	};
+
+	/**
+	 *  从现有通话添加会议成员;
+	 */
+	this.conferenceAddMemberFromExistCall = function (memberName, memberPhone) {
+		if (memberName.length === 0 || $.trim(memberName) === "") {
+			alert('请填写参会者姓名!');
+			return;
+		}
+		if (memberPhone.length === 0 || $.trim(memberPhone) === "") {
+			alert('请填写参会者手机号!');
+			return;
+		}
+		memberName = $.trim(memberName);
+		memberPhone = $.trim(memberPhone);
+
+		// 使用html模版添加新成员
+		var templateObj = document.getElementById("conf_member_template");
+		var existMember = document.getElementById("conf_member_" + memberPhone) != null;
+		if (existMember) {
+			alert('会议成员已经存在,请不要重复添加!');
+			return;
+		}
+
+		var memberHtmlItem = templateObj.innerHTML;
+		memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_name}", "gm"), memberName);
+		memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_phone}", "gm"), memberPhone);
+		memberHtmlItem = memberHtmlItem.replace(new RegExp("{member_status}", "gm"), "即将呼叫");
+		var li = document.createElement("li");
+		li.setAttribute("id", "conf_member_" + memberPhone);
+		li.setAttribute("class", "conf_member_item_row");
+		li.innerHTML = memberHtmlItem;
+		_cc.insertAfter(li, templateObj);
+
+		// 隐藏 mute及 vmute按钮
+		var memberItemId = "#conf_member_" + memberPhone;
+		$(".conf_mute", $(memberItemId)).find("img").show();
+		$(".conf_vmute", $(memberItemId)).find("img").show();
+		$(".conf_re_invite", $(memberItemId)).hide();
+		$(".conf_status", $(memberItemId)).html("通话中").css("color", "green");
+	};
+
+	/**
+	 *  通话转为会议
+	 * @param layOut
+	 * @param confTemplate
+	 * @param callType
+	 * @param customerName
+	 */
+	this.transferToConference = function (layOut, confTemplate, callType, customerName) {
+		console.log("try to transferToConference: ", layOut, confTemplate, callType, customerName);
+		_cc.callControl(
+			"transferToConference",
+			{
+				"layOut": layOut,
+				'callType': callType,
+				'confTemplate': confTemplate,
+				'customerName' : customerName
+			}
+		);
+	};
+
+
+	/**
+	 *  主持人启动电话会议
+	 * @param layOut 会议布局
+	 * @param confTemplate 会议模版
+	 * @param callType 会议类型
+	 */
+	this.conferenceStart = function(layOut, confTemplate, callType) {
+		console.log("正常发起多方通话");
+		var cmd = {};
+		cmd.action = "conference";
+		cmd.body = {"method": "startconf", "args": {
+				"layOut": layOut,
+				'callType': callType,
+				'confTemplate': confTemplate
+			}};
+		ws.send(JSON.stringify(cmd));
+	};
+
+
+	/**
+	 *  VMute会议成员(不显示视频);
+	 **/
+	this.conferenceVMuteMember = function(members) {
+		// single phone
+		if(typeof(members) === "string") {
+			this.conferenceControl("vmute",
+				[
+					{"phone": members}
+				]
+			);
+		}else{
+			// multiple phones array, e.g.: [  {"phone": '15005600321'}, {"phone": '15005600323'} ]
+			this.conferenceControl("vmute", members);
+		}
+	};
+
+
+	/**
+	 *  UnVMute会议成员(显示视频);
+	 **/
+	this.conferenceUnVMuteMember = function(members) {
+		// single phone
+		if(typeof(members) === "string") {
+			this.conferenceControl("unvmute",
+				[
+					{"phone": members}
+				]
+			);
+		}else{
+			// multiple phones array, e.g.: [  {"phone": '15005600321'}, {"phone": '15005600323'} ]
+			this.conferenceControl("unvmute", members);
+		}
+	};
+
+	/**
+	 *  禁言会议成员
+	 **/
+	this.conferenceMuteMember = function(members) {
+		// single phone
+		if(typeof(members) === "string") {
+			this.conferenceControl("mute",
+				[
+					{"phone": members}
+				]
+			);
+		}else{
+			// multiple phones array, e.g.: [  {"phone": '15005600321'}, {"phone": '15005600323'} ]
+			this.conferenceControl("mute", members);
+		}
+	};
+
+	/**
+	 *  解禁会议成员
+	 **/
+	this.conferenceUnMuteMember = function(members) {
+		// single phone
+		if(typeof(members) === "string") {
+			this.conferenceControl("unmute",
+				[
+					{"phone": members}
+				]
+			);
+		}else{
+			// multiple phones array, e.g.: [  {"phone": '15005600321'}, {"phone": '15005600323'} ]
+			this.conferenceControl("unmute", members);
+		}
+	};
+
+	/**
+	 *  增加会议成员
+	 **/
+	this.conferenceAddMembers = function(members) {
+		this.conferenceControl("add", members)
+	};
+
+
+	/**
+	 *  移除会议成员
+	 **/
+	this.conferenceRemoveMembers = function(members) {
+		// single phone
+		if(typeof(members) === "string") {
+			var memberItemObj = $("#conf_member_" + members);
+			if($(".conf_status", memberItemObj).text() === "通话中") {
+				this.conferenceControl("remove",
+					[
+						{"phone": members}
+					]
+				);
+			}
+			memberItemObj.remove();
+		}else{
+			// multiple phones array, e.g.: [  {"phone": '15005600321'}, {"phone": '15005600323'} ]
+			this.conferenceControl("remove", members);
+		}
+	};
+
+	/**
+	 *  结束电话会议
+	 **/
+	this.conferenceEnd = function() {
+		this.conferenceControl("endconf", [])
+	};
+
+
+	/**
+	 *  电话会议控制相关操作
+	 * @param action 操作
+	 * @param phoneList 会议成员
+	 */
+	this.conferenceControl = function (action, phoneList) {
+		var cmd = {};
+		cmd.action = "conference";
+		cmd.body = { "method": action, "memberList": phoneList };
+		ws.send(JSON.stringify(cmd));
+	};
+
+    /*************************  以下是通话监听相关  ***************************/
+
+    /**
+     *   拉取监听的通话列表
+     */
+    this.callMonitorDataPull = function (){
+        var cmd = {};
+        cmd.action = "monitorData";
+        cmd.body = {};
+        ws.send(JSON.stringify(cmd));
+    };
+
+	/**
+	 *   拉取排队中的电话列表
+	 */
+	this.inboundCallQueuePull = function (){
+		var cmd = {};
+		cmd.action = "inboundMonitorData";
+		cmd.body = {};
+		ws.send(JSON.stringify(cmd));
+	};
+
+    // 构造通话监听参数
+    this.monitorControl = function(action, argsObject){
+        var sessionControl = {};
+        sessionControl.action="callMonitor";
+        sessionControl.body = {"cmd" : action, "args" : argsObject };
+        ws.send(JSON.stringify(sessionControl));
+    };
+
+    /**
+     * 通话监听
+     * @param { 通话id } callId
+     * @returns
+     */
+    this.callMonitorStart = function(callId){
+        if(callId == null || callId.length === 0) {
+            console.log('请提供待监听电话的 callId !');
+            return;
+        }
+
+        if(!_cc.getIsConnected()){
+            console.log('请先上线.');
+            return;
+        }
+
+        this.monitorControl(
+            "startMonitoring",
+            {
+                'callSpyId': callId
+            }
+        );
+    };
+
+
+    /**
+     * 结束监听
+     */
+    this.callMonitorEnd = function(){
+        if(!_cc.getIsConnected()){
+            console.log('请先上线.');
+            return;
+        }
+
+        this.monitorControl(
+            "endMonitoring",{}
+        );
+    };
+
+}
+

BIN
docs/icon/extractaudio.png


BIN
docs/icon/video.png


BIN
docs/images/file.gif


BIN
docs/images/keyboard.png


BIN
docs/images/logo.jpg


BIN
docs/images/minus.gif


BIN
docs/images/mute.jpg


BIN
docs/images/mutein.ico


BIN
docs/images/network-callcenter.png


BIN
docs/images/network-tra.png


BIN
docs/images/no_video.jpg


BIN
docs/images/phone-bar.png


BIN
docs/images/phone.png


BIN
docs/images/phonebar/busy_enable.png


BIN
docs/images/phonebar/call.png


BIN
docs/images/phonebar/conference.png


BIN
docs/images/phonebar/consultation.png


BIN
docs/images/phonebar/hangup_enable.png


BIN
docs/images/phonebar/hold.png


BIN
docs/images/phonebar/online.png


BIN
docs/images/phonebar/reset.png


BIN
docs/images/phonebar/setFree.png


BIN
docs/images/phonebar/transfer.png


BIN
docs/images/phonebar/unhold.png


BIN
docs/images/plus.gif


BIN
docs/images/process-flow.png


BIN
docs/images/transparent-25-bg.png


BIN
docs/images/transparent-50-bg.png


BIN
docs/images/transparent-black-75-bg.png


BIN
docs/images/unmute.jpg


BIN
docs/images/video.jpg


BIN
docs/images/wechat.png


BIN
docs/images/wechat2.png


+ 9800 - 0
docs/jquery-1.11.0.js

@@ -0,0 +1,9800 @@
+/*!
+* jQuery JavaScript Library v1.11.0
+* http://jquery.com/
+*
+* Includes Sizzle.js
+* http://sizzlejs.com/
+*
+* Copyright 2005, 2014m jQuery Foundation, Inc. and other contributors
+* Released under the MIT license
+* http://jquery.org/license
+*
+* Date: 2014-06-10T10:12Z
+*/
+(function( window, undefined ) {
+
+// Can't do this because several apps including ASP.NET trace
+// the stack via arguments.caller.callee and Firefox dies if
+// you try to trace through "use strict" call chains. (#13335)
+// Support: Firefox 18+
+//"use strict";
+var
+	// The deferred used on DOM ready
+	readyList,
+
+	// A central reference to the root jQuery(document)
+	rootjQuery,
+
+	// Support: IE<10
+	// For `typeof xmlNode.method` instead of `xmlNode.method !== undefined`
+	core_strundefined = typeof undefined,
+
+	// Use the correct document accordingly with window argument (sandbox)
+	location = window.location,
+	document = window.document,
+	docElem = document.documentElement,
+
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	// [[Class]] -> type pairs
+	class2type = {},
+
+	// List of deleted data cache ids, so we can reuse them
+	core_deletedIds = [],
+
+	core_version = "1.10.0",
+
+	// Save a reference to some core methods
+	core_concat = core_deletedIds.concat,
+	core_push = core_deletedIds.push,
+	core_slice = core_deletedIds.slice,
+	core_indexOf = core_deletedIds.indexOf,
+	core_toString = class2type.toString,
+	core_hasOwn = class2type.hasOwnProperty,
+	core_trim = core_version.trim,
+
+	// Define a local copy of jQuery
+	jQuery = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context, rootjQuery );
+	},
+
+	// Used for matching numbers
+	core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,
+
+	// Used for splitting on whitespace
+	core_rnotwhite = /\S+/g,
+
+	// Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+	// A simple way to check for HTML strings
+	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+	// Strict HTML recognition (#11290: must start with <)
+	rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+
+	// Match a standalone tag
+	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+	// JSON RegExp
+	rvalidchars = /^[\],:{}\s]*$/,
+	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+	rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+	rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,
+
+	// Matches dashed string for camelizing
+	rmsPrefix = /^-ms-/,
+	rdashAlpha = /-([\da-z])/gi,
+
+	// Used by jQuery.camelCase as callback to replace()
+	fcamelCase = function( all, letter ) {
+		return letter.toUpperCase();
+	},
+
+	// The ready event handler
+	completed = function( event ) {
+
+		// readyState === "complete" is good enough for us to call the dom ready in oldIE
+		if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) {
+			detach();
+			jQuery.ready();
+		}
+	},
+	// Clean-up method for dom ready events
+	detach = function() {
+		if ( document.addEventListener ) {
+			document.removeEventListener( "DOMContentLoaded", completed, false );
+			window.removeEventListener( "load", completed, false );
+
+		} else {
+			document.detachEvent( "onreadystatechange", completed );
+			window.detachEvent( "onload", completed );
+		}
+	};
+
+jQuery.fn = jQuery.prototype = {
+	// The current version of jQuery being used
+	jquery: core_version,
+
+	constructor: jQuery,
+	init: function( selector, context, rootjQuery ) {
+		var match, elem;
+
+		// HANDLE: $(""), $(null), $(undefined), $(false)
+		if ( !selector ) {
+			return this;
+		}
+
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+				// Assume that strings that start and end with <> are HTML and skip the regex check
+				match = [ null, selector, null ];
+
+			} else {
+				match = rquickExpr.exec( selector );
+			}
+
+			// Match html or make sure no context is specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] ) {
+					context = context instanceof jQuery ? context[0] : context;
+
+					// scripts is true for back-compat
+					jQuery.merge( this, jQuery.parseHTML(
+						match[1],
+						context && context.nodeType ? context.ownerDocument || context : document,
+						true
+					) );
+
+					// HANDLE: $(html, props)
+					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+						for ( match in context ) {
+							// Properties of context are called as methods if possible
+							if ( jQuery.isFunction( this[ match ] ) ) {
+								this[ match ]( context[ match ] );
+
+							// ...and otherwise set as attributes
+							} else {
+								this.attr( match, context[ match ] );
+							}
+						}
+					}
+
+					return this;
+
+				// HANDLE: $(#id)
+				} else {
+					elem = document.getElementById( match[2] );
+
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE and Opera return items
+						// by name instead of ID
+						if ( elem.id !== match[2] ) {
+							return rootjQuery.find( selector );
+						}
+
+						// Otherwise, we inject the element directly into the jQuery object
+						this.length = 1;
+						this[0] = elem;
+					}
+
+					this.context = document;
+					this.selector = selector;
+					return this;
+				}
+
+			// HANDLE: $(expr, $(...))
+			} else if ( !context || context.jquery ) {
+				return ( context || rootjQuery ).find( selector );
+
+			// HANDLE: $(expr, context)
+			// (which is just equivalent to: $(context).find(expr)
+			} else {
+				return this.constructor( context ).find( selector );
+			}
+
+		// HANDLE: $(DOMElement)
+		} else if ( selector.nodeType ) {
+			this.context = this[0] = selector;
+			this.length = 1;
+			return this;
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) ) {
+			return rootjQuery.ready( selector );
+		}
+
+		if ( selector.selector !== undefined ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return jQuery.makeArray( selector, this );
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The default length of a jQuery object is 0
+	length: 0,
+
+	toArray: function() {
+		return core_slice.call( this );
+	},
+
+	// Get the Nth element in the matched element set OR
+	// Get the whole matched element set as a clean array
+	get: function( num ) {
+		return num == null ?
+
+			// Return a 'clean' array
+			this.toArray() :
+
+			// Return just the object
+			( num < 0 ? this[ this.length + num ] : this[ num ] );
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems ) {
+
+		// Build a new jQuery matched element set
+		var ret = jQuery.merge( this.constructor(), elems );
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+		ret.context = this.context;
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Execute a callback for every element in the matched set.
+	// (You can seed the arguments with an array of args, but this is
+	// only used internally.)
+	each: function( callback, args ) {
+		return jQuery.each( this, callback, args );
+	},
+
+	ready: function( fn ) {
+		// Add the callback
+		jQuery.ready.promise().done( fn );
+
+		return this;
+	},
+
+	slice: function() {
+		return this.pushStack( core_slice.apply( this, arguments ) );
+	},
+
+	first: function() {
+		return this.eq( 0 );
+	},
+
+	last: function() {
+		return this.eq( -1 );
+	},
+
+	eq: function( i ) {
+		var len = this.length,
+			j = +i + ( i < 0 ? len : 0 );
+		return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function( elem, i ) {
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	end: function() {
+		return this.prevObject || this.constructor(null);
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: core_push,
+	sort: [].sort,
+	splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+	var src, copyIsArray, copy, name, options, clone,
+		target = arguments[0] || {},
+		i = 1,
+		length = arguments.length,
+		deep = false;
+
+	// Handle a deep copy situation
+	if ( typeof target === "boolean" ) {
+		deep = target;
+		target = arguments[1] || {};
+		// skip the boolean and the target
+		i = 2;
+	}
+
+	// Handle case when target is a string or something (possible in deep copy)
+	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+		target = {};
+	}
+
+	// extend jQuery itself if only one argument is passed
+	if ( length === i ) {
+		target = this;
+		--i;
+	}
+
+	for ( ; i < length; i++ ) {
+		// Only deal with non-null/undefined values
+		if ( (options = arguments[ i ]) != null ) {
+			// Extend the base object
+			for ( name in options ) {
+				src = target[ name ];
+				copy = options[ name ];
+
+				// Prevent never-ending loop
+				if ( target === copy ) {
+					continue;
+				}
+
+				// Recurse if we're merging plain objects or arrays
+				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+					if ( copyIsArray ) {
+						copyIsArray = false;
+						clone = src && jQuery.isArray(src) ? src : [];
+
+					} else {
+						clone = src && jQuery.isPlainObject(src) ? src : {};
+					}
+
+					// Never move original objects, clone them
+					target[ name ] = jQuery.extend( deep, clone, copy );
+
+				// Don't bring in undefined values
+				} else if ( copy !== undefined ) {
+					target[ name ] = copy;
+				}
+			}
+		}
+	}
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	// Unique for each copy of jQuery on the page
+	// Non-digits removed to match rinlinejQuery
+	expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
+
+	noConflict: function( deep ) {
+		if ( window.$ === jQuery ) {
+			window.$ = _$;
+		}
+
+		if ( deep && window.jQuery === jQuery ) {
+			window.jQuery = _jQuery;
+		}
+
+		return jQuery;
+	},
+
+	// Is the DOM ready to be used? Set to true once it occurs.
+	isReady: false,
+
+	// A counter to track how many items to wait for before
+	// the ready event fires. See #6781
+	readyWait: 1,
+
+	// Hold (or release) the ready event
+	holdReady: function( hold ) {
+		if ( hold ) {
+			jQuery.readyWait++;
+		} else {
+			jQuery.ready( true );
+		}
+	},
+
+	// Handle when the DOM is ready
+	ready: function( wait ) {
+
+		// Abort if there are pending holds or we're already ready
+		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+			return;
+		}
+
+		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+		if ( !document.body ) {
+			return setTimeout( jQuery.ready );
+		}
+
+		// Remember that the DOM is ready
+		jQuery.isReady = true;
+
+		// If a normal DOM Ready event fired, decrement, and wait if need be
+		if ( wait !== true && --jQuery.readyWait > 0 ) {
+			return;
+		}
+
+		// If there are functions bound, to execute
+		readyList.resolveWith( document, [ jQuery ] );
+
+		// Trigger any bound ready events
+		if ( jQuery.fn.trigger ) {
+			jQuery( document ).trigger("ready").off("ready");
+		}
+	},
+
+	// See test/unit/core.js for details concerning isFunction.
+	// Since version 1.3, DOM methods and functions like alert
+	// aren't supported. They return false on IE (#2968).
+	isFunction: function( obj ) {
+		return jQuery.type(obj) === "function";
+	},
+
+	isArray: Array.isArray || function( obj ) {
+		return jQuery.type(obj) === "array";
+	},
+
+	isWindow: function( obj ) {
+		/* jshint eqeqeq: false */
+		return obj != null && obj == obj.window;
+	},
+
+	isNumeric: function( obj ) {
+		return !isNaN( parseFloat(obj) ) && isFinite( obj );
+	},
+
+	type: function( obj ) {
+		if ( obj == null ) {
+			return String( obj );
+		}
+		return typeof obj === "object" || typeof obj === "function" ?
+			class2type[ core_toString.call(obj) ] || "object" :
+			typeof obj;
+	},
+
+	isPlainObject: function( obj ) {
+		var key;
+
+		// Must be an Object.
+		// Because of IE, we also have to check the presence of the constructor property.
+		// Make sure that DOM nodes and window objects don't pass through, as well
+		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+			return false;
+		}
+
+		try {
+			// Not own constructor property must be Object
+			if ( obj.constructor &&
+				!core_hasOwn.call(obj, "constructor") &&
+				!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+				return false;
+			}
+		} catch ( e ) {
+			// IE8,9 Will throw exceptions on certain host objects #9897
+			return false;
+		}
+
+		// Support: IE<9
+		// Handle iteration over inherited properties before own properties.
+		if ( jQuery.support.ownLast ) {
+			for ( key in obj ) {
+				return core_hasOwn.call( obj, key );
+			}
+		}
+
+		// Own properties are enumerated firstly, so to speed up,
+		// if last one is own, then all properties are own.
+		for ( key in obj ) {}
+
+		return key === undefined || core_hasOwn.call( obj, key );
+	},
+
+	isEmptyObject: function( obj ) {
+		var name;
+		for ( name in obj ) {
+			return false;
+		}
+		return true;
+	},
+
+	error: function( msg ) {
+		throw new Error( msg );
+	},
+
+	// data: string of html
+	// context (optional): If specified, the fragment will be created in this context, defaults to document
+	// keepScripts (optional): If true, will include scripts passed in the html string
+	parseHTML: function( data, context, keepScripts ) {
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		if ( typeof context === "boolean" ) {
+			keepScripts = context;
+			context = false;
+		}
+		context = context || document;
+
+		var parsed = rsingleTag.exec( data ),
+			scripts = !keepScripts && [];
+
+		// Single tag
+		if ( parsed ) {
+			return [ context.createElement( parsed[1] ) ];
+		}
+
+		parsed = jQuery.buildFragment( [ data ], context, scripts );
+		if ( scripts ) {
+			jQuery( scripts ).remove();
+		}
+		return jQuery.merge( [], parsed.childNodes );
+	},
+
+	parseJSON: function( data ) {
+		// Attempt to parse using the native JSON parser first
+		if ( window.JSON && window.JSON.parse ) {
+			return window.JSON.parse( data );
+		}
+
+		if ( data === null ) {
+			return data;
+		}
+
+		if ( typeof data === "string" ) {
+
+			// Make sure leading/trailing whitespace is removed (IE can't handle it)
+			data = jQuery.trim( data );
+
+			if ( data ) {
+				// Make sure the incoming data is actual JSON
+				// Logic borrowed from http://json.org/json2.js
+				if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+					.replace( rvalidtokens, "]" )
+					.replace( rvalidbraces, "")) ) {
+
+					return ( new Function( "return " + data ) )();
+				}
+			}
+		}
+
+		jQuery.error( "Invalid JSON: " + data );
+	},
+
+	// Cross-browser xml parsing
+	parseXML: function( data ) {
+		var xml, tmp;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		try {
+			if ( window.DOMParser ) { // Standard
+				tmp = new DOMParser();
+				xml = tmp.parseFromString( data , "text/xml" );
+			} else { // IE
+				xml = new ActiveXObject( "Microsoft.XMLDOM" );
+				xml.async = "false";
+				xml.loadXML( data );
+			}
+		} catch( e ) {
+			xml = undefined;
+		}
+		if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+			jQuery.error( "Invalid XML: " + data );
+		}
+		return xml;
+	},
+
+	noop: function() {},
+
+	// Evaluates a script in a global context
+	// Workarounds based on findings by Jim Driscoll
+	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+	globalEval: function( data ) {
+		if ( data && jQuery.trim( data ) ) {
+			// We use execScript on Internet Explorer
+			// We use an anonymous function so that context is window
+			// rather than jQuery in Firefox
+			( window.execScript || function( data ) {
+				window[ "eval" ].call( window, data );
+			} )( data );
+		}
+	},
+
+	// Convert dashed to camelCase; used by the css and data modules
+	// Microsoft forgot to hump their vendor prefix (#9572)
+	camelCase: function( string ) {
+		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+	},
+
+	// args is for internal usage only
+	each: function( obj, callback, args ) {
+		var value,
+			i = 0,
+			length = obj.length,
+			isArray = isArraylike( obj );
+
+		if ( args ) {
+			if ( isArray ) {
+				for ( ; i < length; i++ ) {
+					value = callback.apply( obj[ i ], args );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( i in obj ) {
+					value = callback.apply( obj[ i ], args );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			}
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( isArray ) {
+				for ( ; i < length; i++ ) {
+					value = callback.call( obj[ i ], i, obj[ i ] );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( i in obj ) {
+					value = callback.call( obj[ i ], i, obj[ i ] );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			}
+		}
+
+		return obj;
+	},
+
+	// Use native String.trim function wherever possible
+	trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+		function( text ) {
+			return text == null ?
+				"" :
+				core_trim.call( text );
+		} :
+
+		// Otherwise use our own trimming functionality
+		function( text ) {
+			return text == null ?
+				"" :
+				( text + "" ).replace( rtrim, "" );
+		},
+
+	// results is for internal usage only
+	makeArray: function( arr, results ) {
+		var ret = results || [];
+
+		if ( arr != null ) {
+			if ( isArraylike( Object(arr) ) ) {
+				jQuery.merge( ret,
+					typeof arr === "string" ?
+					[ arr ] : arr
+				);
+			} else {
+				core_push.call( ret, arr );
+			}
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, arr, i ) {
+		var len;
+
+		if ( arr ) {
+			if ( core_indexOf ) {
+				return core_indexOf.call( arr, elem, i );
+			}
+
+			len = arr.length;
+			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+			for ( ; i < len; i++ ) {
+				// Skip accessing in sparse arrays
+				if ( i in arr && arr[ i ] === elem ) {
+					return i;
+				}
+			}
+		}
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		var l = second.length,
+			i = first.length,
+			j = 0;
+
+		if ( typeof l === "number" ) {
+			for ( ; j < l; j++ ) {
+				first[ i++ ] = second[ j ];
+			}
+		} else {
+			while ( second[j] !== undefined ) {
+				first[ i++ ] = second[ j++ ];
+			}
+		}
+
+		first.length = i;
+
+		return first;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var retVal,
+			ret = [],
+			i = 0,
+			length = elems.length;
+		inv = !!inv;
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( ; i < length; i++ ) {
+			retVal = !!callback( elems[ i ], i );
+			if ( inv !== retVal ) {
+				ret.push( elems[ i ] );
+			}
+		}
+
+		return ret;
+	},
+
+	// arg is for internal usage only
+	map: function( elems, callback, arg ) {
+		var value,
+			i = 0,
+			length = elems.length,
+			isArray = isArraylike( elems ),
+			ret = [];
+
+		// Go through the array, translating each of the items to their
+		if ( isArray ) {
+			for ( ; i < length; i++ ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+
+		// Go through every key on the object,
+		} else {
+			for ( i in elems ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+		}
+
+		// Flatten any nested arrays
+		return core_concat.apply( [], ret );
+	},
+
+	// A global GUID counter for objects
+	guid: 1,
+
+	// Bind a function to a context, optionally partially applying any
+	// arguments.
+	proxy: function( fn, context ) {
+		var args, proxy, tmp;
+
+		if ( typeof context === "string" ) {
+			tmp = fn[ context ];
+			context = fn;
+			fn = tmp;
+		}
+
+		// Quick check to determine if target is callable, in the spec
+		// this throws a TypeError, but we will just return undefined.
+		if ( !jQuery.isFunction( fn ) ) {
+			return undefined;
+		}
+
+		// Simulated bind
+		args = core_slice.call( arguments, 2 );
+		proxy = function() {
+			return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
+		};
+
+		// Set the guid of unique handler to the same of original handler, so it can be removed
+		proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+		return proxy;
+	},
+
+	// Multifunctional method to get and set values of a collection
+	// The value/s can optionally be executed if it's a function
+	access: function( elems, fn, key, value, chainable, emptyGet, raw ) {
+		var i = 0,
+			length = elems.length,
+			bulk = key == null;
+
+		// Sets many values
+		if ( jQuery.type( key ) === "object" ) {
+			chainable = true;
+			for ( i in key ) {
+				jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
+			}
+
+		// Sets one value
+		} else if ( value !== undefined ) {
+			chainable = true;
+
+			if ( !jQuery.isFunction( value ) ) {
+				raw = true;
+			}
+
+			if ( bulk ) {
+				// Bulk operations run against the entire set
+				if ( raw ) {
+					fn.call( elems, value );
+					fn = null;
+
+				// ...except when executing function values
+				} else {
+					bulk = fn;
+					fn = function( elem, key, value ) {
+						return bulk.call( jQuery( elem ), value );
+					};
+				}
+			}
+
+			if ( fn ) {
+				for ( ; i < length; i++ ) {
+					fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
+				}
+			}
+		}
+
+		return chainable ?
+			elems :
+
+			// Gets
+			bulk ?
+				fn.call( elems ) :
+				length ? fn( elems[0], key ) : emptyGet;
+	},
+
+	now: function() {
+		return ( new Date() ).getTime();
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations.
+	// Note: this method belongs to the css module but it's needed here for the support module.
+	// If support gets modularized, this method should be moved back to the css module.
+	swap: function( elem, options, callback, args ) {
+		var ret, name,
+			old = {};
+
+		// Remember the old values, and insert the new ones
+		for ( name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		ret = callback.apply( elem, args || [] );
+
+		// Revert the old values
+		for ( name in options ) {
+			elem.style[ name ] = old[ name ];
+		}
+
+		return ret;
+	}
+});
+
+jQuery.ready.promise = function( obj ) {
+	if ( !readyList ) {
+
+		readyList = jQuery.Deferred();
+
+		// Catch cases where $(document).ready() is called after the browser event has already occurred.
+		// we once tried to use readyState "interactive" here, but it caused issues like the one
+		// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+		if ( document.readyState === "complete" ) {
+			// Handle it asynchronously to allow scripts the opportunity to delay ready
+			setTimeout( jQuery.ready );
+
+		// Standards-based browsers support DOMContentLoaded
+		} else if ( document.addEventListener ) {
+			// Use the handy event callback
+			document.addEventListener( "DOMContentLoaded", completed, false );
+
+			// A fallback to window.onload, that will always work
+			window.addEventListener( "load", completed, false );
+
+		// If IE event model is used
+		} else {
+			// Ensure firing before onload, maybe late but safe also for iframes
+			document.attachEvent( "onreadystatechange", completed );
+
+			// A fallback to window.onload, that will always work
+			window.attachEvent( "onload", completed );
+
+			// If IE and not a frame
+			// continually check to see if the document is ready
+			var top = false;
+
+			try {
+				top = window.frameElement == null && document.documentElement;
+			} catch(e) {}
+
+			if ( top && top.doScroll ) {
+				(function doScrollCheck() {
+					if ( !jQuery.isReady ) {
+
+						try {
+							// Use the trick by Diego Perini
+							// http://javascript.nwbox.com/IEContentLoaded/
+							top.doScroll("left");
+						} catch(e) {
+							return setTimeout( doScrollCheck, 50 );
+						}
+
+						// detach all dom ready events
+						detach();
+
+						// and execute any waiting functions
+						jQuery.ready();
+					}
+				})();
+			}
+		}
+	}
+	return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
+	class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+function isArraylike( obj ) {
+	var length = obj.length,
+		type = jQuery.type( obj );
+
+	if ( jQuery.isWindow( obj ) ) {
+		return false;
+	}
+
+	if ( obj.nodeType === 1 && length ) {
+		return true;
+	}
+
+	return type === "array" || type !== "function" &&
+		( length === 0 ||
+		typeof length === "number" && length > 0 && ( length - 1 ) in obj );
+}
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+/*!
+ * Sizzle CSS Selector Engine v1.9.4-pre
+ * http://sizzlejs.com/
+ *
+ * Copyright 2013 jQuery Foundation, Inc. and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2013-05-15
+ */
+(function( window, undefined ) {
+
+var i,
+	support,
+	cachedruns,
+	Expr,
+	getText,
+	isXML,
+	compile,
+	outermostContext,
+	sortInput,
+
+	// Local document vars
+	setDocument,
+	document,
+	docElem,
+	documentIsHTML,
+	rbuggyQSA,
+	rbuggyMatches,
+	matches,
+	contains,
+
+	// Instance-specific data
+	expando = "sizzle" + -(new Date()),
+	preferredDoc = window.document,
+	dirruns = 0,
+	done = 0,
+	classCache = createCache(),
+	tokenCache = createCache(),
+	compilerCache = createCache(),
+	hasDuplicate = false,
+	sortOrder = function() { return 0; },
+
+	// General-purpose constants
+	strundefined = typeof undefined,
+	MAX_NEGATIVE = 1 << 31,
+
+	// Instance methods
+	hasOwn = ({}).hasOwnProperty,
+	arr = [],
+	pop = arr.pop,
+	push_native = arr.push,
+	push = arr.push,
+	slice = arr.slice,
+	// Use a stripped-down indexOf if we can't use a native one
+	indexOf = arr.indexOf || function( elem ) {
+		var i = 0,
+			len = this.length;
+		for ( ; i < len; i++ ) {
+			if ( this[i] === elem ) {
+				return i;
+			}
+		}
+		return -1;
+	},
+
+	booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+	// Regular expressions
+
+	// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+	whitespace = "[\\x20\\t\\r\\n\\f]",
+	// http://www.w3.org/TR/css3-syntax/#characters
+	characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+	// Loosely modeled on CSS identifier characters
+	// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
+	// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+	identifier = characterEncoding.replace( "w", "w#" ),
+
+	// Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+	attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+		"*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+	// Prefer arguments quoted,
+	//   then not containing pseudos/brackets,
+	//   then attribute selectors/non-parenthetical expressions,
+	//   then anything else
+	// These preferences are here to reduce the number of selectors
+	//   needing tokenize in the PSEUDO preFilter
+	pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)",
+
+	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+	rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+	rsibling = new RegExp( whitespace + "*[+~]" ),
+	rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ),
+
+	rpseudo = new RegExp( pseudos ),
+	ridentifier = new RegExp( "^" + identifier + "$" ),
+
+	matchExpr = {
+		"ID": new RegExp( "^#(" + characterEncoding + ")" ),
+		"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+		"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+		"ATTR": new RegExp( "^" + attributes ),
+		"PSEUDO": new RegExp( "^" + pseudos ),
+		"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+		"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+		// For use in libraries implementing .is()
+		// We use this for POS matching in `select`
+		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+			whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+	},
+
+	rnative = /^[^{]+\{\s*\[native \w/,
+
+	// Easily-parseable/retrievable ID or TAG or CLASS selectors
+	rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+	rinputs = /^(?:input|select|textarea|button)$/i,
+	rheader = /^h\d$/i,
+
+	rescape = /'|\\/g,
+
+	// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+	runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+	funescape = function( _, escaped, escapedWhitespace ) {
+		var high = "0x" + escaped - 0x10000;
+		// NaN means non-codepoint
+		// Support: Firefox
+		// Workaround erroneous numeric interpretation of +"0x"
+		return high !== high || escapedWhitespace ?
+			escaped :
+			// BMP codepoint
+			high < 0 ?
+				String.fromCharCode( high + 0x10000 ) :
+				// Supplemental Plane codepoint (surrogate pair)
+				String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+	};
+
+// Optimize for push.apply( _, NodeList )
+try {
+	push.apply(
+		(arr = slice.call( preferredDoc.childNodes )),
+		preferredDoc.childNodes
+	);
+	// Support: Android<4.0
+	// Detect silently failing push.apply
+	arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+	push = { apply: arr.length ?
+
+		// Leverage slice if possible
+		function( target, els ) {
+			push_native.apply( target, slice.call(els) );
+		} :
+
+		// Support: IE<9
+		// Otherwise append directly
+		function( target, els ) {
+			var j = target.length,
+				i = 0;
+			// Can't trust NodeList.length
+			while ( (target[j++] = els[i++]) ) {}
+			target.length = j - 1;
+		}
+	};
+}
+
+function Sizzle( selector, context, results, seed ) {
+	var match, elem, m, nodeType,
+		// QSA vars
+		i, groups, old, nid, newContext, newSelector;
+
+	if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+		setDocument( context );
+	}
+
+	context = context || document;
+	results = results || [];
+
+	if ( !selector || typeof selector !== "string" ) {
+		return results;
+	}
+
+	if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
+		return [];
+	}
+
+	if ( documentIsHTML && !seed ) {
+
+		// Shortcuts
+		if ( (match = rquickExpr.exec( selector )) ) {
+			// Speed-up: Sizzle("#ID")
+			if ( (m = match[1]) ) {
+				if ( nodeType === 9 ) {
+					elem = context.getElementById( m );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE, Opera, and Webkit return items
+						// by name instead of ID
+						if ( elem.id === m ) {
+							results.push( elem );
+							return results;
+						}
+					} else {
+						return results;
+					}
+				} else {
+					// Context is not a document
+					if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+						contains( context, elem ) && elem.id === m ) {
+						results.push( elem );
+						return results;
+					}
+				}
+
+			// Speed-up: Sizzle("TAG")
+			} else if ( match[2] ) {
+				push.apply( results, context.getElementsByTagName( selector ) );
+				return results;
+
+			// Speed-up: Sizzle(".CLASS")
+			} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
+				push.apply( results, context.getElementsByClassName( m ) );
+				return results;
+			}
+		}
+
+		// QSA path
+		if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+			nid = old = expando;
+			newContext = context;
+			newSelector = nodeType === 9 && selector;
+
+			// qSA works strangely on Element-rooted queries
+			// We can work around this by specifying an extra ID on the root
+			// and working up from there (Thanks to Andrew Dupont for the technique)
+			// IE 8 doesn't work on object elements
+			if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+				groups = tokenize( selector );
+
+				if ( (old = context.getAttribute("id")) ) {
+					nid = old.replace( rescape, "\\$&" );
+				} else {
+					context.setAttribute( "id", nid );
+				}
+				nid = "[id='" + nid + "'] ";
+
+				i = groups.length;
+				while ( i-- ) {
+					groups[i] = nid + toSelector( groups[i] );
+				}
+				newContext = rsibling.test( selector ) && context.parentNode || context;
+				newSelector = groups.join(",");
+			}
+
+			if ( newSelector ) {
+				try {
+					push.apply( results,
+						newContext.querySelectorAll( newSelector )
+					);
+					return results;
+				} catch(qsaError) {
+				} finally {
+					if ( !old ) {
+						context.removeAttribute("id");
+					}
+				}
+			}
+		}
+	}
+
+	// All others
+	return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * For feature detection
+ * @param {Function} fn The function to test for native support
+ */
+function isNative( fn ) {
+	return rnative.test( fn + "" );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
+ *	property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ *	deleting the oldest entry
+ */
+function createCache() {
+	var keys = [];
+
+	function cache( key, value ) {
+		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+		if ( keys.push( key += " " ) > Expr.cacheLength ) {
+			// Only keep the most recent entries
+			delete cache[ keys.shift() ];
+		}
+		return (cache[ key ] = value);
+	}
+	return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+	fn[ expando ] = true;
+	return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+	var div = document.createElement("div");
+
+	try {
+		return !!fn( div );
+	} catch (e) {
+		return false;
+	} finally {
+		// Remove from its parent by default
+		if ( div.parentNode ) {
+			div.parentNode.removeChild( div );
+		}
+		// release memory in IE
+		div = null;
+	}
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied if the test fails
+ * @param {Boolean} test The result of a test. If true, null will be set as the handler in leiu of the specified handler
+ */
+function addHandle( attrs, handler, test ) {
+	attrs = attrs.split("|");
+	var current,
+		i = attrs.length,
+		setHandle = test ? null : handler;
+
+	while ( i-- ) {
+		// Don't override a user's handler
+		if ( !(current = Expr.attrHandle[ attrs[i] ]) || current === handler ) {
+			Expr.attrHandle[ attrs[i] ] = setHandle;
+		}
+	}
+}
+
+/**
+ * Fetches boolean attributes by node
+ * @param {Element} elem
+ * @param {String} name
+ */
+function boolHandler( elem, name ) {
+	// XML does not need to be checked as this will not be assigned for XML documents
+	var val = elem.getAttributeNode( name );
+	return val && val.specified ?
+		val.value :
+		elem[ name ] === true ? name.toLowerCase() : null;
+}
+
+/**
+ * Fetches attributes without interpolation
+ * http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+ * @param {Element} elem
+ * @param {String} name
+ */
+function interpolationHandler( elem, name ) {
+	// XML does not need to be checked as this will not be assigned for XML documents
+	return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+}
+
+/**
+ * Uses defaultValue to retrieve value in IE6/7
+ * @param {Element} elem
+ * @param {String} name
+ */
+function valueHandler( elem ) {
+	// Ignore the value *property* on inputs by using defaultValue
+	// Fallback to Sizzle.attr by returning undefined where appropriate
+	// XML does not need to be checked as this will not be assigned for XML documents
+	if ( elem.nodeName.toLowerCase() === "input" ) {
+		return elem.defaultValue;
+	}
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns Returns -1 if a precedes b, 1 if a follows b
+ */
+function siblingCheck( a, b ) {
+	var cur = b && a,
+		diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+			( ~b.sourceIndex || MAX_NEGATIVE ) -
+			( ~a.sourceIndex || MAX_NEGATIVE );
+
+	// Use IE sourceIndex if available on both nodes
+	if ( diff ) {
+		return diff;
+	}
+
+	// Check if b follows a
+	if ( cur ) {
+		while ( (cur = cur.nextSibling) ) {
+			if ( cur === b ) {
+				return -1;
+			}
+		}
+	}
+
+	return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return name === "input" && elem.type === type;
+	};
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return (name === "input" || name === "button") && elem.type === type;
+	};
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+	return markFunction(function( argument ) {
+		argument = +argument;
+		return markFunction(function( seed, matches ) {
+			var j,
+				matchIndexes = fn( [], seed.length, argument ),
+				i = matchIndexes.length;
+
+			// Match elements found at the specified indexes
+			while ( i-- ) {
+				if ( seed[ (j = matchIndexes[i]) ] ) {
+					seed[j] = !(matches[j] = seed[j]);
+				}
+			}
+		});
+	});
+}
+
+/**
+ * Detect xml
+ * @param {Element|Object} elem An element or a document
+ */
+isXML = Sizzle.isXML = function( elem ) {
+	// documentElement is verified for cases where it doesn't yet exist
+	// (such as loading iframes in IE - #4833)
+	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+	return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+	var doc = node ? node.ownerDocument || node : preferredDoc;
+
+	// If no document and documentElement is available, return
+	if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+		return document;
+	}
+
+	// Set our document
+	document = doc;
+	docElem = doc.documentElement;
+
+	// Support tests
+	documentIsHTML = !isXML( doc );
+
+	/* Attributes
+	---------------------------------------------------------------------- */
+
+	// Support: IE<8
+	// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
+	support.attributes = assert(function( div ) {
+
+		// Support: IE<8
+		// Prevent attribute/property "interpolation"
+		div.innerHTML = "<a href='#'></a>";
+		addHandle( "type|href|height|width", interpolationHandler, div.firstChild.getAttribute("href") === "#" );
+
+		// Support: IE<9
+		// Use getAttributeNode to fetch booleans when getAttribute lies
+		addHandle( booleans, boolHandler, div.getAttribute("disabled") == null );
+
+		div.className = "i";
+		return !div.getAttribute("className");
+	});
+
+	// Support: IE<9
+	// Retrieving value should defer to defaultValue
+	support.input = assert(function( div ) {
+		div.innerHTML = "<input>";
+		div.firstChild.setAttribute( "value", "" );
+		return div.firstChild.getAttribute( "value" ) === "";
+	});
+
+	// IE6/7 still return empty string for value,
+	// but are actually retrieving the property
+	addHandle( "value", valueHandler, support.attributes && support.input );
+
+	/* getElement(s)By*
+	---------------------------------------------------------------------- */
+
+	// Check if getElementsByTagName("*") returns only elements
+	support.getElementsByTagName = assert(function( div ) {
+		div.appendChild( doc.createComment("") );
+		return !div.getElementsByTagName("*").length;
+	});
+
+	// Check if getElementsByClassName can be trusted
+	support.getElementsByClassName = assert(function( div ) {
+		div.innerHTML = "<div class='a'></div><div class='a i'></div>";
+
+		// Support: Safari<4
+		// Catch class over-caching
+		div.firstChild.className = "i";
+		// Support: Opera<10
+		// Catch gEBCN failure to find non-leading classes
+		return div.getElementsByClassName("i").length === 2;
+	});
+
+	// Support: IE<10
+	// Check if getElementById returns elements by name
+	// The broken getElementById methods don't pick up programatically-set names,
+	// so use a roundabout getElementsByName test
+	support.getById = assert(function( div ) {
+		docElem.appendChild( div ).id = expando;
+		return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
+	});
+
+	// ID find and filter
+	if ( support.getById ) {
+		Expr.find["ID"] = function( id, context ) {
+			if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+				var m = context.getElementById( id );
+				// Check parentNode to catch when Blackberry 4.6 returns
+				// nodes that are no longer in the document #6963
+				return m && m.parentNode ? [m] : [];
+			}
+		};
+		Expr.filter["ID"] = function( id ) {
+			var attrId = id.replace( runescape, funescape );
+			return function( elem ) {
+				return elem.getAttribute("id") === attrId;
+			};
+		};
+	} else {
+		// Support: IE6/7
+		// getElementById is not reliable as a find shortcut
+		delete Expr.find["ID"];
+
+		Expr.filter["ID"] =  function( id ) {
+			var attrId = id.replace( runescape, funescape );
+			return function( elem ) {
+				var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+				return node && node.value === attrId;
+			};
+		};
+	}
+
+	// Tag
+	Expr.find["TAG"] = support.getElementsByTagName ?
+		function( tag, context ) {
+			if ( typeof context.getElementsByTagName !== strundefined ) {
+				return context.getElementsByTagName( tag );
+			}
+		} :
+		function( tag, context ) {
+			var elem,
+				tmp = [],
+				i = 0,
+				results = context.getElementsByTagName( tag );
+
+			// Filter out possible comments
+			if ( tag === "*" ) {
+				while ( (elem = results[i++]) ) {
+					if ( elem.nodeType === 1 ) {
+						tmp.push( elem );
+					}
+				}
+
+				return tmp;
+			}
+			return results;
+		};
+
+	// Class
+	Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+		if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
+			return context.getElementsByClassName( className );
+		}
+	};
+
+	/* QSA/matchesSelector
+	---------------------------------------------------------------------- */
+
+	// QSA and matchesSelector support
+
+	// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+	rbuggyMatches = [];
+
+	// qSa(:focus) reports false when true (Chrome 21)
+	// We allow this because of a bug in IE8/9 that throws an error
+	// whenever `document.activeElement` is accessed on an iframe
+	// So, we allow :focus to pass through QSA all the time to avoid the IE error
+	// See http://bugs.jquery.com/ticket/13378
+	rbuggyQSA = [];
+
+	if ( (support.qsa = isNative(doc.querySelectorAll)) ) {
+		// Build QSA regex
+		// Regex strategy adopted from Diego Perini
+		assert(function( div ) {
+			// Select is set to empty string on purpose
+			// This is to test IE's treatment of not explicitly
+			// setting a boolean content attribute,
+			// since its presence should be enough
+			// http://bugs.jquery.com/ticket/12359
+			div.innerHTML = "<select><option selected=''></option></select>";
+
+			// Support: IE8
+			// Boolean attributes and "value" are not treated correctly
+			if ( !div.querySelectorAll("[selected]").length ) {
+				rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+			}
+
+			// Webkit/Opera - :checked should return selected option elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			// IE8 throws error here and will not see later tests
+			if ( !div.querySelectorAll(":checked").length ) {
+				rbuggyQSA.push(":checked");
+			}
+		});
+
+		assert(function( div ) {
+
+			// Support: Opera 10-12/IE8
+			// ^= $= *= and empty values
+			// Should not select anything
+			// Support: Windows 8 Native Apps
+			// The type attribute is restricted during .innerHTML assignment
+			var input = doc.createElement("input");
+			input.setAttribute( "type", "hidden" );
+			div.appendChild( input ).setAttribute( "t", "" );
+
+			if ( div.querySelectorAll("[t^='']").length ) {
+				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+			}
+
+			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+			// IE8 throws error here and will not see later tests
+			if ( !div.querySelectorAll(":enabled").length ) {
+				rbuggyQSA.push( ":enabled", ":disabled" );
+			}
+
+			// Opera 10-11 does not throw on post-comma invalid pseudos
+			div.querySelectorAll("*,:x");
+			rbuggyQSA.push(",.*:");
+		});
+	}
+
+	if ( (support.matchesSelector = isNative( (matches = docElem.webkitMatchesSelector ||
+		docElem.mozMatchesSelector ||
+		docElem.oMatchesSelector ||
+		docElem.msMatchesSelector) )) ) {
+
+		assert(function( div ) {
+			// Check to see if it's possible to do matchesSelector
+			// on a disconnected node (IE 9)
+			support.disconnectedMatch = matches.call( div, "div" );
+
+			// This should fail with an exception
+			// Gecko does not error, returns false instead
+			matches.call( div, "[s!='']:x" );
+			rbuggyMatches.push( "!=", pseudos );
+		});
+	}
+
+	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+	/* Contains
+	---------------------------------------------------------------------- */
+
+	// Element contains another
+	// Purposefully does not implement inclusive descendent
+	// As in, an element does not contain itself
+	contains = isNative(docElem.contains) || docElem.compareDocumentPosition ?
+		function( a, b ) {
+			var adown = a.nodeType === 9 ? a.documentElement : a,
+				bup = b && b.parentNode;
+			return a === bup || !!( bup && bup.nodeType === 1 && (
+				adown.contains ?
+					adown.contains( bup ) :
+					a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+			));
+		} :
+		function( a, b ) {
+			if ( b ) {
+				while ( (b = b.parentNode) ) {
+					if ( b === a ) {
+						return true;
+					}
+				}
+			}
+			return false;
+		};
+
+	/* Sorting
+	---------------------------------------------------------------------- */
+
+	// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+	// Detached nodes confoundingly follow *each other*
+	support.sortDetached = assert(function( div1 ) {
+		// Should return 1, but returns 4 (following)
+		return div1.compareDocumentPosition( doc.createElement("div") ) & 1;
+	});
+
+	// Document order sorting
+	sortOrder = docElem.compareDocumentPosition ?
+	function( a, b ) {
+
+		// Flag for duplicate removal
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+
+		var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b );
+
+		if ( compare ) {
+			// Disconnected nodes
+			if ( compare & 1 ||
+				(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+				// Choose the first element that is related to our preferred document
+				if ( a === doc || contains(preferredDoc, a) ) {
+					return -1;
+				}
+				if ( b === doc || contains(preferredDoc, b) ) {
+					return 1;
+				}
+
+				// Maintain original order
+				return sortInput ?
+					( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+					0;
+			}
+
+			return compare & 4 ? -1 : 1;
+		}
+
+		// Not directly comparable, sort on existence of method
+		return a.compareDocumentPosition ? -1 : 1;
+	} :
+	function( a, b ) {
+		var cur,
+			i = 0,
+			aup = a.parentNode,
+			bup = b.parentNode,
+			ap = [ a ],
+			bp = [ b ];
+
+		// Exit early if the nodes are identical
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+
+		// Parentless nodes are either documents or disconnected
+		} else if ( !aup || !bup ) {
+			return a === doc ? -1 :
+				b === doc ? 1 :
+				aup ? -1 :
+				bup ? 1 :
+				sortInput ?
+				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+				0;
+
+		// If the nodes are siblings, we can do a quick check
+		} else if ( aup === bup ) {
+			return siblingCheck( a, b );
+		}
+
+		// Otherwise we need full lists of their ancestors for comparison
+		cur = a;
+		while ( (cur = cur.parentNode) ) {
+			ap.unshift( cur );
+		}
+		cur = b;
+		while ( (cur = cur.parentNode) ) {
+			bp.unshift( cur );
+		}
+
+		// Walk down the tree looking for a discrepancy
+		while ( ap[i] === bp[i] ) {
+			i++;
+		}
+
+		return i ?
+			// Do a sibling check if the nodes have a common ancestor
+			siblingCheck( ap[i], bp[i] ) :
+
+			// Otherwise nodes in our document sort first
+			ap[i] === preferredDoc ? -1 :
+			bp[i] === preferredDoc ? 1 :
+			0;
+	};
+
+	return doc;
+};
+
+Sizzle.matches = function( expr, elements ) {
+	return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+	// Set document vars if needed
+	if ( ( elem.ownerDocument || elem ) !== document ) {
+		setDocument( elem );
+	}
+
+	// Make sure that attribute selectors are quoted
+	expr = expr.replace( rattributeQuotes, "='$1']" );
+
+	if ( support.matchesSelector && documentIsHTML &&
+		( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+		( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
+
+		try {
+			var ret = matches.call( elem, expr );
+
+			// IE 9's matchesSelector returns false on disconnected nodes
+			if ( ret || support.disconnectedMatch ||
+					// As well, disconnected nodes are said to be in a document
+					// fragment in IE 9
+					elem.document && elem.document.nodeType !== 11 ) {
+				return ret;
+			}
+		} catch(e) {}
+	}
+
+	return Sizzle( expr, document, null, [elem] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+	// Set document vars if needed
+	if ( ( context.ownerDocument || context ) !== document ) {
+		setDocument( context );
+	}
+	return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+	// Set document vars if needed
+	if ( ( elem.ownerDocument || elem ) !== document ) {
+		setDocument( elem );
+	}
+
+	var fn = Expr.attrHandle[ name.toLowerCase() ],
+		// Don't get fooled by Object.prototype properties (jQuery #13807)
+		val = ( fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+			fn( elem, name, !documentIsHTML ) :
+			undefined );
+
+	return val === undefined ?
+		support.attributes || !documentIsHTML ?
+			elem.getAttribute( name ) :
+			(val = elem.getAttributeNode(name)) && val.specified ?
+				val.value :
+				null :
+		val;
+};
+
+Sizzle.error = function( msg ) {
+	throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+	var elem,
+		duplicates = [],
+		j = 0,
+		i = 0;
+
+	// Unless we *know* we can detect duplicates, assume their presence
+	hasDuplicate = !support.detectDuplicates;
+	sortInput = !support.sortStable && results.slice( 0 );
+	results.sort( sortOrder );
+
+	if ( hasDuplicate ) {
+		while ( (elem = results[i++]) ) {
+			if ( elem === results[ i ] ) {
+				j = duplicates.push( i );
+			}
+		}
+		while ( j-- ) {
+			results.splice( duplicates[ j ], 1 );
+		}
+	}
+
+	return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+	var node,
+		ret = "",
+		i = 0,
+		nodeType = elem.nodeType;
+
+	if ( !nodeType ) {
+		// If no nodeType, this is expected to be an array
+		for ( ; (node = elem[i]); i++ ) {
+			// Do not traverse comment nodes
+			ret += getText( node );
+		}
+	} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+		// Use textContent for elements
+		// innerText usage removed for consistency of new lines (see #11153)
+		if ( typeof elem.textContent === "string" ) {
+			return elem.textContent;
+		} else {
+			// Traverse its children
+			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+				ret += getText( elem );
+			}
+		}
+	} else if ( nodeType === 3 || nodeType === 4 ) {
+		return elem.nodeValue;
+	}
+	// Do not include comment or processing instruction nodes
+
+	return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+	// Can be adjusted by the user
+	cacheLength: 50,
+
+	createPseudo: markFunction,
+
+	match: matchExpr,
+
+	attrHandle: {},
+
+	find: {},
+
+	relative: {
+		">": { dir: "parentNode", first: true },
+		" ": { dir: "parentNode" },
+		"+": { dir: "previousSibling", first: true },
+		"~": { dir: "previousSibling" }
+	},
+
+	preFilter: {
+		"ATTR": function( match ) {
+			match[1] = match[1].replace( runescape, funescape );
+
+			// Move the given value to match[3] whether quoted or unquoted
+			match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape );
+
+			if ( match[2] === "~=" ) {
+				match[3] = " " + match[3] + " ";
+			}
+
+			return match.slice( 0, 4 );
+		},
+
+		"CHILD": function( match ) {
+			/* matches from matchExpr["CHILD"]
+				1 type (only|nth|...)
+				2 what (child|of-type)
+				3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+				4 xn-component of xn+y argument ([+-]?\d*n|)
+				5 sign of xn-component
+				6 x of xn-component
+				7 sign of y-component
+				8 y of y-component
+			*/
+			match[1] = match[1].toLowerCase();
+
+			if ( match[1].slice( 0, 3 ) === "nth" ) {
+				// nth-* requires argument
+				if ( !match[3] ) {
+					Sizzle.error( match[0] );
+				}
+
+				// numeric x and y parameters for Expr.filter.CHILD
+				// remember that false/true cast respectively to 0/1
+				match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+				match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+			// other types prohibit arguments
+			} else if ( match[3] ) {
+				Sizzle.error( match[0] );
+			}
+
+			return match;
+		},
+
+		"PSEUDO": function( match ) {
+			var excess,
+				unquoted = !match[5] && match[2];
+
+			if ( matchExpr["CHILD"].test( match[0] ) ) {
+				return null;
+			}
+
+			// Accept quoted arguments as-is
+			if ( match[3] && match[4] !== undefined ) {
+				match[2] = match[4];
+
+			// Strip excess characters from unquoted arguments
+			} else if ( unquoted && rpseudo.test( unquoted ) &&
+				// Get excess from tokenize (recursively)
+				(excess = tokenize( unquoted, true )) &&
+				// advance to the next closing parenthesis
+				(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+				// excess is a negative index
+				match[0] = match[0].slice( 0, excess );
+				match[2] = unquoted.slice( 0, excess );
+			}
+
+			// Return only captures needed by the pseudo filter method (type and argument)
+			return match.slice( 0, 3 );
+		}
+	},
+
+	filter: {
+
+		"TAG": function( nodeNameSelector ) {
+			var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+			return nodeNameSelector === "*" ?
+				function() { return true; } :
+				function( elem ) {
+					return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+				};
+		},
+
+		"CLASS": function( className ) {
+			var pattern = classCache[ className + " " ];
+
+			return pattern ||
+				(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+				classCache( className, function( elem ) {
+					return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
+				});
+		},
+
+		"ATTR": function( name, operator, check ) {
+			return function( elem ) {
+				var result = Sizzle.attr( elem, name );
+
+				if ( result == null ) {
+					return operator === "!=";
+				}
+				if ( !operator ) {
+					return true;
+				}
+
+				result += "";
+
+				return operator === "=" ? result === check :
+					operator === "!=" ? result !== check :
+					operator === "^=" ? check && result.indexOf( check ) === 0 :
+					operator === "*=" ? check && result.indexOf( check ) > -1 :
+					operator === "$=" ? check && result.slice( -check.length ) === check :
+					operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+					operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+					false;
+			};
+		},
+
+		"CHILD": function( type, what, argument, first, last ) {
+			var simple = type.slice( 0, 3 ) !== "nth",
+				forward = type.slice( -4 ) !== "last",
+				ofType = what === "of-type";
+
+			return first === 1 && last === 0 ?
+
+				// Shortcut for :nth-*(n)
+				function( elem ) {
+					return !!elem.parentNode;
+				} :
+
+				function( elem, context, xml ) {
+					var cache, outerCache, node, diff, nodeIndex, start,
+						dir = simple !== forward ? "nextSibling" : "previousSibling",
+						parent = elem.parentNode,
+						name = ofType && elem.nodeName.toLowerCase(),
+						useCache = !xml && !ofType;
+
+					if ( parent ) {
+
+						// :(first|last|only)-(child|of-type)
+						if ( simple ) {
+							while ( dir ) {
+								node = elem;
+								while ( (node = node[ dir ]) ) {
+									if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
+										return false;
+									}
+								}
+								// Reverse direction for :only-* (if we haven't yet done so)
+								start = dir = type === "only" && !start && "nextSibling";
+							}
+							return true;
+						}
+
+						start = [ forward ? parent.firstChild : parent.lastChild ];
+
+						// non-xml :nth-child(...) stores cache data on `parent`
+						if ( forward && useCache ) {
+							// Seek `elem` from a previously-cached index
+							outerCache = parent[ expando ] || (parent[ expando ] = {});
+							cache = outerCache[ type ] || [];
+							nodeIndex = cache[0] === dirruns && cache[1];
+							diff = cache[0] === dirruns && cache[2];
+							node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+							while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+								// Fallback to seeking `elem` from the start
+								(diff = nodeIndex = 0) || start.pop()) ) {
+
+								// When found, cache indexes on `parent` and break
+								if ( node.nodeType === 1 && ++diff && node === elem ) {
+									outerCache[ type ] = [ dirruns, nodeIndex, diff ];
+									break;
+								}
+							}
+
+						// Use previously-cached element index if available
+						} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
+							diff = cache[1];
+
+						// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
+						} else {
+							// Use the same loop as above to seek `elem` from the start
+							while ( (node = ++nodeIndex && node && node[ dir ] ||
+								(diff = nodeIndex = 0) || start.pop()) ) {
+
+								if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
+									// Cache the index of each encountered element
+									if ( useCache ) {
+										(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
+									}
+
+									if ( node === elem ) {
+										break;
+									}
+								}
+							}
+						}
+
+						// Incorporate the offset, then check against cycle size
+						diff -= last;
+						return diff === first || ( diff % first === 0 && diff / first >= 0 );
+					}
+				};
+		},
+
+		"PSEUDO": function( pseudo, argument ) {
+			// pseudo-class names are case-insensitive
+			// http://www.w3.org/TR/selectors/#pseudo-classes
+			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+			// Remember that setFilters inherits from pseudos
+			var args,
+				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+					Sizzle.error( "unsupported pseudo: " + pseudo );
+
+			// The user may use createPseudo to indicate that
+			// arguments are needed to create the filter function
+			// just as Sizzle does
+			if ( fn[ expando ] ) {
+				return fn( argument );
+			}
+
+			// But maintain support for old signatures
+			if ( fn.length > 1 ) {
+				args = [ pseudo, pseudo, "", argument ];
+				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+					markFunction(function( seed, matches ) {
+						var idx,
+							matched = fn( seed, argument ),
+							i = matched.length;
+						while ( i-- ) {
+							idx = indexOf.call( seed, matched[i] );
+							seed[ idx ] = !( matches[ idx ] = matched[i] );
+						}
+					}) :
+					function( elem ) {
+						return fn( elem, 0, args );
+					};
+			}
+
+			return fn;
+		}
+	},
+
+	pseudos: {
+		// Potentially complex pseudos
+		"not": markFunction(function( selector ) {
+			// Trim the selector passed to compile
+			// to avoid treating leading and trailing
+			// spaces as combinators
+			var input = [],
+				results = [],
+				matcher = compile( selector.replace( rtrim, "$1" ) );
+
+			return matcher[ expando ] ?
+				markFunction(function( seed, matches, context, xml ) {
+					var elem,
+						unmatched = matcher( seed, null, xml, [] ),
+						i = seed.length;
+
+					// Match elements unmatched by `matcher`
+					while ( i-- ) {
+						if ( (elem = unmatched[i]) ) {
+							seed[i] = !(matches[i] = elem);
+						}
+					}
+				}) :
+				function( elem, context, xml ) {
+					input[0] = elem;
+					matcher( input, null, xml, results );
+					return !results.pop();
+				};
+		}),
+
+		"has": markFunction(function( selector ) {
+			return function( elem ) {
+				return Sizzle( selector, elem ).length > 0;
+			};
+		}),
+
+		"contains": markFunction(function( text ) {
+			return function( elem ) {
+				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+			};
+		}),
+
+		// "Whether an element is represented by a :lang() selector
+		// is based solely on the element's language value
+		// being equal to the identifier C,
+		// or beginning with the identifier C immediately followed by "-".
+		// The matching of C against the element's language value is performed case-insensitively.
+		// The identifier C does not have to be a valid language name."
+		// http://www.w3.org/TR/selectors/#lang-pseudo
+		"lang": markFunction( function( lang ) {
+			// lang value must be a valid identifier
+			if ( !ridentifier.test(lang || "") ) {
+				Sizzle.error( "unsupported lang: " + lang );
+			}
+			lang = lang.replace( runescape, funescape ).toLowerCase();
+			return function( elem ) {
+				var elemLang;
+				do {
+					if ( (elemLang = documentIsHTML ?
+						elem.lang :
+						elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+						elemLang = elemLang.toLowerCase();
+						return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+					}
+				} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+				return false;
+			};
+		}),
+
+		// Miscellaneous
+		"target": function( elem ) {
+			var hash = window.location && window.location.hash;
+			return hash && hash.slice( 1 ) === elem.id;
+		},
+
+		"root": function( elem ) {
+			return elem === docElem;
+		},
+
+		"focus": function( elem ) {
+			return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+		},
+
+		// Boolean properties
+		"enabled": function( elem ) {
+			return elem.disabled === false;
+		},
+
+		"disabled": function( elem ) {
+			return elem.disabled === true;
+		},
+
+		"checked": function( elem ) {
+			// In CSS3, :checked should return both checked and selected elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			var nodeName = elem.nodeName.toLowerCase();
+			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+		},
+
+		"selected": function( elem ) {
+			// Accessing this property makes selected-by-default
+			// options in Safari work properly
+			if ( elem.parentNode ) {
+				elem.parentNode.selectedIndex;
+			}
+
+			return elem.selected === true;
+		},
+
+		// Contents
+		"empty": function( elem ) {
+			// http://www.w3.org/TR/selectors/#empty-pseudo
+			// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+			//   not comment, processing instructions, or others
+			// Thanks to Diego Perini for the nodeName shortcut
+			//   Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+				if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) {
+					return false;
+				}
+			}
+			return true;
+		},
+
+		"parent": function( elem ) {
+			return !Expr.pseudos["empty"]( elem );
+		},
+
+		// Element/input types
+		"header": function( elem ) {
+			return rheader.test( elem.nodeName );
+		},
+
+		"input": function( elem ) {
+			return rinputs.test( elem.nodeName );
+		},
+
+		"button": function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return name === "input" && elem.type === "button" || name === "button";
+		},
+
+		"text": function( elem ) {
+			var attr;
+			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+			// use getAttribute instead to test this case
+			return elem.nodeName.toLowerCase() === "input" &&
+				elem.type === "text" &&
+				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type );
+		},
+
+		// Position-in-collection
+		"first": createPositionalPseudo(function() {
+			return [ 0 ];
+		}),
+
+		"last": createPositionalPseudo(function( matchIndexes, length ) {
+			return [ length - 1 ];
+		}),
+
+		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ argument < 0 ? argument + length : argument ];
+		}),
+
+		"even": createPositionalPseudo(function( matchIndexes, length ) {
+			var i = 0;
+			for ( ; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"odd": createPositionalPseudo(function( matchIndexes, length ) {
+			var i = 1;
+			for ( ; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			var i = argument < 0 ? argument + length : argument;
+			for ( ; --i >= 0; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			var i = argument < 0 ? argument + length : argument;
+			for ( ; ++i < length; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		})
+	}
+};
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+	Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+	Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+function tokenize( selector, parseOnly ) {
+	var matched, match, tokens, type,
+		soFar, groups, preFilters,
+		cached = tokenCache[ selector + " " ];
+
+	if ( cached ) {
+		return parseOnly ? 0 : cached.slice( 0 );
+	}
+
+	soFar = selector;
+	groups = [];
+	preFilters = Expr.preFilter;
+
+	while ( soFar ) {
+
+		// Comma and first run
+		if ( !matched || (match = rcomma.exec( soFar )) ) {
+			if ( match ) {
+				// Don't consume trailing commas as valid
+				soFar = soFar.slice( match[0].length ) || soFar;
+			}
+			groups.push( tokens = [] );
+		}
+
+		matched = false;
+
+		// Combinators
+		if ( (match = rcombinators.exec( soFar )) ) {
+			matched = match.shift();
+			tokens.push({
+				value: matched,
+				// Cast descendant combinators to space
+				type: match[0].replace( rtrim, " " )
+			});
+			soFar = soFar.slice( matched.length );
+		}
+
+		// Filters
+		for ( type in Expr.filter ) {
+			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+				(match = preFilters[ type ]( match ))) ) {
+				matched = match.shift();
+				tokens.push({
+					value: matched,
+					type: type,
+					matches: match
+				});
+				soFar = soFar.slice( matched.length );
+			}
+		}
+
+		if ( !matched ) {
+			break;
+		}
+	}
+
+	// Return the length of the invalid excess
+	// if we're just parsing
+	// Otherwise, throw an error or return tokens
+	return parseOnly ?
+		soFar.length :
+		soFar ?
+			Sizzle.error( selector ) :
+			// Cache the tokens
+			tokenCache( selector, groups ).slice( 0 );
+}
+
+function toSelector( tokens ) {
+	var i = 0,
+		len = tokens.length,
+		selector = "";
+	for ( ; i < len; i++ ) {
+		selector += tokens[i].value;
+	}
+	return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+	var dir = combinator.dir,
+		checkNonElements = base && dir === "parentNode",
+		doneName = done++;
+
+	return combinator.first ?
+		// Check against closest ancestor/preceding element
+		function( elem, context, xml ) {
+			while ( (elem = elem[ dir ]) ) {
+				if ( elem.nodeType === 1 || checkNonElements ) {
+					return matcher( elem, context, xml );
+				}
+			}
+		} :
+
+		// Check against all ancestor/preceding elements
+		function( elem, context, xml ) {
+			var data, cache, outerCache,
+				dirkey = dirruns + " " + doneName;
+
+			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+			if ( xml ) {
+				while ( (elem = elem[ dir ]) ) {
+					if ( elem.nodeType === 1 || checkNonElements ) {
+						if ( matcher( elem, context, xml ) ) {
+							return true;
+						}
+					}
+				}
+			} else {
+				while ( (elem = elem[ dir ]) ) {
+					if ( elem.nodeType === 1 || checkNonElements ) {
+						outerCache = elem[ expando ] || (elem[ expando ] = {});
+						if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {
+							if ( (data = cache[1]) === true || data === cachedruns ) {
+								return data === true;
+							}
+						} else {
+							cache = outerCache[ dir ] = [ dirkey ];
+							cache[1] = matcher( elem, context, xml ) || cachedruns;
+							if ( cache[1] === true ) {
+								return true;
+							}
+						}
+					}
+				}
+			}
+		};
+}
+
+function elementMatcher( matchers ) {
+	return matchers.length > 1 ?
+		function( elem, context, xml ) {
+			var i = matchers.length;
+			while ( i-- ) {
+				if ( !matchers[i]( elem, context, xml ) ) {
+					return false;
+				}
+			}
+			return true;
+		} :
+		matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+	var elem,
+		newUnmatched = [],
+		i = 0,
+		len = unmatched.length,
+		mapped = map != null;
+
+	for ( ; i < len; i++ ) {
+		if ( (elem = unmatched[i]) ) {
+			if ( !filter || filter( elem, context, xml ) ) {
+				newUnmatched.push( elem );
+				if ( mapped ) {
+					map.push( i );
+				}
+			}
+		}
+	}
+
+	return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+	if ( postFilter && !postFilter[ expando ] ) {
+		postFilter = setMatcher( postFilter );
+	}
+	if ( postFinder && !postFinder[ expando ] ) {
+		postFinder = setMatcher( postFinder, postSelector );
+	}
+	return markFunction(function( seed, results, context, xml ) {
+		var temp, i, elem,
+			preMap = [],
+			postMap = [],
+			preexisting = results.length,
+
+			// Get initial elements from seed or context
+			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+			// Prefilter to get matcher input, preserving a map for seed-results synchronization
+			matcherIn = preFilter && ( seed || !selector ) ?
+				condense( elems, preMap, preFilter, context, xml ) :
+				elems,
+
+			matcherOut = matcher ?
+				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+					// ...intermediate processing is necessary
+					[] :
+
+					// ...otherwise use results directly
+					results :
+				matcherIn;
+
+		// Find primary matches
+		if ( matcher ) {
+			matcher( matcherIn, matcherOut, context, xml );
+		}
+
+		// Apply postFilter
+		if ( postFilter ) {
+			temp = condense( matcherOut, postMap );
+			postFilter( temp, [], context, xml );
+
+			// Un-match failing elements by moving them back to matcherIn
+			i = temp.length;
+			while ( i-- ) {
+				if ( (elem = temp[i]) ) {
+					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+				}
+			}
+		}
+
+		if ( seed ) {
+			if ( postFinder || preFilter ) {
+				if ( postFinder ) {
+					// Get the final matcherOut by condensing this intermediate into postFinder contexts
+					temp = [];
+					i = matcherOut.length;
+					while ( i-- ) {
+						if ( (elem = matcherOut[i]) ) {
+							// Restore matcherIn since elem is not yet a final match
+							temp.push( (matcherIn[i] = elem) );
+						}
+					}
+					postFinder( null, (matcherOut = []), temp, xml );
+				}
+
+				// Move matched elements from seed to results to keep them synchronized
+				i = matcherOut.length;
+				while ( i-- ) {
+					if ( (elem = matcherOut[i]) &&
+						(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+						seed[temp] = !(results[temp] = elem);
+					}
+				}
+			}
+
+		// Add elements to results, through postFinder if defined
+		} else {
+			matcherOut = condense(
+				matcherOut === results ?
+					matcherOut.splice( preexisting, matcherOut.length ) :
+					matcherOut
+			);
+			if ( postFinder ) {
+				postFinder( null, results, matcherOut, xml );
+			} else {
+				push.apply( results, matcherOut );
+			}
+		}
+	});
+}
+
+function matcherFromTokens( tokens ) {
+	var checkContext, matcher, j,
+		len = tokens.length,
+		leadingRelative = Expr.relative[ tokens[0].type ],
+		implicitRelative = leadingRelative || Expr.relative[" "],
+		i = leadingRelative ? 1 : 0,
+
+		// The foundational matcher ensures that elements are reachable from top-level context(s)
+		matchContext = addCombinator( function( elem ) {
+			return elem === checkContext;
+		}, implicitRelative, true ),
+		matchAnyContext = addCombinator( function( elem ) {
+			return indexOf.call( checkContext, elem ) > -1;
+		}, implicitRelative, true ),
+		matchers = [ function( elem, context, xml ) {
+			return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+				(checkContext = context).nodeType ?
+					matchContext( elem, context, xml ) :
+					matchAnyContext( elem, context, xml ) );
+		} ];
+
+	for ( ; i < len; i++ ) {
+		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+			matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+		} else {
+			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+			// Return special upon seeing a positional matcher
+			if ( matcher[ expando ] ) {
+				// Find the next relative operator (if any) for proper handling
+				j = ++i;
+				for ( ; j < len; j++ ) {
+					if ( Expr.relative[ tokens[j].type ] ) {
+						break;
+					}
+				}
+				return setMatcher(
+					i > 1 && elementMatcher( matchers ),
+					i > 1 && toSelector(
+						// If the preceding token was a descendant combinator, insert an implicit any-element `*`
+						tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+					).replace( rtrim, "$1" ),
+					matcher,
+					i < j && matcherFromTokens( tokens.slice( i, j ) ),
+					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+					j < len && toSelector( tokens )
+				);
+			}
+			matchers.push( matcher );
+		}
+	}
+
+	return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+	// A counter to specify which element is currently being matched
+	var matcherCachedRuns = 0,
+		bySet = setMatchers.length > 0,
+		byElement = elementMatchers.length > 0,
+		superMatcher = function( seed, context, xml, results, expandContext ) {
+			var elem, j, matcher,
+				setMatched = [],
+				matchedCount = 0,
+				i = "0",
+				unmatched = seed && [],
+				outermost = expandContext != null,
+				contextBackup = outermostContext,
+				// We must always have either seed elements or context
+				elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+				// Use integer dirruns iff this is the outermost matcher
+				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);
+
+			if ( outermost ) {
+				outermostContext = context !== document && context;
+				cachedruns = matcherCachedRuns;
+			}
+
+			// Add elements passing elementMatchers directly to results
+			// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
+			for ( ; (elem = elems[i]) != null; i++ ) {
+				if ( byElement && elem ) {
+					j = 0;
+					while ( (matcher = elementMatchers[j++]) ) {
+						if ( matcher( elem, context, xml ) ) {
+							results.push( elem );
+							break;
+						}
+					}
+					if ( outermost ) {
+						dirruns = dirrunsUnique;
+						cachedruns = ++matcherCachedRuns;
+					}
+				}
+
+				// Track unmatched elements for set filters
+				if ( bySet ) {
+					// They will have gone through all possible matchers
+					if ( (elem = !matcher && elem) ) {
+						matchedCount--;
+					}
+
+					// Lengthen the array for every element, matched or not
+					if ( seed ) {
+						unmatched.push( elem );
+					}
+				}
+			}
+
+			// Apply set filters to unmatched elements
+			matchedCount += i;
+			if ( bySet && i !== matchedCount ) {
+				j = 0;
+				while ( (matcher = setMatchers[j++]) ) {
+					matcher( unmatched, setMatched, context, xml );
+				}
+
+				if ( seed ) {
+					// Reintegrate element matches to eliminate the need for sorting
+					if ( matchedCount > 0 ) {
+						while ( i-- ) {
+							if ( !(unmatched[i] || setMatched[i]) ) {
+								setMatched[i] = pop.call( results );
+							}
+						}
+					}
+
+					// Discard index placeholder values to get only actual matches
+					setMatched = condense( setMatched );
+				}
+
+				// Add matches to results
+				push.apply( results, setMatched );
+
+				// Seedless set matches succeeding multiple successful matchers stipulate sorting
+				if ( outermost && !seed && setMatched.length > 0 &&
+					( matchedCount + setMatchers.length ) > 1 ) {
+
+					Sizzle.uniqueSort( results );
+				}
+			}
+
+			// Override manipulation of globals by nested matchers
+			if ( outermost ) {
+				dirruns = dirrunsUnique;
+				outermostContext = contextBackup;
+			}
+
+			return unmatched;
+		};
+
+	return bySet ?
+		markFunction( superMatcher ) :
+		superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+	var i,
+		setMatchers = [],
+		elementMatchers = [],
+		cached = compilerCache[ selector + " " ];
+
+	if ( !cached ) {
+		// Generate a function of recursive functions that can be used to check each element
+		if ( !group ) {
+			group = tokenize( selector );
+		}
+		i = group.length;
+		while ( i-- ) {
+			cached = matcherFromTokens( group[i] );
+			if ( cached[ expando ] ) {
+				setMatchers.push( cached );
+			} else {
+				elementMatchers.push( cached );
+			}
+		}
+
+		// Cache the compiled function
+		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+	}
+	return cached;
+};
+
+function multipleContexts( selector, contexts, results ) {
+	var i = 0,
+		len = contexts.length;
+	for ( ; i < len; i++ ) {
+		Sizzle( selector, contexts[i], results );
+	}
+	return results;
+}
+
+function select( selector, context, results, seed ) {
+	var i, tokens, token, type, find,
+		match = tokenize( selector );
+
+	if ( !seed ) {
+		// Try to minimize operations if there is only one group
+		if ( match.length === 1 ) {
+
+			// Take a shortcut and set the context if the root selector is an ID
+			tokens = match[0] = match[0].slice( 0 );
+			if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+					support.getById && context.nodeType === 9 && documentIsHTML &&
+					Expr.relative[ tokens[1].type ] ) {
+
+				context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+				if ( !context ) {
+					return results;
+				}
+				selector = selector.slice( tokens.shift().value.length );
+			}
+
+			// Fetch a seed set for right-to-left matching
+			i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+			while ( i-- ) {
+				token = tokens[i];
+
+				// Abort if we hit a combinator
+				if ( Expr.relative[ (type = token.type) ] ) {
+					break;
+				}
+				if ( (find = Expr.find[ type ]) ) {
+					// Search, expanding context for leading sibling combinators
+					if ( (seed = find(
+						token.matches[0].replace( runescape, funescape ),
+						rsibling.test( tokens[0].type ) && context.parentNode || context
+					)) ) {
+
+						// If seed is empty or no tokens remain, we can return early
+						tokens.splice( i, 1 );
+						selector = seed.length && toSelector( tokens );
+						if ( !selector ) {
+							push.apply( results, seed );
+							return results;
+						}
+
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// Compile and execute a filtering function
+	// Provide `match` to avoid retokenization if we modified the selector above
+	compile( selector, match )(
+		seed,
+		context,
+		!documentIsHTML,
+		results,
+		rsibling.test( selector )
+	);
+	return results;
+}
+
+// Deprecated
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Chrome<<14
+// Always assume duplicates if they aren't passed to the comparison function
+[0, 0].sort( sortOrder );
+support.detectDuplicates = hasDuplicate;
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})( window );
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+	var object = optionsCache[ options ] = {};
+	jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {
+		object[ flag ] = true;
+	});
+	return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *	options: an optional list of space-separated options that will change how
+ *			the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *	once:			will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *	memory:			will keep track of previous values and will call any callback added
+ *					after the list has been fired right away with the latest "memorized"
+ *					values (like a Deferred)
+ *
+ *	unique:			will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *	stopOnFalse:	interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+	// Convert options from String-formatted to Object-formatted if needed
+	// (we check in cache first)
+	options = typeof options === "string" ?
+		( optionsCache[ options ] || createOptions( options ) ) :
+		jQuery.extend( {}, options );
+
+	var // Flag to know if list is currently firing
+		firing,
+		// Last fire value (for non-forgettable lists)
+		memory,
+		// Flag to know if list was already fired
+		fired,
+		// End of the loop when firing
+		firingLength,
+		// Index of currently firing callback (modified by remove if needed)
+		firingIndex,
+		// First callback to fire (used internally by add and fireWith)
+		firingStart,
+		// Actual callback list
+		list = [],
+		// Stack of fire calls for repeatable lists
+		stack = !options.once && [],
+		// Fire callbacks
+		fire = function( data ) {
+			memory = options.memory && data;
+			fired = true;
+			firingIndex = firingStart || 0;
+			firingStart = 0;
+			firingLength = list.length;
+			firing = true;
+			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+					memory = false; // To prevent further calls using add
+					break;
+				}
+			}
+			firing = false;
+			if ( list ) {
+				if ( stack ) {
+					if ( stack.length ) {
+						fire( stack.shift() );
+					}
+				} else if ( memory ) {
+					list = [];
+				} else {
+					self.disable();
+				}
+			}
+		},
+		// Actual Callbacks object
+		self = {
+			// Add a callback or a collection of callbacks to the list
+			add: function() {
+				if ( list ) {
+					// First, we save the current length
+					var start = list.length;
+					(function add( args ) {
+						jQuery.each( args, function( _, arg ) {
+							var type = jQuery.type( arg );
+							if ( type === "function" ) {
+								if ( !options.unique || !self.has( arg ) ) {
+									list.push( arg );
+								}
+							} else if ( arg && arg.length && type !== "string" ) {
+								// Inspect recursively
+								add( arg );
+							}
+						});
+					})( arguments );
+					// Do we need to add the callbacks to the
+					// current firing batch?
+					if ( firing ) {
+						firingLength = list.length;
+					// With memory, if we're not firing then
+					// we should call right away
+					} else if ( memory ) {
+						firingStart = start;
+						fire( memory );
+					}
+				}
+				return this;
+			},
+			// Remove a callback from the list
+			remove: function() {
+				if ( list ) {
+					jQuery.each( arguments, function( _, arg ) {
+						var index;
+						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+							list.splice( index, 1 );
+							// Handle firing indexes
+							if ( firing ) {
+								if ( index <= firingLength ) {
+									firingLength--;
+								}
+								if ( index <= firingIndex ) {
+									firingIndex--;
+								}
+							}
+						}
+					});
+				}
+				return this;
+			},
+			// Check if a given callback is in the list.
+			// If no argument is given, return whether or not list has callbacks attached.
+			has: function( fn ) {
+				return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
+			},
+			// Remove all callbacks from the list
+			empty: function() {
+				list = [];
+				firingLength = 0;
+				return this;
+			},
+			// Have the list do nothing anymore
+			disable: function() {
+				list = stack = memory = undefined;
+				return this;
+			},
+			// Is it disabled?
+			disabled: function() {
+				return !list;
+			},
+			// Lock the list in its current state
+			lock: function() {
+				stack = undefined;
+				if ( !memory ) {
+					self.disable();
+				}
+				return this;
+			},
+			// Is it locked?
+			locked: function() {
+				return !stack;
+			},
+			// Call all callbacks with the given context and arguments
+			fireWith: function( context, args ) {
+				args = args || [];
+				args = [ context, args.slice ? args.slice() : args ];
+				if ( list && ( !fired || stack ) ) {
+					if ( firing ) {
+						stack.push( args );
+					} else {
+						fire( args );
+					}
+				}
+				return this;
+			},
+			// Call all the callbacks with the given arguments
+			fire: function() {
+				self.fireWith( this, arguments );
+				return this;
+			},
+			// To know if the callbacks have already been called at least once
+			fired: function() {
+				return !!fired;
+			}
+		};
+
+	return self;
+};
+jQuery.extend({
+
+	Deferred: function( func ) {
+		var tuples = [
+				// action, add listener, listener list, final state
+				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+				[ "notify", "progress", jQuery.Callbacks("memory") ]
+			],
+			state = "pending",
+			promise = {
+				state: function() {
+					return state;
+				},
+				always: function() {
+					deferred.done( arguments ).fail( arguments );
+					return this;
+				},
+				then: function( /* fnDone, fnFail, fnProgress */ ) {
+					var fns = arguments;
+					return jQuery.Deferred(function( newDefer ) {
+						jQuery.each( tuples, function( i, tuple ) {
+							var action = tuple[ 0 ],
+								fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
+							// deferred[ done | fail | progress ] for forwarding actions to newDefer
+							deferred[ tuple[1] ](function() {
+								var returned = fn && fn.apply( this, arguments );
+								if ( returned && jQuery.isFunction( returned.promise ) ) {
+									returned.promise()
+										.done( newDefer.resolve )
+										.fail( newDefer.reject )
+										.progress( newDefer.notify );
+								} else {
+									newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
+								}
+							});
+						});
+						fns = null;
+					}).promise();
+				},
+				// Get a promise for this deferred
+				// If obj is provided, the promise aspect is added to the object
+				promise: function( obj ) {
+					return obj != null ? jQuery.extend( obj, promise ) : promise;
+				}
+			},
+			deferred = {};
+
+		// Keep pipe for back-compat
+		promise.pipe = promise.then;
+
+		// Add list-specific methods
+		jQuery.each( tuples, function( i, tuple ) {
+			var list = tuple[ 2 ],
+				stateString = tuple[ 3 ];
+
+			// promise[ done | fail | progress ] = list.add
+			promise[ tuple[1] ] = list.add;
+
+			// Handle state
+			if ( stateString ) {
+				list.add(function() {
+					// state = [ resolved | rejected ]
+					state = stateString;
+
+				// [ reject_list | resolve_list ].disable; progress_list.lock
+				}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+			}
+
+			// deferred[ resolve | reject | notify ]
+			deferred[ tuple[0] ] = function() {
+				deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
+				return this;
+			};
+			deferred[ tuple[0] + "With" ] = list.fireWith;
+		});
+
+		// Make the deferred a promise
+		promise.promise( deferred );
+
+		// Call given func if any
+		if ( func ) {
+			func.call( deferred, deferred );
+		}
+
+		// All done!
+		return deferred;
+	},
+
+	// Deferred helper
+	when: function( subordinate /* , ..., subordinateN */ ) {
+		var i = 0,
+			resolveValues = core_slice.call( arguments ),
+			length = resolveValues.length,
+
+			// the count of uncompleted subordinates
+			remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+			// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+			deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+			// Update function for both resolve and progress values
+			updateFunc = function( i, contexts, values ) {
+				return function( value ) {
+					contexts[ i ] = this;
+					values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+					if( values === progressValues ) {
+						deferred.notifyWith( contexts, values );
+					} else if ( !( --remaining ) ) {
+						deferred.resolveWith( contexts, values );
+					}
+				};
+			},
+
+			progressValues, progressContexts, resolveContexts;
+
+		// add listeners to Deferred subordinates; treat others as resolved
+		if ( length > 1 ) {
+			progressValues = new Array( length );
+			progressContexts = new Array( length );
+			resolveContexts = new Array( length );
+			for ( ; i < length; i++ ) {
+				if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+					resolveValues[ i ].promise()
+						.done( updateFunc( i, resolveContexts, resolveValues ) )
+						.fail( deferred.reject )
+						.progress( updateFunc( i, progressContexts, progressValues ) );
+				} else {
+					--remaining;
+				}
+			}
+		}
+
+		// if we're not waiting on anything, resolve the master
+		if ( !remaining ) {
+			deferred.resolveWith( resolveContexts, resolveValues );
+		}
+
+		return deferred.promise();
+	}
+});
+jQuery.support = (function( support ) {
+
+	var all, a, input, select, fragment, opt, eventName, isSupported, i,
+		div = document.createElement("div");
+
+	// Setup
+	div.setAttribute( "className", "t" );
+	div.innerHTML = "  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+	// Finish early in limited (non-browser) environments
+	all = div.getElementsByTagName("*") || [];
+	a = div.getElementsByTagName("a")[ 0 ];
+	if ( !a || !a.style || !all.length ) {
+		return support;
+	}
+
+	// First batch of tests
+	select = document.createElement("select");
+	opt = select.appendChild( document.createElement("option") );
+	input = div.getElementsByTagName("input")[ 0 ];
+
+	a.style.cssText = "top:1px;float:left;opacity:.5";
+
+	// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+	support.getSetAttribute = div.className !== "t";
+
+	// IE strips leading whitespace when .innerHTML is used
+	support.leadingWhitespace = div.firstChild.nodeType === 3;
+
+	// Make sure that tbody elements aren't automatically inserted
+	// IE will insert them into empty tables
+	support.tbody = !div.getElementsByTagName("tbody").length;
+
+	// Make sure that link elements get serialized correctly by innerHTML
+	// This requires a wrapper element in IE
+	support.htmlSerialize = !!div.getElementsByTagName("link").length;
+
+	// Get the style information from getAttribute
+	// (IE uses .cssText instead)
+	support.style = /top/.test( a.getAttribute("style") );
+
+	// Make sure that URLs aren't manipulated
+	// (IE normalizes it by default)
+	support.hrefNormalized = a.getAttribute("href") === "/a";
+
+	// Make sure that element opacity exists
+	// (IE uses filter instead)
+	// Use a regex to work around a WebKit issue. See #5145
+	support.opacity = /^0.5/.test( a.style.opacity );
+
+	// Verify style float existence
+	// (IE uses styleFloat instead of cssFloat)
+	support.cssFloat = !!a.style.cssFloat;
+
+	// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
+	support.checkOn = !!input.value;
+
+	// Make sure that a selected-by-default option has a working selected property.
+	// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+	support.optSelected = opt.selected;
+
+	// Tests for enctype support on a form (#6743)
+	support.enctype = !!document.createElement("form").enctype;
+
+	// Makes sure cloning an html5 element does not cause problems
+	// Where outerHTML is undefined, this still works
+	support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>";
+
+	// Will be defined later
+	support.inlineBlockNeedsLayout = false;
+	support.shrinkWrapBlocks = false;
+	support.pixelPosition = false;
+	support.deleteExpando = true;
+	support.noCloneEvent = true;
+	support.reliableMarginRight = true;
+	support.boxSizingReliable = true;
+
+	// Make sure checked status is properly cloned
+	input.checked = true;
+	support.noCloneChecked = input.cloneNode( true ).checked;
+
+	// Make sure that the options inside disabled selects aren't marked as disabled
+	// (WebKit marks them as disabled)
+	select.disabled = true;
+	support.optDisabled = !opt.disabled;
+
+	// Support: IE<9
+	try {
+		delete div.test;
+	} catch( e ) {
+		support.deleteExpando = false;
+	}
+
+	// Check if we can trust getAttribute("value")
+	input = document.createElement("input");
+	input.setAttribute( "value", "" );
+	support.input = input.getAttribute( "value" ) === "";
+
+	// Check if an input maintains its value after becoming a radio
+	input.value = "t";
+	input.setAttribute( "type", "radio" );
+	support.radioValue = input.value === "t";
+
+	// #11217 - WebKit loses check when the name is after the checked attribute
+	input.setAttribute( "checked", "t" );
+	input.setAttribute( "name", "t" );
+
+	fragment = document.createDocumentFragment();
+	fragment.appendChild( input );
+
+	// Check if a disconnected checkbox will retain its checked
+	// value of true after appended to the DOM (IE6/7)
+	support.appendChecked = input.checked;
+
+	// WebKit doesn't clone checked state correctly in fragments
+	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+	// Support: IE<9
+	// Opera does not clone events (and typeof div.attachEvent === undefined).
+	// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
+	if ( div.attachEvent ) {
+		div.attachEvent( "onclick", function() {
+			support.noCloneEvent = false;
+		});
+
+		div.cloneNode( true ).click();
+	}
+
+	// Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
+	// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)
+	for ( i in { submit: true, change: true, focusin: true }) {
+		div.setAttribute( eventName = "on" + i, "t" );
+
+		support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
+	}
+
+	div.style.backgroundClip = "content-box";
+	div.cloneNode( true ).style.backgroundClip = "";
+	support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+	// Support: IE<9
+	// Iteration over object's inherited properties before its own.
+	for ( i in jQuery( support ) ) {
+		break;
+	}
+	support.ownLast = i !== "0";
+
+	// Run tests that need a body at doc ready
+	jQuery(function() {
+		var container, marginDiv, tds,
+			divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
+			body = document.getElementsByTagName("body")[0];
+
+		if ( !body ) {
+			// Return for frameset docs that don't have a body
+			return;
+		}
+
+		container = document.createElement("div");
+		container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";
+
+		body.appendChild( container ).appendChild( div );
+
+		// Support: IE8
+		// Check if table cells still have offsetWidth/Height when they are set
+		// to display:none and there are still other visible table cells in a
+		// table row; if so, offsetWidth/Height are not reliable for use when
+		// determining if an element has been hidden directly using
+		// display:none (it is still safe to use offsets if a parent element is
+		// hidden; don safety goggles and see bug #4512 for more information).
+		div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+		tds = div.getElementsByTagName("td");
+		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+		isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+		tds[ 0 ].style.display = "";
+		tds[ 1 ].style.display = "none";
+
+		// Support: IE8
+		// Check if empty table cells still have offsetWidth/Height
+		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+		// Check box-sizing and margin behavior.
+		div.innerHTML = "";
+		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+
+		// Workaround failing boxSizing test due to offsetWidth returning wrong value
+		// with some non-1 values of body zoom, ticket #13543
+		jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() {
+			support.boxSizing = div.offsetWidth === 4;
+		});
+
+		// Use window.getComputedStyle because jsdom on node.js will break without it.
+		if ( window.getComputedStyle ) {
+			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+			// Check if div with explicit width and no margin-right incorrectly
+			// gets computed margin-right based on width of container. (#3333)
+			// Fails in WebKit before Feb 2011 nightlies
+			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+			marginDiv = div.appendChild( document.createElement("div") );
+			marginDiv.style.cssText = div.style.cssText = divReset;
+			marginDiv.style.marginRight = marginDiv.style.width = "0";
+			div.style.width = "1px";
+
+			support.reliableMarginRight =
+				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+		}
+
+		if ( typeof div.style.zoom !== core_strundefined ) {
+			// Support: IE<8
+			// Check if natively block-level elements act like inline-block
+			// elements when setting their display to 'inline' and giving
+			// them layout
+			div.innerHTML = "";
+			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+			// Support: IE6
+			// Check if elements with layout shrink-wrap their children
+			div.style.display = "block";
+			div.innerHTML = "<div></div>";
+			div.firstChild.style.width = "5px";
+			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+			if ( support.inlineBlockNeedsLayout ) {
+				// Prevent IE 6 from affecting layout for positioned elements #11048
+				// Prevent IE from shrinking the body in IE 7 mode #12869
+				// Support: IE<8
+				body.style.zoom = 1;
+			}
+		}
+
+		body.removeChild( container );
+
+		// Null elements to avoid leaks in IE
+		container = div = tds = marginDiv = null;
+	});
+
+	// Null elements to avoid leaks in IE
+	all = select = fragment = opt = a = input = null;
+
+	return support;
+})({});
+
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+	rmultiDash = /([A-Z])/g;
+
+function internalData( elem, name, data, pvt /* Internal Use Only */ ){
+	if ( !jQuery.acceptData( elem ) ) {
+		return;
+	}
+
+	var ret, thisCache,
+		internalKey = jQuery.expando,
+
+		// We have to handle DOM nodes and JS objects differently because IE6-7
+		// can't GC object references properly across the DOM-JS boundary
+		isNode = elem.nodeType,
+
+		// Only DOM nodes need the global jQuery cache; JS object data is
+		// attached directly to the object so GC can occur automatically
+		cache = isNode ? jQuery.cache : elem,
+
+		// Only defining an ID for JS objects if its cache already exists allows
+		// the code to shortcut on the same path as a DOM node with no cache
+		id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+	// Avoid doing any more work than we need to when trying to get data on an
+	// object that has no data at all
+	if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) {
+		return;
+	}
+
+	if ( !id ) {
+		// Only DOM nodes need a new unique ID for each element since their data
+		// ends up in the global cache
+		if ( isNode ) {
+			id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++;
+		} else {
+			id = internalKey;
+		}
+	}
+
+	if ( !cache[ id ] ) {
+		// Avoid exposing jQuery metadata on plain JS objects when the object
+		// is serialized using JSON.stringify
+		cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
+	}
+
+	// An object can be passed to jQuery.data instead of a key/value pair; this gets
+	// shallow copied over onto the existing cache
+	if ( typeof name === "object" || typeof name === "function" ) {
+		if ( pvt ) {
+			cache[ id ] = jQuery.extend( cache[ id ], name );
+		} else {
+			cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+		}
+	}
+
+	thisCache = cache[ id ];
+
+	// jQuery data() is stored in a separate object inside the object's internal data
+	// cache in order to avoid key collisions between internal data and user-defined
+	// data.
+	if ( !pvt ) {
+		if ( !thisCache.data ) {
+			thisCache.data = {};
+		}
+
+		thisCache = thisCache.data;
+	}
+
+	if ( data !== undefined ) {
+		thisCache[ jQuery.camelCase( name ) ] = data;
+	}
+
+	// Check for both converted-to-camel and non-converted data property names
+	// If a data property was specified
+	if ( typeof name === "string" ) {
+
+		// First Try to find as-is property data
+		ret = thisCache[ name ];
+
+		// Test for null|undefined property data
+		if ( ret == null ) {
+
+			// Try to find the camelCased property
+			ret = thisCache[ jQuery.camelCase( name ) ];
+		}
+	} else {
+		ret = thisCache;
+	}
+
+	return ret;
+}
+
+function internalRemoveData( elem, name, pvt ) {
+	if ( !jQuery.acceptData( elem ) ) {
+		return;
+	}
+
+	var thisCache, i,
+		isNode = elem.nodeType,
+
+		// See jQuery.data for more information
+		cache = isNode ? jQuery.cache : elem,
+		id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+	// If there is already no cache entry for this object, there is no
+	// purpose in continuing
+	if ( !cache[ id ] ) {
+		return;
+	}
+
+	if ( name ) {
+
+		thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+		if ( thisCache ) {
+
+			// Support array or space separated string names for data keys
+			if ( !jQuery.isArray( name ) ) {
+
+				// try the string as a key before any manipulation
+				if ( name in thisCache ) {
+					name = [ name ];
+				} else {
+
+					// split the camel cased version by spaces unless a key with the spaces exists
+					name = jQuery.camelCase( name );
+					if ( name in thisCache ) {
+						name = [ name ];
+					} else {
+						name = name.split(" ");
+					}
+				}
+			} else {
+				// If "name" is an array of keys...
+				// When data is initially created, via ("key", "val") signature,
+				// keys will be converted to camelCase.
+				// Since there is no way to tell _how_ a key was added, remove
+				// both plain key and camelCase key. #12786
+				// This will only penalize the array argument path.
+				name = name.concat( jQuery.map( name, jQuery.camelCase ) );
+			}
+
+			i = name.length;
+			while ( i-- ) {
+				delete thisCache[ name[i] ];
+			}
+
+			// If there is no data left in the cache, we want to continue
+			// and let the cache object itself get destroyed
+			if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {
+				return;
+			}
+		}
+	}
+
+	// See jQuery.data for more information
+	if ( !pvt ) {
+		delete cache[ id ].data;
+
+		// Don't destroy the parent cache unless the internal data object
+		// had been the only thing left in it
+		if ( !isEmptyDataObject( cache[ id ] ) ) {
+			return;
+		}
+	}
+
+	// Destroy the cache
+	if ( isNode ) {
+		jQuery.cleanData( [ elem ], true );
+
+	// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+	/* jshint eqeqeq: false */
+	} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+		/* jshint eqeqeq: true */
+		delete cache[ id ];
+
+	// When all else fails, null
+	} else {
+		cache[ id ] = null;
+	}
+}
+
+jQuery.extend({
+	cache: {},
+
+	// The following elements throw uncatchable exceptions if you
+	// attempt to add expando properties to them.
+	noData: {
+		"applet": true,
+		"embed": true,
+		// Ban all objects except for Flash (which handle expandos)
+		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
+	},
+
+	hasData: function( elem ) {
+		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+		return !!elem && !isEmptyDataObject( elem );
+	},
+
+	data: function( elem, name, data ) {
+		return internalData( elem, name, data );
+	},
+
+	removeData: function( elem, name ) {
+		return internalRemoveData( elem, name );
+	},
+
+	// For internal use only.
+	_data: function( elem, name, data ) {
+		return internalData( elem, name, data, true );
+	},
+
+	_removeData: function( elem, name ) {
+		return internalRemoveData( elem, name, true );
+	},
+
+	// A method for determining if a DOM node can handle the data expando
+	acceptData: function( elem ) {
+		// Do not set data on non-element because it will not be cleared (#8335).
+		if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {
+			return false;
+		}
+
+		var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+		// nodes accept data unless otherwise specified; rejection can be conditional
+		return !noData || noData !== true && elem.getAttribute("classid") === noData;
+	}
+});
+
+jQuery.fn.extend({
+	data: function( key, value ) {
+		var attrs, name,
+			data = null,
+			i = 0,
+			elem = this[0];
+
+		// Special expections of .data basically thwart jQuery.access,
+		// so implement the relevant behavior ourselves
+
+		// Gets all values
+		if ( key === undefined ) {
+			if ( this.length ) {
+				data = jQuery.data( elem );
+
+				if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+					attrs = elem.attributes;
+					for ( ; i < attrs.length; i++ ) {
+						name = attrs[i].name;
+
+						if ( name.indexOf("data-") === 0 ) {
+							name = jQuery.camelCase( name.slice(5) );
+
+							dataAttr( elem, name, data[ name ] );
+						}
+					}
+					jQuery._data( elem, "parsedAttrs", true );
+				}
+			}
+
+			return data;
+		}
+
+		// Sets multiple values
+		if ( typeof key === "object" ) {
+			return this.each(function() {
+				jQuery.data( this, key );
+			});
+		}
+
+		return arguments.length > 1 ?
+
+			// Sets one value
+			this.each(function() {
+				jQuery.data( this, key, value );
+			}) :
+
+			// Gets one value
+			// Try to fetch any internally stored data first
+			elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
+	},
+
+	removeData: function( key ) {
+		return this.each(function() {
+			jQuery.removeData( this, key );
+		});
+	}
+});
+
+function dataAttr( elem, key, data ) {
+	// If nothing was found internally, try to fetch any
+	// data from the HTML5 data-* attribute
+	if ( data === undefined && elem.nodeType === 1 ) {
+
+		var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+		data = elem.getAttribute( name );
+
+		if ( typeof data === "string" ) {
+			try {
+				data = data === "true" ? true :
+					data === "false" ? false :
+					data === "null" ? null :
+					// Only convert to a number if it doesn't change the string
+					+data + "" === data ? +data :
+					rbrace.test( data ) ? jQuery.parseJSON( data ) :
+						data;
+			} catch( e ) {}
+
+			// Make sure we set the data so it isn't changed later
+			jQuery.data( elem, key, data );
+
+		} else {
+			data = undefined;
+		}
+	}
+
+	return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+	var name;
+	for ( name in obj ) {
+
+		// if the public data object is empty, the private is still empty
+		if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+			continue;
+		}
+		if ( name !== "toJSON" ) {
+			return false;
+		}
+	}
+
+	return true;
+}
+jQuery.extend({
+	queue: function( elem, type, data ) {
+		var queue;
+
+		if ( elem ) {
+			type = ( type || "fx" ) + "queue";
+			queue = jQuery._data( elem, type );
+
+			// Speed up dequeue by getting out quickly if this is just a lookup
+			if ( data ) {
+				if ( !queue || jQuery.isArray(data) ) {
+					queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+				} else {
+					queue.push( data );
+				}
+			}
+			return queue || [];
+		}
+	},
+
+	dequeue: function( elem, type ) {
+		type = type || "fx";
+
+		var queue = jQuery.queue( elem, type ),
+			startLength = queue.length,
+			fn = queue.shift(),
+			hooks = jQuery._queueHooks( elem, type ),
+			next = function() {
+				jQuery.dequeue( elem, type );
+			};
+
+		// If the fx queue is dequeued, always remove the progress sentinel
+		if ( fn === "inprogress" ) {
+			fn = queue.shift();
+			startLength--;
+		}
+
+		hooks.cur = fn;
+		if ( fn ) {
+
+			// Add a progress sentinel to prevent the fx queue from being
+			// automatically dequeued
+			if ( type === "fx" ) {
+				queue.unshift( "inprogress" );
+			}
+
+			// clear up the last queue stop function
+			delete hooks.stop;
+			fn.call( elem, next, hooks );
+		}
+
+		if ( !startLength && hooks ) {
+			hooks.empty.fire();
+		}
+	},
+
+	// not intended for public consumption - generates a queueHooks object, or returns the current one
+	_queueHooks: function( elem, type ) {
+		var key = type + "queueHooks";
+		return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+			empty: jQuery.Callbacks("once memory").add(function() {
+				jQuery._removeData( elem, type + "queue" );
+				jQuery._removeData( elem, key );
+			})
+		});
+	}
+});
+
+jQuery.fn.extend({
+	queue: function( type, data ) {
+		var setter = 2;
+
+		if ( typeof type !== "string" ) {
+			data = type;
+			type = "fx";
+			setter--;
+		}
+
+		if ( arguments.length < setter ) {
+			return jQuery.queue( this[0], type );
+		}
+
+		return data === undefined ?
+			this :
+			this.each(function() {
+				var queue = jQuery.queue( this, type, data );
+
+				// ensure a hooks for this queue
+				jQuery._queueHooks( this, type );
+
+				if ( type === "fx" && queue[0] !== "inprogress" ) {
+					jQuery.dequeue( this, type );
+				}
+			});
+	},
+	dequeue: function( type ) {
+		return this.each(function() {
+			jQuery.dequeue( this, type );
+		});
+	},
+	// Based off of the plugin by Clint Helfers, with permission.
+	// http://blindsignals.com/index.php/2009/07/jquery-delay/
+	delay: function( time, type ) {
+		time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+		type = type || "fx";
+
+		return this.queue( type, function( next, hooks ) {
+			var timeout = setTimeout( next, time );
+			hooks.stop = function() {
+				clearTimeout( timeout );
+			};
+		});
+	},
+	clearQueue: function( type ) {
+		return this.queue( type || "fx", [] );
+	},
+	// Get a promise resolved when queues of a certain type
+	// are emptied (fx is the type by default)
+	promise: function( type, obj ) {
+		var tmp,
+			count = 1,
+			defer = jQuery.Deferred(),
+			elements = this,
+			i = this.length,
+			resolve = function() {
+				if ( !( --count ) ) {
+					defer.resolveWith( elements, [ elements ] );
+				}
+			};
+
+		if ( typeof type !== "string" ) {
+			obj = type;
+			type = undefined;
+		}
+		type = type || "fx";
+
+		while( i-- ) {
+			tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+			if ( tmp && tmp.empty ) {
+				count++;
+				tmp.empty.add( resolve );
+			}
+		}
+		resolve();
+		return defer.promise( obj );
+	}
+});
+var nodeHook, boolHook,
+	rclass = /[\t\r\n\f]/g,
+	rreturn = /\r/g,
+	rfocusable = /^(?:input|select|textarea|button|object)$/i,
+	rclickable = /^(?:a|area)$/i,
+	ruseDefault = /^(?:checked|selected)$/i,
+	getSetAttribute = jQuery.support.getSetAttribute,
+	getSetInput = jQuery.support.input;
+
+jQuery.fn.extend({
+	attr: function( name, value ) {
+		return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+	},
+
+	removeAttr: function( name ) {
+		return this.each(function() {
+			jQuery.removeAttr( this, name );
+		});
+	},
+
+	prop: function( name, value ) {
+		return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+	},
+
+	removeProp: function( name ) {
+		name = jQuery.propFix[ name ] || name;
+		return this.each(function() {
+			// try/catch handles cases where IE balks (such as removing a property on window)
+			try {
+				this[ name ] = undefined;
+				delete this[ name ];
+			} catch( e ) {}
+		});
+	},
+
+	addClass: function( value ) {
+		var classes, elem, cur, clazz, j,
+			i = 0,
+			len = this.length,
+			proceed = typeof value === "string" && value;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).addClass( value.call( this, j, this.className ) );
+			});
+		}
+
+		if ( proceed ) {
+			// The disjunction here is for better compressibility (see removeClass)
+			classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+			for ( ; i < len; i++ ) {
+				elem = this[ i ];
+				cur = elem.nodeType === 1 && ( elem.className ?
+					( " " + elem.className + " " ).replace( rclass, " " ) :
+					" "
+				);
+
+				if ( cur ) {
+					j = 0;
+					while ( (clazz = classes[j++]) ) {
+						if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+							cur += clazz + " ";
+						}
+					}
+					elem.className = jQuery.trim( cur );
+
+				}
+			}
+		}
+
+		return this;
+	},
+
+	removeClass: function( value ) {
+		var classes, elem, cur, clazz, j,
+			i = 0,
+			len = this.length,
+			proceed = arguments.length === 0 || typeof value === "string" && value;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).removeClass( value.call( this, j, this.className ) );
+			});
+		}
+		if ( proceed ) {
+			classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+			for ( ; i < len; i++ ) {
+				elem = this[ i ];
+				// This expression is here for better compressibility (see addClass)
+				cur = elem.nodeType === 1 && ( elem.className ?
+					( " " + elem.className + " " ).replace( rclass, " " ) :
+					""
+				);
+
+				if ( cur ) {
+					j = 0;
+					while ( (clazz = classes[j++]) ) {
+						// Remove *all* instances
+						while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
+							cur = cur.replace( " " + clazz + " ", " " );
+						}
+					}
+					elem.className = value ? jQuery.trim( cur ) : "";
+				}
+			}
+		}
+
+		return this;
+	},
+
+	toggleClass: function( value, stateVal ) {
+		var type = typeof value,
+			isBool = typeof stateVal === "boolean";
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( i ) {
+				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+			});
+		}
+
+		return this.each(function() {
+			if ( type === "string" ) {
+				// toggle individual class names
+				var className,
+					i = 0,
+					self = jQuery( this ),
+					state = stateVal,
+					classNames = value.match( core_rnotwhite ) || [];
+
+				while ( (className = classNames[ i++ ]) ) {
+					// check each className given, space separated list
+					state = isBool ? state : !self.hasClass( className );
+					self[ state ? "addClass" : "removeClass" ]( className );
+				}
+
+			// Toggle whole class name
+			} else if ( type === core_strundefined || type === "boolean" ) {
+				if ( this.className ) {
+					// store className if set
+					jQuery._data( this, "__className__", this.className );
+				}
+
+				// If the element has a class name or if we're passed "false",
+				// then remove the whole classname (if there was one, the above saved it).
+				// Otherwise bring back whatever was previously saved (if anything),
+				// falling back to the empty string if nothing was stored.
+				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+			}
+		});
+	},
+
+	hasClass: function( selector ) {
+		var className = " " + selector + " ",
+			i = 0,
+			l = this.length;
+		for ( ; i < l; i++ ) {
+			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+				return true;
+			}
+		}
+
+		return false;
+	},
+
+	val: function( value ) {
+		var ret, hooks, isFunction,
+			elem = this[0];
+
+		if ( !arguments.length ) {
+			if ( elem ) {
+				hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+					return ret;
+				}
+
+				ret = elem.value;
+
+				return typeof ret === "string" ?
+					// handle most common string cases
+					ret.replace(rreturn, "") :
+					// handle cases where value is null/undef or number
+					ret == null ? "" : ret;
+			}
+
+			return;
+		}
+
+		isFunction = jQuery.isFunction( value );
+
+		return this.each(function( i ) {
+			var val;
+
+			if ( this.nodeType !== 1 ) {
+				return;
+			}
+
+			if ( isFunction ) {
+				val = value.call( this, i, jQuery( this ).val() );
+			} else {
+				val = value;
+			}
+
+			// Treat null/undefined as ""; convert numbers to string
+			if ( val == null ) {
+				val = "";
+			} else if ( typeof val === "number" ) {
+				val += "";
+			} else if ( jQuery.isArray( val ) ) {
+				val = jQuery.map(val, function ( value ) {
+					return value == null ? "" : value + "";
+				});
+			}
+
+			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+			// If set returns undefined, fall back to normal setting
+			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+				this.value = val;
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	valHooks: {
+		option: {
+			get: function( elem ) {
+				// Use proper attribute retrieval(#6932, #12072)
+				var val = jQuery.find.attr( elem, "value" );
+				return val != null ?
+					val :
+					elem.text;
+			}
+		},
+		select: {
+			get: function( elem ) {
+				var value, option,
+					options = elem.options,
+					index = elem.selectedIndex,
+					one = elem.type === "select-one" || index < 0,
+					values = one ? null : [],
+					max = one ? index + 1 : options.length,
+					i = index < 0 ?
+						max :
+						one ? index : 0;
+
+				// Loop through all the selected options
+				for ( ; i < max; i++ ) {
+					option = options[ i ];
+
+					// oldIE doesn't update selected after form reset (#2551)
+					if ( ( option.selected || i === index ) &&
+							// Don't return options that are disabled or in a disabled optgroup
+							( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
+							( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+						// Get the specific value for the option
+						value = jQuery( option ).val();
+
+						// We don't need an array for one selects
+						if ( one ) {
+							return value;
+						}
+
+						// Multi-Selects return an array
+						values.push( value );
+					}
+				}
+
+				return values;
+			},
+
+			set: function( elem, value ) {
+				var optionSet, option,
+					options = elem.options,
+					values = jQuery.makeArray( value ),
+					i = options.length;
+
+				while ( i-- ) {
+					option = options[ i ];
+					if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) {
+						optionSet = true;
+					}
+				}
+
+				// force browsers to behave consistently when non-matching value is set
+				if ( !optionSet ) {
+					elem.selectedIndex = -1;
+				}
+				return values;
+			}
+		}
+	},
+
+	attr: function( elem, name, value ) {
+		var hooks, ret,
+			nType = elem.nodeType;
+
+		// don't get/set attributes on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		// Fallback to prop when attributes are not supported
+		if ( typeof elem.getAttribute === core_strundefined ) {
+			return jQuery.prop( elem, name, value );
+		}
+
+		// All attributes are lowercase
+		// Grab necessary hook if one is defined
+		if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+			name = name.toLowerCase();
+			hooks = jQuery.attrHooks[ name ] ||
+				( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
+		}
+
+		if ( value !== undefined ) {
+
+			if ( value === null ) {
+				jQuery.removeAttr( elem, name );
+
+			} else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				elem.setAttribute( name, value + "" );
+				return value;
+			}
+
+		} else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+			return ret;
+
+		} else {
+			ret = jQuery.find.attr( elem, name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return ret == null ?
+				undefined :
+				ret;
+		}
+	},
+
+	removeAttr: function( elem, value ) {
+		var name, propName,
+			i = 0,
+			attrNames = value && value.match( core_rnotwhite );
+
+		if ( attrNames && elem.nodeType === 1 ) {
+			while ( (name = attrNames[i++]) ) {
+				propName = jQuery.propFix[ name ] || name;
+
+				// Boolean attributes get special treatment (#10870)
+				if ( jQuery.expr.match.bool.test( name ) ) {
+					// Set corresponding property to false
+					if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
+						elem[ propName ] = false;
+					// Support: IE<9
+					// Also clear defaultChecked/defaultSelected (if appropriate)
+					} else {
+						elem[ jQuery.camelCase( "default-" + name ) ] =
+							elem[ propName ] = false;
+					}
+
+				// See #9699 for explanation of this approach (setting first, then removal)
+				} else {
+					jQuery.attr( elem, name, "" );
+				}
+
+				elem.removeAttribute( getSetAttribute ? name : propName );
+			}
+		}
+	},
+
+	attrHooks: {
+		type: {
+			set: function( elem, value ) {
+				if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+					// Setting the type on a radio button after the value resets the value in IE6-9
+					// Reset value to default in case type is set after value during creation
+					var val = elem.value;
+					elem.setAttribute( "type", value );
+					if ( val ) {
+						elem.value = val;
+					}
+					return value;
+				}
+			}
+		}
+	},
+
+	propFix: {
+		"for": "htmlFor",
+		"class": "className"
+	},
+
+	prop: function( elem, name, value ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set properties on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		if ( notxml ) {
+			// Fix name and attach hooks
+			name = jQuery.propFix[ name ] || name;
+			hooks = jQuery.propHooks[ name ];
+		}
+
+		if ( value !== undefined ) {
+			return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?
+				ret :
+				( elem[ name ] = value );
+
+		} else {
+			return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ?
+				ret :
+				elem[ name ];
+		}
+	},
+
+	propHooks: {
+		tabIndex: {
+			get: function( elem ) {
+				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+				// Use proper attribute retrieval(#12072)
+				var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+				return tabindex ?
+					parseInt( tabindex, 10 ) :
+					rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+						0 :
+						-1;
+			}
+		}
+	}
+});
+
+// Hooks for boolean attributes
+boolHook = {
+	set: function( elem, value, name ) {
+		if ( value === false ) {
+			// Remove boolean attributes when set to false
+			jQuery.removeAttr( elem, name );
+		} else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
+			// IE<8 needs the *property* name
+			elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
+
+		// Use defaultChecked and defaultSelected for oldIE
+		} else {
+			elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
+		}
+
+		return name;
+	}
+};
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+	var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr;
+
+	jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ?
+		function( elem, name, isXML ) {
+			var fn = jQuery.expr.attrHandle[ name ],
+				ret = isXML ?
+					undefined :
+					/* jshint eqeqeq: false */
+					(jQuery.expr.attrHandle[ name ] = undefined) !=
+						getter( elem, name, isXML ) ?
+
+						name.toLowerCase() :
+						null;
+			jQuery.expr.attrHandle[ name ] = fn;
+			return ret;
+		} :
+		function( elem, name, isXML ) {
+			return isXML ?
+				undefined :
+				elem[ jQuery.camelCase( "default-" + name ) ] ?
+					name.toLowerCase() :
+					null;
+		};
+});
+
+// fix oldIE attroperties
+if ( !getSetInput || !getSetAttribute ) {
+	jQuery.attrHooks.value = {
+		set: function( elem, value, name ) {
+			if ( jQuery.nodeName( elem, "input" ) ) {
+				// Does not return so that setAttribute is also used
+				elem.defaultValue = value;
+			} else {
+				// Use nodeHook if defined (#1954); otherwise setAttribute is fine
+				return nodeHook && nodeHook.set( elem, value, name );
+			}
+		}
+	};
+}
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+	// Use this for any attribute in IE6/7
+	// This fixes almost every IE6/7 issue
+	nodeHook = {
+		set: function( elem, value, name ) {
+			// Set the existing or create a new attribute node
+			var ret = elem.getAttributeNode( name );
+			if ( !ret ) {
+				elem.setAttributeNode(
+					(ret = elem.ownerDocument.createAttribute( name ))
+				);
+			}
+
+			ret.value = value += "";
+
+			// Break association with cloned elements by also using setAttribute (#9646)
+			return name === "value" || value === elem.getAttribute( name ) ?
+				value :
+				undefined;
+		}
+	};
+	jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords =
+		// Some attributes are constructed with empty-string values when not defined
+		function( elem, name, isXML ) {
+			var ret;
+			return isXML ?
+				undefined :
+				(ret = elem.getAttributeNode( name )) && ret.value !== "" ?
+					ret.value :
+					null;
+		};
+	jQuery.valHooks.button = {
+		get: function( elem, name ) {
+			var ret = elem.getAttributeNode( name );
+			return ret && ret.specified ?
+				ret.value :
+				undefined;
+		},
+		set: nodeHook.set
+	};
+
+	// Set contenteditable to false on removals(#10429)
+	// Setting to empty string throws an error as an invalid value
+	jQuery.attrHooks.contenteditable = {
+		set: function( elem, value, name ) {
+			nodeHook.set( elem, value === "" ? false : value, name );
+		}
+	};
+
+	// Set width and height to auto instead of 0 on empty string( Bug #8150 )
+	// This is for removals
+	jQuery.each([ "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = {
+			set: function( elem, value ) {
+				if ( value === "" ) {
+					elem.setAttribute( name, "auto" );
+					return value;
+				}
+			}
+		};
+	});
+}
+
+
+// Some attributes require a special call on IE
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !jQuery.support.hrefNormalized ) {
+	// href/src property should get the full normalized URL (#10299/#12915)
+	jQuery.each([ "href", "src" ], function( i, name ) {
+		jQuery.propHooks[ name ] = {
+			get: function( elem ) {
+				return elem.getAttribute( name, 4 );
+			}
+		};
+	});
+}
+
+if ( !jQuery.support.style ) {
+	jQuery.attrHooks.style = {
+		get: function( elem ) {
+			// Return undefined in the case of empty string
+			// Note: IE uppercases css property names, but if we were to .toLowerCase()
+			// .cssText, that would destroy case senstitivity in URL's, like in "background"
+			return elem.style.cssText || undefined;
+		},
+		set: function( elem, value ) {
+			return ( elem.style.cssText = value + "" );
+		}
+	};
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+	jQuery.propHooks.selected = {
+		get: function( elem ) {
+			var parent = elem.parentNode;
+
+			if ( parent ) {
+				parent.selectedIndex;
+
+				// Make sure that it also works with optgroups, see #5701
+				if ( parent.parentNode ) {
+					parent.parentNode.selectedIndex;
+				}
+			}
+			return null;
+		}
+	};
+}
+
+jQuery.each([
+	"tabIndex",
+	"readOnly",
+	"maxLength",
+	"cellSpacing",
+	"cellPadding",
+	"rowSpan",
+	"colSpan",
+	"useMap",
+	"frameBorder",
+	"contentEditable"
+], function() {
+	jQuery.propFix[ this.toLowerCase() ] = this;
+});
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+	jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+jQuery.each([ "radio", "checkbox" ], function() {
+	jQuery.valHooks[ this ] = {
+		set: function( elem, value ) {
+			if ( jQuery.isArray( value ) ) {
+				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+			}
+		}
+	};
+	if ( !jQuery.support.checkOn ) {
+		jQuery.valHooks[ this ].get = function( elem ) {
+			// Support: Webkit
+			// "" is returned instead of "on" if a value isn't specified
+			return elem.getAttribute("value") === null ? "on" : elem.value;
+		};
+	}
+});
+var rformElems = /^(?:input|select|textarea)$/i,
+	rkeyEvent = /^key/,
+	rmouseEvent = /^(?:mouse|contextmenu)|click/,
+	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+	rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
+
+function returnTrue() {
+	return true;
+}
+
+function returnFalse() {
+	return false;
+}
+
+function safeActiveElement() {
+	try {
+		return document.activeElement;
+	} catch ( err ) { }
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+	global: {},
+
+	add: function( elem, types, handler, data, selector ) {
+		var tmp, events, t, handleObjIn,
+			special, eventHandle, handleObj,
+			handlers, type, namespaces, origType,
+			elemData = jQuery._data( elem );
+
+		// Don't attach events to noData or text/comment nodes (but allow plain objects)
+		if ( !elemData ) {
+			return;
+		}
+
+		// Caller can pass in an object of custom data in lieu of the handler
+		if ( handler.handler ) {
+			handleObjIn = handler;
+			handler = handleObjIn.handler;
+			selector = handleObjIn.selector;
+		}
+
+		// Make sure that the handler has a unique ID, used to find/remove it later
+		if ( !handler.guid ) {
+			handler.guid = jQuery.guid++;
+		}
+
+		// Init the element's event structure and main handler, if this is the first
+		if ( !(events = elemData.events) ) {
+			events = elemData.events = {};
+		}
+		if ( !(eventHandle = elemData.handle) ) {
+			eventHandle = elemData.handle = function( e ) {
+				// Discard the second event of a jQuery.event.trigger() and
+				// when an event is called after a page has unloaded
+				return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
+					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+					undefined;
+			};
+			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+			eventHandle.elem = elem;
+		}
+
+		// Handle multiple events separated by a space
+		types = ( types || "" ).match( core_rnotwhite ) || [""];
+		t = types.length;
+		while ( t-- ) {
+			tmp = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tmp[1];
+			namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+			// There *must* be a type, no attaching namespace-only handlers
+			if ( !type ) {
+				continue;
+			}
+
+			// If event changes its type, use the special event handlers for the changed type
+			special = jQuery.event.special[ type ] || {};
+
+			// If selector defined, determine special event api type, otherwise given type
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+
+			// Update special based on newly reset type
+			special = jQuery.event.special[ type ] || {};
+
+			// handleObj is passed to all event handlers
+			handleObj = jQuery.extend({
+				type: type,
+				origType: origType,
+				data: data,
+				handler: handler,
+				guid: handler.guid,
+				selector: selector,
+				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+				namespace: namespaces.join(".")
+			}, handleObjIn );
+
+			// Init the event handler queue if we're the first
+			if ( !(handlers = events[ type ]) ) {
+				handlers = events[ type ] = [];
+				handlers.delegateCount = 0;
+
+				// Only use addEventListener/attachEvent if the special events handler returns false
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					if ( elem.addEventListener ) {
+						elem.addEventListener( type, eventHandle, false );
+
+					} else if ( elem.attachEvent ) {
+						elem.attachEvent( "on" + type, eventHandle );
+					}
+				}
+			}
+
+			if ( special.add ) {
+				special.add.call( elem, handleObj );
+
+				if ( !handleObj.handler.guid ) {
+					handleObj.handler.guid = handler.guid;
+				}
+			}
+
+			// Add to the element's handler list, delegates in front
+			if ( selector ) {
+				handlers.splice( handlers.delegateCount++, 0, handleObj );
+			} else {
+				handlers.push( handleObj );
+			}
+
+			// Keep track of which events have ever been used, for event optimization
+			jQuery.event.global[ type ] = true;
+		}
+
+		// Nullify elem to prevent memory leaks in IE
+		elem = null;
+	},
+
+	// Detach an event or set of events from an element
+	remove: function( elem, types, handler, selector, mappedTypes ) {
+		var j, handleObj, tmp,
+			origCount, t, events,
+			special, handlers, type,
+			namespaces, origType,
+			elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+		if ( !elemData || !(events = elemData.events) ) {
+			return;
+		}
+
+		// Once for each type.namespace in types; type may be omitted
+		types = ( types || "" ).match( core_rnotwhite ) || [""];
+		t = types.length;
+		while ( t-- ) {
+			tmp = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tmp[1];
+			namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+			// Unbind all events (on this namespace, if provided) for the element
+			if ( !type ) {
+				for ( type in events ) {
+					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+				}
+				continue;
+			}
+
+			special = jQuery.event.special[ type ] || {};
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+			handlers = events[ type ] || [];
+			tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
+
+			// Remove matching events
+			origCount = j = handlers.length;
+			while ( j-- ) {
+				handleObj = handlers[ j ];
+
+				if ( ( mappedTypes || origType === handleObj.origType ) &&
+					( !handler || handler.guid === handleObj.guid ) &&
+					( !tmp || tmp.test( handleObj.namespace ) ) &&
+					( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+					handlers.splice( j, 1 );
+
+					if ( handleObj.selector ) {
+						handlers.delegateCount--;
+					}
+					if ( special.remove ) {
+						special.remove.call( elem, handleObj );
+					}
+				}
+			}
+
+			// Remove generic event handler if we removed something and no more handlers exist
+			// (avoids potential for endless recursion during removal of special event handlers)
+			if ( origCount && !handlers.length ) {
+				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+					jQuery.removeEvent( elem, type, elemData.handle );
+				}
+
+				delete events[ type ];
+			}
+		}
+
+		// Remove the expando if it's no longer used
+		if ( jQuery.isEmptyObject( events ) ) {
+			delete elemData.handle;
+
+			// removeData also checks for emptiness and clears the expando if empty
+			// so use it instead of delete
+			jQuery._removeData( elem, "events" );
+		}
+	},
+
+	trigger: function( event, data, elem, onlyHandlers ) {
+		var handle, ontype, cur,
+			bubbleType, special, tmp, i,
+			eventPath = [ elem || document ],
+			type = core_hasOwn.call( event, "type" ) ? event.type : event,
+			namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
+
+		cur = tmp = elem = elem || document;
+
+		// Don't do events on text and comment nodes
+		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+			return;
+		}
+
+		// focus/blur morphs to focusin/out; ensure we're not firing them right now
+		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+			return;
+		}
+
+		if ( type.indexOf(".") >= 0 ) {
+			// Namespaced trigger; create a regexp to match event type in handle()
+			namespaces = type.split(".");
+			type = namespaces.shift();
+			namespaces.sort();
+		}
+		ontype = type.indexOf(":") < 0 && "on" + type;
+
+		// Caller can pass in a jQuery.Event object, Object, or just an event type string
+		event = event[ jQuery.expando ] ?
+			event :
+			new jQuery.Event( type, typeof event === "object" && event );
+
+		// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+		event.isTrigger = onlyHandlers ? 2 : 3;
+		event.namespace = namespaces.join(".");
+		event.namespace_re = event.namespace ?
+			new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
+			null;
+
+		// Clean up the event in case it is being reused
+		event.result = undefined;
+		if ( !event.target ) {
+			event.target = elem;
+		}
+
+		// Clone any incoming data and prepend the event, creating the handler arg list
+		data = data == null ?
+			[ event ] :
+			jQuery.makeArray( data, [ event ] );
+
+		// Allow special events to draw outside the lines
+		special = jQuery.event.special[ type ] || {};
+		if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+			return;
+		}
+
+		// Determine event propagation path in advance, per W3C events spec (#9951)
+		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+			bubbleType = special.delegateType || type;
+			if ( !rfocusMorph.test( bubbleType + type ) ) {
+				cur = cur.parentNode;
+			}
+			for ( ; cur; cur = cur.parentNode ) {
+				eventPath.push( cur );
+				tmp = cur;
+			}
+
+			// Only add window if we got to document (e.g., not plain obj or detached DOM)
+			if ( tmp === (elem.ownerDocument || document) ) {
+				eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+			}
+		}
+
+		// Fire handlers on the event path
+		i = 0;
+		while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
+
+			event.type = i > 1 ?
+				bubbleType :
+				special.bindType || type;
+
+			// jQuery handler
+			handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+			if ( handle ) {
+				handle.apply( cur, data );
+			}
+
+			// Native handler
+			handle = ontype && cur[ ontype ];
+			if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+				event.preventDefault();
+			}
+		}
+		event.type = type;
+
+		// If nobody prevented the default action, do it now
+		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+			if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&
+				jQuery.acceptData( elem ) ) {
+
+				// Call a native DOM method on the target with the same name name as the event.
+				// Can't use an .isFunction() check here because IE6/7 fails that test.
+				// Don't do default actions on window, that's where global variables be (#6170)
+				if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {
+
+					// Don't re-trigger an onFOO event when we call its FOO() method
+					tmp = elem[ ontype ];
+
+					if ( tmp ) {
+						elem[ ontype ] = null;
+					}
+
+					// Prevent re-triggering of the same event, since we already bubbled it above
+					jQuery.event.triggered = type;
+					try {
+						elem[ type ]();
+					} catch ( e ) {
+						// IE<9 dies on focus/blur to hidden element (#1486,#12518)
+						// only reproducible on winXP IE8 native, not IE9 in IE8 mode
+					}
+					jQuery.event.triggered = undefined;
+
+					if ( tmp ) {
+						elem[ ontype ] = tmp;
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	dispatch: function( event ) {
+
+		// Make a writable jQuery.Event from the native event object
+		event = jQuery.event.fix( event );
+
+		var i, ret, handleObj, matched, j,
+			handlerQueue = [],
+			args = core_slice.call( arguments ),
+			handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
+			special = jQuery.event.special[ event.type ] || {};
+
+		// Use the fix-ed jQuery.Event rather than the (read-only) native event
+		args[0] = event;
+		event.delegateTarget = this;
+
+		// Call the preDispatch hook for the mapped type, and let it bail if desired
+		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+			return;
+		}
+
+		// Determine handlers
+		handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+		// Run delegates first; they may want to stop propagation beneath us
+		i = 0;
+		while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
+			event.currentTarget = matched.elem;
+
+			j = 0;
+			while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
+
+				// Triggered event must either 1) have no namespace, or
+				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+				if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
+
+					event.handleObj = handleObj;
+					event.data = handleObj.data;
+
+					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+							.apply( matched.elem, args );
+
+					if ( ret !== undefined ) {
+						if ( (event.result = ret) === false ) {
+							event.preventDefault();
+							event.stopPropagation();
+						}
+					}
+				}
+			}
+		}
+
+		// Call the postDispatch hook for the mapped type
+		if ( special.postDispatch ) {
+			special.postDispatch.call( this, event );
+		}
+
+		return event.result;
+	},
+
+	handlers: function( event, handlers ) {
+		var sel, handleObj, matches, i,
+			handlerQueue = [],
+			delegateCount = handlers.delegateCount,
+			cur = event.target;
+
+		// Find delegate handlers
+		// Black-hole SVG <use> instance trees (#13180)
+		// Avoid non-left-click bubbling in Firefox (#3861)
+		if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
+
+			/* jshint eqeqeq: false */
+			for ( ; cur != this; cur = cur.parentNode || this ) {
+				/* jshint eqeqeq: true */
+
+				// Don't check non-elements (#13208)
+				// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+				if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
+					matches = [];
+					for ( i = 0; i < delegateCount; i++ ) {
+						handleObj = handlers[ i ];
+
+						// Don't conflict with Object.prototype properties (#13203)
+						sel = handleObj.selector + " ";
+
+						if ( matches[ sel ] === undefined ) {
+							matches[ sel ] = handleObj.needsContext ?
+								jQuery( sel, this ).index( cur ) >= 0 :
+								jQuery.find( sel, this, null, [ cur ] ).length;
+						}
+						if ( matches[ sel ] ) {
+							matches.push( handleObj );
+						}
+					}
+					if ( matches.length ) {
+						handlerQueue.push({ elem: cur, handlers: matches });
+					}
+				}
+			}
+		}
+
+		// Add the remaining (directly-bound) handlers
+		if ( delegateCount < handlers.length ) {
+			handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
+		}
+
+		return handlerQueue;
+	},
+
+	fix: function( event ) {
+		if ( event[ jQuery.expando ] ) {
+			return event;
+		}
+
+		// Create a writable copy of the event object and normalize some properties
+		var i, prop, copy,
+			type = event.type,
+			originalEvent = event,
+			fixHook = this.fixHooks[ type ];
+
+		if ( !fixHook ) {
+			this.fixHooks[ type ] = fixHook =
+				rmouseEvent.test( type ) ? this.mouseHooks :
+				rkeyEvent.test( type ) ? this.keyHooks :
+				{};
+		}
+		copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+		event = new jQuery.Event( originalEvent );
+
+		i = copy.length;
+		while ( i-- ) {
+			prop = copy[ i ];
+			event[ prop ] = originalEvent[ prop ];
+		}
+
+		// Support: IE<9
+		// Fix target property (#1925)
+		if ( !event.target ) {
+			event.target = originalEvent.srcElement || document;
+		}
+
+		// Support: Chrome 23+, Safari?
+		// Target should not be a text node (#504, #13143)
+		if ( event.target.nodeType === 3 ) {
+			event.target = event.target.parentNode;
+		}
+
+		// Support: IE<9
+		// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
+		event.metaKey = !!event.metaKey;
+
+		return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
+	},
+
+	// Includes some event props shared by KeyEvent and MouseEvent
+	props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+	fixHooks: {},
+
+	keyHooks: {
+		props: "char charCode key keyCode".split(" "),
+		filter: function( event, original ) {
+
+			// Add which for key events
+			if ( event.which == null ) {
+				event.which = original.charCode != null ? original.charCode : original.keyCode;
+			}
+
+			return event;
+		}
+	},
+
+	mouseHooks: {
+		props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+		filter: function( event, original ) {
+			var body, eventDoc, doc,
+				button = original.button,
+				fromElement = original.fromElement;
+
+			// Calculate pageX/Y if missing and clientX/Y available
+			if ( event.pageX == null && original.clientX != null ) {
+				eventDoc = event.target.ownerDocument || document;
+				doc = eventDoc.documentElement;
+				body = eventDoc.body;
+
+				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+			}
+
+			// Add relatedTarget, if necessary
+			if ( !event.relatedTarget && fromElement ) {
+				event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+			}
+
+			// Add which for click: 1 === left; 2 === middle; 3 === right
+			// Note: button is not normalized, so don't use it
+			if ( !event.which && button !== undefined ) {
+				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+			}
+
+			return event;
+		}
+	},
+
+	special: {
+		load: {
+			// Prevent triggered image.load events from bubbling to window.load
+			noBubble: true
+		},
+		focus: {
+			// Fire native event if possible so blur/focus sequence is correct
+			trigger: function() {
+				if ( this !== safeActiveElement() && this.focus ) {
+					try {
+						this.focus();
+						return false;
+					} catch ( e ) {
+						// Support: IE<9
+						// If we error on focus to hidden element (#1486, #12518),
+						// let .trigger() run the handlers
+					}
+				}
+			},
+			delegateType: "focusin"
+		},
+		blur: {
+			trigger: function() {
+				if ( this === safeActiveElement() && this.blur ) {
+					this.blur();
+					return false;
+				}
+			},
+			delegateType: "focusout"
+		},
+		click: {
+			// For checkbox, fire native event so checked state will be right
+			trigger: function() {
+				if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
+					this.click();
+					return false;
+				}
+			},
+
+			// For cross-browser consistency, don't fire native .click() on links
+			_default: function( event ) {
+				return jQuery.nodeName( event.target, "a" );
+			}
+		},
+
+		beforeunload: {
+			postDispatch: function( event ) {
+
+				// Even when returnValue equals to undefined Firefox will still show alert
+				if ( event.result !== undefined ) {
+					event.originalEvent.returnValue = event.result;
+				}
+			}
+		}
+	},
+
+	simulate: function( type, elem, event, bubble ) {
+		// Piggyback on a donor event to simulate a different one.
+		// Fake originalEvent to avoid donor's stopPropagation, but if the
+		// simulated event prevents default then we do the same on the donor.
+		var e = jQuery.extend(
+			new jQuery.Event(),
+			event,
+			{
+				type: type,
+				isSimulated: true,
+				originalEvent: {}
+			}
+		);
+		if ( bubble ) {
+			jQuery.event.trigger( e, null, elem );
+		} else {
+			jQuery.event.dispatch.call( elem, e );
+		}
+		if ( e.isDefaultPrevented() ) {
+			event.preventDefault();
+		}
+	}
+};
+
+jQuery.removeEvent = document.removeEventListener ?
+	function( elem, type, handle ) {
+		if ( elem.removeEventListener ) {
+			elem.removeEventListener( type, handle, false );
+		}
+	} :
+	function( elem, type, handle ) {
+		var name = "on" + type;
+
+		if ( elem.detachEvent ) {
+
+			// #8545, #7054, preventing memory leaks for custom events in IE6-8
+			// detachEvent needed property on element, by name of that event, to properly expose it to GC
+			if ( typeof elem[ name ] === core_strundefined ) {
+				elem[ name ] = null;
+			}
+
+			elem.detachEvent( name, handle );
+		}
+	};
+
+jQuery.Event = function( src, props ) {
+	// Allow instantiation without the 'new' keyword
+	if ( !(this instanceof jQuery.Event) ) {
+		return new jQuery.Event( src, props );
+	}
+
+	// Event object
+	if ( src && src.type ) {
+		this.originalEvent = src;
+		this.type = src.type;
+
+		// Events bubbling up the document may have been marked as prevented
+		// by a handler lower down the tree; reflect the correct value.
+		this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+			src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+	// Event type
+	} else {
+		this.type = src;
+	}
+
+	// Put explicitly provided properties onto the event object
+	if ( props ) {
+		jQuery.extend( this, props );
+	}
+
+	// Create a timestamp if incoming event doesn't have one
+	this.timeStamp = src && src.timeStamp || jQuery.now();
+
+	// Mark it as fixed
+	this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+	isDefaultPrevented: returnFalse,
+	isPropagationStopped: returnFalse,
+	isImmediatePropagationStopped: returnFalse,
+
+	preventDefault: function() {
+		var e = this.originalEvent;
+
+		this.isDefaultPrevented = returnTrue;
+		if ( !e ) {
+			return;
+		}
+
+		// If preventDefault exists, run it on the original event
+		if ( e.preventDefault ) {
+			e.preventDefault();
+
+		// Support: IE
+		// Otherwise set the returnValue property of the original event to false
+		} else {
+			e.returnValue = false;
+		}
+	},
+	stopPropagation: function() {
+		var e = this.originalEvent;
+
+		this.isPropagationStopped = returnTrue;
+		if ( !e ) {
+			return;
+		}
+		// If stopPropagation exists, run it on the original event
+		if ( e.stopPropagation ) {
+			e.stopPropagation();
+		}
+
+		// Support: IE
+		// Set the cancelBubble property of the original event to true
+		e.cancelBubble = true;
+	},
+	stopImmediatePropagation: function() {
+		this.isImmediatePropagationStopped = returnTrue;
+		this.stopPropagation();
+	}
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+	mouseenter: "mouseover",
+	mouseleave: "mouseout"
+}, function( orig, fix ) {
+	jQuery.event.special[ orig ] = {
+		delegateType: fix,
+		bindType: fix,
+
+		handle: function( event ) {
+			var ret,
+				target = this,
+				related = event.relatedTarget,
+				handleObj = event.handleObj;
+
+			// For mousenter/leave call the handler if related is outside the target.
+			// NB: No relatedTarget if the mouse left/entered the browser window
+			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+				event.type = handleObj.origType;
+				ret = handleObj.handler.apply( this, arguments );
+				event.type = fix;
+			}
+			return ret;
+		}
+	};
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+	jQuery.event.special.submit = {
+		setup: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Lazy-add a submit handler when a descendant form may potentially be submitted
+			jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+				// Node name check avoids a VML-related crash in IE (#9807)
+				var elem = e.target,
+					form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+				if ( form && !jQuery._data( form, "submitBubbles" ) ) {
+					jQuery.event.add( form, "submit._submit", function( event ) {
+						event._submit_bubble = true;
+					});
+					jQuery._data( form, "submitBubbles", true );
+				}
+			});
+			// return undefined since we don't need an event listener
+		},
+
+		postDispatch: function( event ) {
+			// If form was submitted by the user, bubble the event up the tree
+			if ( event._submit_bubble ) {
+				delete event._submit_bubble;
+				if ( this.parentNode && !event.isTrigger ) {
+					jQuery.event.simulate( "submit", this.parentNode, event, true );
+				}
+			}
+		},
+
+		teardown: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+			jQuery.event.remove( this, "._submit" );
+		}
+	};
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+	jQuery.event.special.change = {
+
+		setup: function() {
+
+			if ( rformElems.test( this.nodeName ) ) {
+				// IE doesn't fire change on a check/radio until blur; trigger it on click
+				// after a propertychange. Eat the blur-change in special.change.handle.
+				// This still fires onchange a second time for check/radio after blur.
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					jQuery.event.add( this, "propertychange._change", function( event ) {
+						if ( event.originalEvent.propertyName === "checked" ) {
+							this._just_changed = true;
+						}
+					});
+					jQuery.event.add( this, "click._change", function( event ) {
+						if ( this._just_changed && !event.isTrigger ) {
+							this._just_changed = false;
+						}
+						// Allow triggered, simulated change events (#11500)
+						jQuery.event.simulate( "change", this, event, true );
+					});
+				}
+				return false;
+			}
+			// Delegated event; lazy-add a change handler on descendant inputs
+			jQuery.event.add( this, "beforeactivate._change", function( e ) {
+				var elem = e.target;
+
+				if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) {
+					jQuery.event.add( elem, "change._change", function( event ) {
+						if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+							jQuery.event.simulate( "change", this.parentNode, event, true );
+						}
+					});
+					jQuery._data( elem, "changeBubbles", true );
+				}
+			});
+		},
+
+		handle: function( event ) {
+			var elem = event.target;
+
+			// Swallow native change events from checkbox/radio, we already triggered them above
+			if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+				return event.handleObj.handler.apply( this, arguments );
+			}
+		},
+
+		teardown: function() {
+			jQuery.event.remove( this, "._change" );
+
+			return !rformElems.test( this.nodeName );
+		}
+	};
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+		// Attach a single capturing handler while someone wants focusin/focusout
+		var attaches = 0,
+			handler = function( event ) {
+				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+			};
+
+		jQuery.event.special[ fix ] = {
+			setup: function() {
+				if ( attaches++ === 0 ) {
+					document.addEventListener( orig, handler, true );
+				}
+			},
+			teardown: function() {
+				if ( --attaches === 0 ) {
+					document.removeEventListener( orig, handler, true );
+				}
+			}
+		};
+	});
+}
+
+jQuery.fn.extend({
+
+	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+		var type, origFn;
+
+		// Types can be a map of types/handlers
+		if ( typeof types === "object" ) {
+			// ( types-Object, selector, data )
+			if ( typeof selector !== "string" ) {
+				// ( types-Object, data )
+				data = data || selector;
+				selector = undefined;
+			}
+			for ( type in types ) {
+				this.on( type, selector, data, types[ type ], one );
+			}
+			return this;
+		}
+
+		if ( data == null && fn == null ) {
+			// ( types, fn )
+			fn = selector;
+			data = selector = undefined;
+		} else if ( fn == null ) {
+			if ( typeof selector === "string" ) {
+				// ( types, selector, fn )
+				fn = data;
+				data = undefined;
+			} else {
+				// ( types, data, fn )
+				fn = data;
+				data = selector;
+				selector = undefined;
+			}
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		} else if ( !fn ) {
+			return this;
+		}
+
+		if ( one === 1 ) {
+			origFn = fn;
+			fn = function( event ) {
+				// Can use an empty set, since event contains the info
+				jQuery().off( event );
+				return origFn.apply( this, arguments );
+			};
+			// Use same guid so caller can remove using origFn
+			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+		}
+		return this.each( function() {
+			jQuery.event.add( this, types, fn, data, selector );
+		});
+	},
+	one: function( types, selector, data, fn ) {
+		return this.on( types, selector, data, fn, 1 );
+	},
+	off: function( types, selector, fn ) {
+		var handleObj, type;
+		if ( types && types.preventDefault && types.handleObj ) {
+			// ( event )  dispatched jQuery.Event
+			handleObj = types.handleObj;
+			jQuery( types.delegateTarget ).off(
+				handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+				handleObj.selector,
+				handleObj.handler
+			);
+			return this;
+		}
+		if ( typeof types === "object" ) {
+			// ( types-object [, selector] )
+			for ( type in types ) {
+				this.off( type, selector, types[ type ] );
+			}
+			return this;
+		}
+		if ( selector === false || typeof selector === "function" ) {
+			// ( types [, fn] )
+			fn = selector;
+			selector = undefined;
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		}
+		return this.each(function() {
+			jQuery.event.remove( this, types, fn, selector );
+		});
+	},
+
+	trigger: function( type, data ) {
+		return this.each(function() {
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	triggerHandler: function( type, data ) {
+		var elem = this[0];
+		if ( elem ) {
+			return jQuery.event.trigger( type, data, elem, true );
+		}
+	}
+});
+var isSimple = /^.[^:#\[\.,]*$/,
+	rparentsprev = /^(?:parents|prev(?:Until|All))/,
+	rneedsContext = jQuery.expr.match.needsContext,
+	// methods guaranteed to produce a unique set when starting from a unique set
+	guaranteedUnique = {
+		children: true,
+		contents: true,
+		next: true,
+		prev: true
+	};
+
+jQuery.fn.extend({
+	find: function( selector ) {
+		var i,
+			ret = [],
+			self = this,
+			len = self.length;
+
+		if ( typeof selector !== "string" ) {
+			return this.pushStack( jQuery( selector ).filter(function() {
+				for ( i = 0; i < len; i++ ) {
+					if ( jQuery.contains( self[ i ], this ) ) {
+						return true;
+					}
+				}
+			}) );
+		}
+
+		for ( i = 0; i < len; i++ ) {
+			jQuery.find( selector, self[ i ], ret );
+		}
+
+		// Needed because $( selector, context ) becomes $( context ).find( selector )
+		ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
+		ret.selector = this.selector ? this.selector + " " + selector : selector;
+		return ret;
+	},
+
+	has: function( target ) {
+		var i,
+			targets = jQuery( target, this ),
+			len = targets.length;
+
+		return this.filter(function() {
+			for ( i = 0; i < len; i++ ) {
+				if ( jQuery.contains( this, targets[i] ) ) {
+					return true;
+				}
+			}
+		});
+	},
+
+	not: function( selector ) {
+		return this.pushStack( winnow(this, selector || [], true) );
+	},
+
+	filter: function( selector ) {
+		return this.pushStack( winnow(this, selector || [], false) );
+	},
+
+	is: function( selector ) {
+		return !!winnow(
+			this,
+
+			// If this is a positional/relative selector, check membership in the returned set
+			// so $("p:first").is("p:last") won't return true for a doc with two "p".
+			typeof selector === "string" && rneedsContext.test( selector ) ?
+				jQuery( selector ) :
+				selector || [],
+			false
+		).length;
+	},
+
+	closest: function( selectors, context ) {
+		var cur,
+			i = 0,
+			l = this.length,
+			ret = [],
+			pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+				jQuery( selectors, context || this.context ) :
+				0;
+
+		for ( ; i < l; i++ ) {
+			for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {
+				// Always skip document fragments
+				if ( cur.nodeType < 11 && (pos ?
+					pos.index(cur) > -1 :
+
+					// Don't pass non-elements to Sizzle
+					cur.nodeType === 1 &&
+						jQuery.find.matchesSelector(cur, selectors)) ) {
+
+					cur = ret.push( cur );
+					break;
+				}
+			}
+		}
+
+		return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+
+		// No argument, return index in parent
+		if ( !elem ) {
+			return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1;
+		}
+
+		// index in selector
+		if ( typeof elem === "string" ) {
+			return jQuery.inArray( this[0], jQuery( elem ) );
+		}
+
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem.jquery ? elem[0] : elem, this );
+	},
+
+	add: function( selector, context ) {
+		var set = typeof selector === "string" ?
+				jQuery( selector, context ) :
+				jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+			all = jQuery.merge( this.get(), set );
+
+		return this.pushStack( jQuery.unique(all) );
+	},
+
+	addBack: function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter(selector)
+		);
+	}
+});
+
+function sibling( cur, dir ) {
+	do {
+		cur = cur[ dir ];
+	} while ( cur && cur.nodeType !== 1 );
+
+	return cur;
+}
+
+jQuery.each({
+	parent: function( elem ) {
+		var parent = elem.parentNode;
+		return parent && parent.nodeType !== 11 ? parent : null;
+	},
+	parents: function( elem ) {
+		return jQuery.dir( elem, "parentNode" );
+	},
+	parentsUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "parentNode", until );
+	},
+	next: function( elem ) {
+		return sibling( elem, "nextSibling" );
+	},
+	prev: function( elem ) {
+		return sibling( elem, "previousSibling" );
+	},
+	nextAll: function( elem ) {
+		return jQuery.dir( elem, "nextSibling" );
+	},
+	prevAll: function( elem ) {
+		return jQuery.dir( elem, "previousSibling" );
+	},
+	nextUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "nextSibling", until );
+	},
+	prevUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "previousSibling", until );
+	},
+	siblings: function( elem ) {
+		return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+	},
+	children: function( elem ) {
+		return jQuery.sibling( elem.firstChild );
+	},
+	contents: function( elem ) {
+		return jQuery.nodeName( elem, "iframe" ) ?
+			elem.contentDocument || elem.contentWindow.document :
+			jQuery.merge( [], elem.childNodes );
+	}
+}, function( name, fn ) {
+	jQuery.fn[ name ] = function( until, selector ) {
+		var ret = jQuery.map( this, fn, until );
+
+		if ( name.slice( -5 ) !== "Until" ) {
+			selector = until;
+		}
+
+		if ( selector && typeof selector === "string" ) {
+			ret = jQuery.filter( selector, ret );
+		}
+
+		if ( this.length > 1 ) {
+			// Remove duplicates
+			if ( !guaranteedUnique[ name ] ) {
+				ret = jQuery.unique( ret );
+			}
+
+			// Reverse order for parents* and prev-derivatives
+			if ( rparentsprev.test( name ) ) {
+				ret = ret.reverse();
+			}
+		}
+
+		return this.pushStack( ret );
+	};
+});
+
+jQuery.extend({
+	filter: function( expr, elems, not ) {
+		var elem = elems[ 0 ];
+
+		if ( not ) {
+			expr = ":not(" + expr + ")";
+		}
+
+		return elems.length === 1 && elem.nodeType === 1 ?
+			jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
+			jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+				return elem.nodeType === 1;
+			}));
+	},
+
+	dir: function( elem, dir, until ) {
+		var matched = [],
+			cur = elem[ dir ];
+
+		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+			if ( cur.nodeType === 1 ) {
+				matched.push( cur );
+			}
+			cur = cur[dir];
+		}
+		return matched;
+	},
+
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType === 1 && n !== elem ) {
+				r.push( n );
+			}
+		}
+
+		return r;
+	}
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+	if ( jQuery.isFunction( qualifier ) ) {
+		return jQuery.grep( elements, function( elem, i ) {
+			/* jshint -W018 */
+			return !!qualifier.call( elem, i, elem ) !== not;
+		});
+
+	}
+
+	if ( qualifier.nodeType ) {
+		return jQuery.grep( elements, function( elem ) {
+			return ( elem === qualifier ) !== not;
+		});
+
+	}
+
+	if ( typeof qualifier === "string" ) {
+		if ( isSimple.test( qualifier ) ) {
+			return jQuery.filter( qualifier, elements, not );
+		}
+
+		qualifier = jQuery.filter( qualifier, elements );
+	}
+
+	return jQuery.grep( elements, function( elem ) {
+		return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not;
+	});
+}
+function createSafeFragment( document ) {
+	var list = nodeNames.split( "|" ),
+		safeFrag = document.createDocumentFragment();
+
+	if ( safeFrag.createElement ) {
+		while ( list.length ) {
+			safeFrag.createElement(
+				list.pop()
+			);
+		}
+	}
+	return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+	rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+	rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+	rleadingWhitespace = /^\s+/,
+	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+	rtagName = /<([\w:]+)/,
+	rtbody = /<tbody/i,
+	rhtml = /<|&#?\w+;/,
+	rnoInnerhtml = /<(?:script|style|link)/i,
+	manipulation_rcheckableType = /^(?:checkbox|radio)$/i,
+	// checked="checked" or checked
+	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+	rscriptType = /^$|\/(?:java|ecma)script/i,
+	rscriptTypeMasked = /^true\/(.*)/,
+	rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
+
+	// We have to close these tags to support XHTML (#13200)
+	wrapMap = {
+		option: [ 1, "<select multiple='multiple'>", "</select>" ],
+		legend: [ 1, "<fieldset>", "</fieldset>" ],
+		area: [ 1, "<map>", "</map>" ],
+		param: [ 1, "<object>", "</object>" ],
+		thead: [ 1, "<table>", "</table>" ],
+		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+		// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+		// unless wrapped in a div with non-breaking characters in front of it.
+		_default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>"  ]
+	},
+	safeFragment = createSafeFragment( document ),
+	fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+jQuery.fn.extend({
+	text: function( value ) {
+		return jQuery.access( this, function( value ) {
+			return value === undefined ?
+				jQuery.text( this ) :
+				this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+		}, null, value, arguments.length );
+	},
+
+	append: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+				var target = manipulationTarget( this, elem );
+				target.appendChild( elem );
+			}
+		});
+	},
+
+	prepend: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+				var target = manipulationTarget( this, elem );
+				target.insertBefore( elem, target.firstChild );
+			}
+		});
+	},
+
+	before: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.parentNode ) {
+				this.parentNode.insertBefore( elem, this );
+			}
+		});
+	},
+
+	after: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.parentNode ) {
+				this.parentNode.insertBefore( elem, this.nextSibling );
+			}
+		});
+	},
+
+	// keepData is for internal use only--do not document
+	remove: function( selector, keepData ) {
+		var elem,
+			elems = selector ? jQuery.filter( selector, this ) : this,
+			i = 0;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( !keepData && elem.nodeType === 1 ) {
+				jQuery.cleanData( getAll( elem ) );
+			}
+
+			if ( elem.parentNode ) {
+				if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
+					setGlobalEval( getAll( elem, "script" ) );
+				}
+				elem.parentNode.removeChild( elem );
+			}
+		}
+
+		return this;
+	},
+
+	empty: function() {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			// Remove element nodes and prevent memory leaks
+			if ( elem.nodeType === 1 ) {
+				jQuery.cleanData( getAll( elem, false ) );
+			}
+
+			// Remove any remaining nodes
+			while ( elem.firstChild ) {
+				elem.removeChild( elem.firstChild );
+			}
+
+			// If this is a select, ensure that it displays empty (#12336)
+			// Support: IE<9
+			if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
+				elem.options.length = 0;
+			}
+		}
+
+		return this;
+	},
+
+	clone: function( dataAndEvents, deepDataAndEvents ) {
+		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+		return this.map( function () {
+			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+		});
+	},
+
+	html: function( value ) {
+		return jQuery.access( this, function( value ) {
+			var elem = this[0] || {},
+				i = 0,
+				l = this.length;
+
+			if ( value === undefined ) {
+				return elem.nodeType === 1 ?
+					elem.innerHTML.replace( rinlinejQuery, "" ) :
+					undefined;
+			}
+
+			// See if we can take a shortcut and just use innerHTML
+			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+				( jQuery.support.htmlSerialize || !rnoshimcache.test( value )  ) &&
+				( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+				!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+				value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+				try {
+					for (; i < l; i++ ) {
+						// Remove element nodes and prevent memory leaks
+						elem = this[i] || {};
+						if ( elem.nodeType === 1 ) {
+							jQuery.cleanData( getAll( elem, false ) );
+							elem.innerHTML = value;
+						}
+					}
+
+					elem = 0;
+
+				// If using innerHTML throws an exception, use the fallback method
+				} catch(e) {}
+			}
+
+			if ( elem ) {
+				this.empty().append( value );
+			}
+		}, null, value, arguments.length );
+	},
+
+	replaceWith: function() {
+		var
+			// Snapshot the DOM in case .domManip sweeps something relevant into its fragment
+			args = jQuery.map( this, function( elem ) {
+				return [ elem.nextSibling, elem.parentNode ];
+			}),
+			i = 0;
+
+		// Make the changes, replacing each context element with the new content
+		this.domManip( arguments, function( elem ) {
+			var next = args[ i++ ],
+				parent = args[ i++ ];
+
+			if ( parent ) {
+				// Don't use the snapshot next if it has moved (#13810)
+				if ( next && next.parentNode !== parent ) {
+					next = this.nextSibling;
+				}
+				jQuery( this ).remove();
+				parent.insertBefore( elem, next );
+			}
+		// Allow new content to include elements from the context set
+		}, true );
+
+		// Force removal if there was no new content (e.g., from empty arguments)
+		return i ? this : this.remove();
+	},
+
+	detach: function( selector ) {
+		return this.remove( selector, true );
+	},
+
+	domManip: function( args, callback, allowIntersection ) {
+
+		// Flatten any nested arrays
+		args = core_concat.apply( [], args );
+
+		var first, node, hasScripts,
+			scripts, doc, fragment,
+			i = 0,
+			l = this.length,
+			set = this,
+			iNoClone = l - 1,
+			value = args[0],
+			isFunction = jQuery.isFunction( value );
+
+		// We can't cloneNode fragments that contain checked, in WebKit
+		if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {
+			return this.each(function( index ) {
+				var self = set.eq( index );
+				if ( isFunction ) {
+					args[0] = value.call( this, index, self.html() );
+				}
+				self.domManip( args, callback, allowIntersection );
+			});
+		}
+
+		if ( l ) {
+			fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this );
+			first = fragment.firstChild;
+
+			if ( fragment.childNodes.length === 1 ) {
+				fragment = first;
+			}
+
+			if ( first ) {
+				scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+				hasScripts = scripts.length;
+
+				// Use the original fragment for the last item instead of the first because it can end up
+				// being emptied incorrectly in certain situations (#8070).
+				for ( ; i < l; i++ ) {
+					node = fragment;
+
+					if ( i !== iNoClone ) {
+						node = jQuery.clone( node, true, true );
+
+						// Keep references to cloned scripts for later restoration
+						if ( hasScripts ) {
+							jQuery.merge( scripts, getAll( node, "script" ) );
+						}
+					}
+
+					callback.call( this[i], node, i );
+				}
+
+				if ( hasScripts ) {
+					doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+					// Reenable scripts
+					jQuery.map( scripts, restoreScript );
+
+					// Evaluate executable scripts on first document insertion
+					for ( i = 0; i < hasScripts; i++ ) {
+						node = scripts[ i ];
+						if ( rscriptType.test( node.type || "" ) &&
+							!jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
+
+							if ( node.src ) {
+								// Hope ajax is available...
+								jQuery._evalUrl( node.src );
+							} else {
+								jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );
+							}
+						}
+					}
+				}
+
+				// Fix #11809: Avoid leaking memory
+				fragment = first = null;
+			}
+		}
+
+		return this;
+	}
+});
+
+// Support: IE<8
+// Manipulating tables requires a tbody
+function manipulationTarget( elem, content ) {
+	return jQuery.nodeName( elem, "table" ) &&
+		jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ?
+
+		elem.getElementsByTagName("tbody")[0] ||
+			elem.appendChild( elem.ownerDocument.createElement("tbody") ) :
+		elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+	elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type;
+	return elem;
+}
+function restoreScript( elem ) {
+	var match = rscriptTypeMasked.exec( elem.type );
+	if ( match ) {
+		elem.type = match[1];
+	} else {
+		elem.removeAttribute("type");
+	}
+	return elem;
+}
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+	var elem,
+		i = 0;
+	for ( ; (elem = elems[i]) != null; i++ ) {
+		jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
+	}
+}
+
+function cloneCopyEvent( src, dest ) {
+
+	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+		return;
+	}
+
+	var type, i, l,
+		oldData = jQuery._data( src ),
+		curData = jQuery._data( dest, oldData ),
+		events = oldData.events;
+
+	if ( events ) {
+		delete curData.handle;
+		curData.events = {};
+
+		for ( type in events ) {
+			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+				jQuery.event.add( dest, type, events[ type ][ i ] );
+			}
+		}
+	}
+
+	// make the cloned public data object a copy from the original
+	if ( curData.data ) {
+		curData.data = jQuery.extend( {}, curData.data );
+	}
+}
+
+function fixCloneNodeIssues( src, dest ) {
+	var nodeName, e, data;
+
+	// We do not need to do anything for non-Elements
+	if ( dest.nodeType !== 1 ) {
+		return;
+	}
+
+	nodeName = dest.nodeName.toLowerCase();
+
+	// IE6-8 copies events bound via attachEvent when using cloneNode.
+	if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) {
+		data = jQuery._data( dest );
+
+		for ( e in data.events ) {
+			jQuery.removeEvent( dest, e, data.handle );
+		}
+
+		// Event data gets referenced instead of copied if the expando gets copied too
+		dest.removeAttribute( jQuery.expando );
+	}
+
+	// IE blanks contents when cloning scripts, and tries to evaluate newly-set text
+	if ( nodeName === "script" && dest.text !== src.text ) {
+		disableScript( dest ).text = src.text;
+		restoreScript( dest );
+
+	// IE6-10 improperly clones children of object elements using classid.
+	// IE10 throws NoModificationAllowedError if parent is null, #12132.
+	} else if ( nodeName === "object" ) {
+		if ( dest.parentNode ) {
+			dest.outerHTML = src.outerHTML;
+		}
+
+		// This path appears unavoidable for IE9. When cloning an object
+		// element in IE9, the outerHTML strategy above is not sufficient.
+		// If the src has innerHTML and the destination does not,
+		// copy the src.innerHTML into the dest.innerHTML. #10324
+		if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {
+			dest.innerHTML = src.innerHTML;
+		}
+
+	} else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) {
+		// IE6-8 fails to persist the checked state of a cloned checkbox
+		// or radio button. Worse, IE6-7 fail to give the cloned element
+		// a checked appearance if the defaultChecked value isn't also set
+
+		dest.defaultChecked = dest.checked = src.checked;
+
+		// IE6-7 get confused and end up setting the value of a cloned
+		// checkbox/radio button to an empty string instead of "on"
+		if ( dest.value !== src.value ) {
+			dest.value = src.value;
+		}
+
+	// IE6-8 fails to return the selected option to the default selected
+	// state when cloning options
+	} else if ( nodeName === "option" ) {
+		dest.defaultSelected = dest.selected = src.defaultSelected;
+
+	// IE6-8 fails to set the defaultValue to the correct value when
+	// cloning other types of input fields
+	} else if ( nodeName === "input" || nodeName === "textarea" ) {
+		dest.defaultValue = src.defaultValue;
+	}
+}
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function( name, original ) {
+	jQuery.fn[ name ] = function( selector ) {
+		var elems,
+			i = 0,
+			ret = [],
+			insert = jQuery( selector ),
+			last = insert.length - 1;
+
+		for ( ; i <= last; i++ ) {
+			elems = i === last ? this : this.clone(true);
+			jQuery( insert[i] )[ original ]( elems );
+
+			// Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()
+			core_push.apply( ret, elems.get() );
+		}
+
+		return this.pushStack( ret );
+	};
+});
+
+function getAll( context, tag ) {
+	var elems, elem,
+		i = 0,
+		found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) :
+			typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) :
+			undefined;
+
+	if ( !found ) {
+		for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
+			if ( !tag || jQuery.nodeName( elem, tag ) ) {
+				found.push( elem );
+			} else {
+				jQuery.merge( found, getAll( elem, tag ) );
+			}
+		}
+	}
+
+	return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
+		jQuery.merge( [ context ], found ) :
+		found;
+}
+
+// Used in buildFragment, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+	if ( manipulation_rcheckableType.test( elem.type ) ) {
+		elem.defaultChecked = elem.checked;
+	}
+}
+
+jQuery.extend({
+	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+		var destElements, node, clone, i, srcElements,
+			inPage = jQuery.contains( elem.ownerDocument, elem );
+
+		if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+			clone = elem.cloneNode( true );
+
+		// IE<=8 does not properly clone detached, unknown element nodes
+		} else {
+			fragmentDiv.innerHTML = elem.outerHTML;
+			fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+		}
+
+		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+
+			// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
+			destElements = getAll( clone );
+			srcElements = getAll( elem );
+
+			// Fix all IE cloning issues
+			for ( i = 0; (node = srcElements[i]) != null; ++i ) {
+				// Ensure that the destination node is not null; Fixes #9587
+				if ( destElements[i] ) {
+					fixCloneNodeIssues( node, destElements[i] );
+				}
+			}
+		}
+
+		// Copy the events from the original to the clone
+		if ( dataAndEvents ) {
+			if ( deepDataAndEvents ) {
+				srcElements = srcElements || getAll( elem );
+				destElements = destElements || getAll( clone );
+
+				for ( i = 0; (node = srcElements[i]) != null; i++ ) {
+					cloneCopyEvent( node, destElements[i] );
+				}
+			} else {
+				cloneCopyEvent( elem, clone );
+			}
+		}
+
+		// Preserve script evaluation history
+		destElements = getAll( clone, "script" );
+		if ( destElements.length > 0 ) {
+			setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+		}
+
+		destElements = srcElements = node = null;
+
+		// Return the cloned set
+		return clone;
+	},
+
+	buildFragment: function( elems, context, scripts, selection ) {
+		var j, elem, contains,
+			tmp, tag, tbody, wrap,
+			l = elems.length,
+
+			// Ensure a safe fragment
+			safe = createSafeFragment( context ),
+
+			nodes = [],
+			i = 0;
+
+		for ( ; i < l; i++ ) {
+			elem = elems[ i ];
+
+			if ( elem || elem === 0 ) {
+
+				// Add nodes directly
+				if ( jQuery.type( elem ) === "object" ) {
+					jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+				// Convert non-html into a text node
+				} else if ( !rhtml.test( elem ) ) {
+					nodes.push( context.createTextNode( elem ) );
+
+				// Convert html into DOM nodes
+				} else {
+					tmp = tmp || safe.appendChild( context.createElement("div") );
+
+					// Deserialize a standard representation
+					tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+					wrap = wrapMap[ tag ] || wrapMap._default;
+
+					tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2];
+
+					// Descend through wrappers to the right content
+					j = wrap[0];
+					while ( j-- ) {
+						tmp = tmp.lastChild;
+					}
+
+					// Manually add leading whitespace removed by IE
+					if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+						nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );
+					}
+
+					// Remove IE's autoinserted <tbody> from table fragments
+					if ( !jQuery.support.tbody ) {
+
+						// String was a <table>, *may* have spurious <tbody>
+						elem = tag === "table" && !rtbody.test( elem ) ?
+							tmp.firstChild :
+
+							// String was a bare <thead> or <tfoot>
+							wrap[1] === "<table>" && !rtbody.test( elem ) ?
+								tmp :
+								0;
+
+						j = elem && elem.childNodes.length;
+						while ( j-- ) {
+							if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) {
+								elem.removeChild( tbody );
+							}
+						}
+					}
+
+					jQuery.merge( nodes, tmp.childNodes );
+
+					// Fix #12392 for WebKit and IE > 9
+					tmp.textContent = "";
+
+					// Fix #12392 for oldIE
+					while ( tmp.firstChild ) {
+						tmp.removeChild( tmp.firstChild );
+					}
+
+					// Remember the top-level container for proper cleanup
+					tmp = safe.lastChild;
+				}
+			}
+		}
+
+		// Fix #11356: Clear elements from fragment
+		if ( tmp ) {
+			safe.removeChild( tmp );
+		}
+
+		// Reset defaultChecked for any radios and checkboxes
+		// about to be appended to the DOM in IE 6/7 (#8060)
+		if ( !jQuery.support.appendChecked ) {
+			jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
+		}
+
+		i = 0;
+		while ( (elem = nodes[ i++ ]) ) {
+
+			// #4087 - If origin and destination elements are the same, and this is
+			// that element, do not do anything
+			if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
+				continue;
+			}
+
+			contains = jQuery.contains( elem.ownerDocument, elem );
+
+			// Append to fragment
+			tmp = getAll( safe.appendChild( elem ), "script" );
+
+			// Preserve script evaluation history
+			if ( contains ) {
+				setGlobalEval( tmp );
+			}
+
+			// Capture executables
+			if ( scripts ) {
+				j = 0;
+				while ( (elem = tmp[ j++ ]) ) {
+					if ( rscriptType.test( elem.type || "" ) ) {
+						scripts.push( elem );
+					}
+				}
+			}
+		}
+
+		tmp = null;
+
+		return safe;
+	},
+
+	cleanData: function( elems, /* internal */ acceptData ) {
+		var elem, type, id, data,
+			i = 0,
+			internalKey = jQuery.expando,
+			cache = jQuery.cache,
+			deleteExpando = jQuery.support.deleteExpando,
+			special = jQuery.event.special;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( acceptData || jQuery.acceptData( elem ) ) {
+
+				id = elem[ internalKey ];
+				data = id && cache[ id ];
+
+				if ( data ) {
+					if ( data.events ) {
+						for ( type in data.events ) {
+							if ( special[ type ] ) {
+								jQuery.event.remove( elem, type );
+
+							// This is a shortcut to avoid jQuery.event.remove's overhead
+							} else {
+								jQuery.removeEvent( elem, type, data.handle );
+							}
+						}
+					}
+
+					// Remove cache only if it was not already removed by jQuery.event.remove
+					if ( cache[ id ] ) {
+
+						delete cache[ id ];
+
+						// IE does not allow us to delete expando properties from nodes,
+						// nor does it have a removeAttribute function on Document nodes;
+						// we must handle all of these cases
+						if ( deleteExpando ) {
+							delete elem[ internalKey ];
+
+						} else if ( typeof elem.removeAttribute !== core_strundefined ) {
+							elem.removeAttribute( internalKey );
+
+						} else {
+							elem[ internalKey ] = null;
+						}
+
+						core_deletedIds.push( id );
+					}
+				}
+			}
+		}
+	},
+
+	_evalUrl: function( url ) {
+		return jQuery.ajax({
+			url: url,
+			type: "GET",
+			dataType: "script",
+			async: false,
+			global: false,
+			"throws": true
+		});
+	}
+});
+jQuery.fn.extend({
+	wrapAll: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapAll( html.call(this, i) );
+			});
+		}
+
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+			if ( this[0].parentNode ) {
+				wrap.insertBefore( this[0] );
+			}
+
+			wrap.map(function() {
+				var elem = this;
+
+				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+					elem = elem.firstChild;
+				}
+
+				return elem;
+			}).append( this );
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapInner( html.call(this, i) );
+			});
+		}
+
+		return this.each(function() {
+			var self = jQuery( this ),
+				contents = self.contents();
+
+			if ( contents.length ) {
+				contents.wrapAll( html );
+
+			} else {
+				self.append( html );
+			}
+		});
+	},
+
+	wrap: function( html ) {
+		var isFunction = jQuery.isFunction( html );
+
+		return this.each(function(i) {
+			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+		});
+	},
+
+	unwrap: function() {
+		return this.parent().each(function() {
+			if ( !jQuery.nodeName( this, "body" ) ) {
+				jQuery( this ).replaceWith( this.childNodes );
+			}
+		}).end();
+	}
+});
+var iframe, getStyles, curCSS,
+	ralpha = /alpha\([^)]*\)/i,
+	ropacity = /opacity\s*=\s*([^)]*)/,
+	rposition = /^(top|right|bottom|left)$/,
+	// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+	// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+	rmargin = /^margin/,
+	rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+	rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+	rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ),
+	elemdisplay = { BODY: "block" },
+
+	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+	cssNormalTransform = {
+		letterSpacing: 0,
+		fontWeight: 400
+	},
+
+	cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+	cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+	// shortcut for names that are not vendor prefixed
+	if ( name in style ) {
+		return name;
+	}
+
+	// check for vendor prefixed names
+	var capName = name.charAt(0).toUpperCase() + name.slice(1),
+		origName = name,
+		i = cssPrefixes.length;
+
+	while ( i-- ) {
+		name = cssPrefixes[ i ] + capName;
+		if ( name in style ) {
+			return name;
+		}
+	}
+
+	return origName;
+}
+
+function isHidden( elem, el ) {
+	// isHidden might be called from jQuery#filter function;
+	// in that case, element will be second argument
+	elem = el || elem;
+	return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+	var display, elem, hidden,
+		values = [],
+		index = 0,
+		length = elements.length;
+
+	for ( ; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+
+		values[ index ] = jQuery._data( elem, "olddisplay" );
+		display = elem.style.display;
+		if ( show ) {
+			// Reset the inline display of this element to learn if it is
+			// being hidden by cascaded rules or not
+			if ( !values[ index ] && display === "none" ) {
+				elem.style.display = "";
+			}
+
+			// Set elements which have been overridden with display: none
+			// in a stylesheet to whatever the default browser style is
+			// for such an element
+			if ( elem.style.display === "" && isHidden( elem ) ) {
+				values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+			}
+		} else {
+
+			if ( !values[ index ] ) {
+				hidden = isHidden( elem );
+
+				if ( display && display !== "none" || !hidden ) {
+					jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
+				}
+			}
+		}
+	}
+
+	// Set the display of most of the elements in a second loop
+	// to avoid the constant reflow
+	for ( index = 0; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+			elem.style.display = show ? values[ index ] || "" : "none";
+		}
+	}
+
+	return elements;
+}
+
+jQuery.fn.extend({
+	css: function( name, value ) {
+		return jQuery.access( this, function( elem, name, value ) {
+			var len, styles,
+				map = {},
+				i = 0;
+
+			if ( jQuery.isArray( name ) ) {
+				styles = getStyles( elem );
+				len = name.length;
+
+				for ( ; i < len; i++ ) {
+					map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+				}
+
+				return map;
+			}
+
+			return value !== undefined ?
+				jQuery.style( elem, name, value ) :
+				jQuery.css( elem, name );
+		}, name, value, arguments.length > 1 );
+	},
+	show: function() {
+		return showHide( this, true );
+	},
+	hide: function() {
+		return showHide( this );
+	},
+	toggle: function( state ) {
+		var bool = typeof state === "boolean";
+
+		return this.each(function() {
+			if ( bool ? state : isHidden( this ) ) {
+				jQuery( this ).show();
+			} else {
+				jQuery( this ).hide();
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	// Add in style property hooks for overriding the default
+	// behavior of getting and setting a style property
+	cssHooks: {
+		opacity: {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// We should always get a number back from opacity
+					var ret = curCSS( elem, "opacity" );
+					return ret === "" ? "1" : ret;
+				}
+			}
+		}
+	},
+
+	// Don't automatically add "px" to these possibly-unitless properties
+	cssNumber: {
+		"columnCount": true,
+		"fillOpacity": true,
+		"fontWeight": true,
+		"lineHeight": true,
+		"opacity": true,
+		"orphans": true,
+		"widows": true,
+		"zIndex": true,
+		"zoom": true
+	},
+
+	// Add in properties whose names you wish to fix before
+	// setting or getting the value
+	cssProps: {
+		// normalize float css property
+		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+	},
+
+	// Get and set the style property on a DOM Node
+	style: function( elem, name, value, extra ) {
+		// Don't set styles on text and comment nodes
+		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+			return;
+		}
+
+		// Make sure that we're working with the right name
+		var ret, type, hooks,
+			origName = jQuery.camelCase( name ),
+			style = elem.style;
+
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// Check if we're setting a value
+		if ( value !== undefined ) {
+			type = typeof value;
+
+			// convert relative number strings (+= or -=) to relative numbers. #7345
+			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+				// Fixes bug #9237
+				type = "number";
+			}
+
+			// Make sure that NaN and null values aren't set. See: #7116
+			if ( value == null || type === "number" && isNaN( value ) ) {
+				return;
+			}
+
+			// If a number was passed in, add 'px' to the (except for certain CSS properties)
+			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+				value += "px";
+			}
+
+			// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
+			// but it would mean to define eight (for every problematic property) identical functions
+			if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) {
+				style[ name ] = "inherit";
+			}
+
+			// If a hook was provided, use that value, otherwise just set the specified value
+			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+
+				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+				// Fixes bug #5509
+				try {
+					style[ name ] = value;
+				} catch(e) {}
+			}
+
+		} else {
+			// If a hook was provided get the non-computed value from there
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+				return ret;
+			}
+
+			// Otherwise just get the value from the style object
+			return style[ name ];
+		}
+	},
+
+	css: function( elem, name, extra, styles ) {
+		var num, val, hooks,
+			origName = jQuery.camelCase( name );
+
+		// Make sure that we're working with the right name
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// If a hook was provided get the computed value from there
+		if ( hooks && "get" in hooks ) {
+			val = hooks.get( elem, true, extra );
+		}
+
+		// Otherwise, if a way to get the computed value exists, use that
+		if ( val === undefined ) {
+			val = curCSS( elem, name, styles );
+		}
+
+		//convert "normal" to computed value
+		if ( val === "normal" && name in cssNormalTransform ) {
+			val = cssNormalTransform[ name ];
+		}
+
+		// Return, converting to number if forced or a qualifier was provided and val looks numeric
+		if ( extra === "" || extra ) {
+			num = parseFloat( val );
+			return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
+		}
+		return val;
+	}
+});
+
+// NOTE: we've included the "window" in window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+	getStyles = function( elem ) {
+		return window.getComputedStyle( elem, null );
+	};
+
+	curCSS = function( elem, name, _computed ) {
+		var width, minWidth, maxWidth,
+			computed = _computed || getStyles( elem ),
+
+			// getPropertyValue is only needed for .css('filter') in IE9, see #12537
+			ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined,
+			style = elem.style;
+
+		if ( computed ) {
+
+			if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+				ret = jQuery.style( elem, name );
+			}
+
+			// A tribute to the "awesome hack by Dean Edwards"
+			// Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+			// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+			// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+			if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+
+				// Remember the original values
+				width = style.width;
+				minWidth = style.minWidth;
+				maxWidth = style.maxWidth;
+
+				// Put in the new values to get a computed value out
+				style.minWidth = style.maxWidth = style.width = ret;
+				ret = computed.width;
+
+				// Revert the changed values
+				style.width = width;
+				style.minWidth = minWidth;
+				style.maxWidth = maxWidth;
+			}
+		}
+
+		return ret;
+	};
+} else if ( document.documentElement.currentStyle ) {
+	getStyles = function( elem ) {
+		return elem.currentStyle;
+	};
+
+	curCSS = function( elem, name, _computed ) {
+		var left, rs, rsLeft,
+			computed = _computed || getStyles( elem ),
+			ret = computed ? computed[ name ] : undefined,
+			style = elem.style;
+
+		// Avoid setting ret to empty string here
+		// so we don't default to auto
+		if ( ret == null && style && style[ name ] ) {
+			ret = style[ name ];
+		}
+
+		// From the awesome hack by Dean Edwards
+		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+		// If we're not dealing with a regular pixel number
+		// but a number that has a weird ending, we need to convert it to pixels
+		// but not position css attributes, as those are proportional to the parent element instead
+		// and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+		if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+			// Remember the original values
+			left = style.left;
+			rs = elem.runtimeStyle;
+			rsLeft = rs && rs.left;
+
+			// Put in the new values to get a computed value out
+			if ( rsLeft ) {
+				rs.left = elem.currentStyle.left;
+			}
+			style.left = name === "fontSize" ? "1em" : ret;
+			ret = style.pixelLeft + "px";
+
+			// Revert the changed values
+			style.left = left;
+			if ( rsLeft ) {
+				rs.left = rsLeft;
+			}
+		}
+
+		return ret === "" ? "auto" : ret;
+	};
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+	var matches = rnumsplit.exec( value );
+	return matches ?
+		// Guard against undefined "subtract", e.g., when used as in cssHooks
+		Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+		value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
+	var i = extra === ( isBorderBox ? "border" : "content" ) ?
+		// If we already have the right measurement, avoid augmentation
+		4 :
+		// Otherwise initialize for horizontal or vertical properties
+		name === "width" ? 1 : 0,
+
+		val = 0;
+
+	for ( ; i < 4; i += 2 ) {
+		// both box models exclude margin, so add it if we want it
+		if ( extra === "margin" ) {
+			val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+		}
+
+		if ( isBorderBox ) {
+			// border-box includes padding, so remove it if we want content
+			if ( extra === "content" ) {
+				val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+			}
+
+			// at this point, extra isn't border nor margin, so remove border
+			if ( extra !== "margin" ) {
+				val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+			}
+		} else {
+			// at this point, extra isn't content, so add padding
+			val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+			// at this point, extra isn't content nor padding, so add border
+			if ( extra !== "padding" ) {
+				val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+			}
+		}
+	}
+
+	return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+	// Start with offset property, which is equivalent to the border-box value
+	var valueIsBorderBox = true,
+		val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+		styles = getStyles( elem ),
+		isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+	// some non-html elements return undefined for offsetWidth, so check for null/undefined
+	// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+	// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+	if ( val <= 0 || val == null ) {
+		// Fall back to computed then uncomputed css if necessary
+		val = curCSS( elem, name, styles );
+		if ( val < 0 || val == null ) {
+			val = elem.style[ name ];
+		}
+
+		// Computed unit is not pixels. Stop here and return.
+		if ( rnumnonpx.test(val) ) {
+			return val;
+		}
+
+		// we need the check for style in case a browser which returns unreliable values
+		// for getComputedStyle silently falls back to the reliable elem.style
+		valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+		// Normalize "", auto, and prepare for extra
+		val = parseFloat( val ) || 0;
+	}
+
+	// use the active box-sizing model to add/subtract irrelevant styles
+	return ( val +
+		augmentWidthOrHeight(
+			elem,
+			name,
+			extra || ( isBorderBox ? "border" : "content" ),
+			valueIsBorderBox,
+			styles
+		)
+	) + "px";
+}
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+	var doc = document,
+		display = elemdisplay[ nodeName ];
+
+	if ( !display ) {
+		display = actualDisplay( nodeName, doc );
+
+		// If the simple way fails, read from inside an iframe
+		if ( display === "none" || !display ) {
+			// Use the already-created iframe if possible
+			iframe = ( iframe ||
+				jQuery("<iframe frameborder='0' width='0' height='0'/>")
+				.css( "cssText", "display:block !important" )
+			).appendTo( doc.documentElement );
+
+			// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
+			doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document;
+			doc.write("<!doctype html><html><body>");
+			doc.close();
+
+			display = actualDisplay( nodeName, doc );
+			iframe.detach();
+		}
+
+		// Store the correct default display
+		elemdisplay[ nodeName ] = display;
+	}
+
+	return display;
+}
+
+// Called ONLY from within css_defaultDisplay
+function actualDisplay( name, doc ) {
+	var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
+		display = jQuery.css( elem[0], "display" );
+	elem.remove();
+	return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+	jQuery.cssHooks[ name ] = {
+		get: function( elem, computed, extra ) {
+			if ( computed ) {
+				// certain elements can have dimension info if we invisibly show them
+				// however, it must have a current display style that would benefit from this
+				return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ?
+					jQuery.swap( elem, cssShow, function() {
+						return getWidthOrHeight( elem, name, extra );
+					}) :
+					getWidthOrHeight( elem, name, extra );
+			}
+		},
+
+		set: function( elem, value, extra ) {
+			var styles = extra && getStyles( elem );
+			return setPositiveNumber( elem, value, extra ?
+				augmentWidthOrHeight(
+					elem,
+					name,
+					extra,
+					jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+					styles
+				) : 0
+			);
+		}
+	};
+});
+
+if ( !jQuery.support.opacity ) {
+	jQuery.cssHooks.opacity = {
+		get: function( elem, computed ) {
+			// IE uses filters for opacity
+			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+				( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+				computed ? "1" : "";
+		},
+
+		set: function( elem, value ) {
+			var style = elem.style,
+				currentStyle = elem.currentStyle,
+				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+				filter = currentStyle && currentStyle.filter || style.filter || "";
+
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			style.zoom = 1;
+
+			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+			// if value === "", then remove inline opacity #12685
+			if ( ( value >= 1 || value === "" ) &&
+					jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+					style.removeAttribute ) {
+
+				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+				// if "filter:" is present at all, clearType is disabled, we want to avoid this
+				// style.removeAttribute is IE Only, but so apparently is this code path...
+				style.removeAttribute( "filter" );
+
+				// if there is no filter style applied in a css rule or unset inline opacity, we are done
+				if ( value === "" || currentStyle && !currentStyle.filter ) {
+					return;
+				}
+			}
+
+			// otherwise, set new filter values
+			style.filter = ralpha.test( filter ) ?
+				filter.replace( ralpha, opacity ) :
+				filter + " " + opacity;
+		}
+	};
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+	if ( !jQuery.support.reliableMarginRight ) {
+		jQuery.cssHooks.marginRight = {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+					// Work around by temporarily setting element display to inline-block
+					return jQuery.swap( elem, { "display": "inline-block" },
+						curCSS, [ elem, "marginRight" ] );
+				}
+			}
+		};
+	}
+
+	// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+	// getComputedStyle returns percent when specified for top/left/bottom/right
+	// rather than make the css module depend on the offset module, we just check for it here
+	if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+		jQuery.each( [ "top", "left" ], function( i, prop ) {
+			jQuery.cssHooks[ prop ] = {
+				get: function( elem, computed ) {
+					if ( computed ) {
+						computed = curCSS( elem, prop );
+						// if curCSS returns percentage, fallback to offset
+						return rnumnonpx.test( computed ) ?
+							jQuery( elem ).position()[ prop ] + "px" :
+							computed;
+					}
+				}
+			};
+		});
+	}
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.hidden = function( elem ) {
+		// Support: Opera <= 12.12
+		// Opera reports offsetWidths and offsetHeights less than zero on some elements
+		return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||
+			(!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none");
+	};
+
+	jQuery.expr.filters.visible = function( elem ) {
+		return !jQuery.expr.filters.hidden( elem );
+	};
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+	margin: "",
+	padding: "",
+	border: "Width"
+}, function( prefix, suffix ) {
+	jQuery.cssHooks[ prefix + suffix ] = {
+		expand: function( value ) {
+			var i = 0,
+				expanded = {},
+
+				// assumes a single number if not a string
+				parts = typeof value === "string" ? value.split(" ") : [ value ];
+
+			for ( ; i < 4; i++ ) {
+				expanded[ prefix + cssExpand[ i ] + suffix ] =
+					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+			}
+
+			return expanded;
+		}
+	};
+
+	if ( !rmargin.test( prefix ) ) {
+		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+	}
+});
+var r20 = /%20/g,
+	rbracket = /\[\]$/,
+	rCRLF = /\r?\n/g,
+	rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+	rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+jQuery.fn.extend({
+	serialize: function() {
+		return jQuery.param( this.serializeArray() );
+	},
+	serializeArray: function() {
+		return this.map(function(){
+			// Can add propHook for "elements" to filter or add form elements
+			var elements = jQuery.prop( this, "elements" );
+			return elements ? jQuery.makeArray( elements ) : this;
+		})
+		.filter(function(){
+			var type = this.type;
+			// Use .is(":disabled") so that fieldset[disabled] works
+			return this.name && !jQuery( this ).is( ":disabled" ) &&
+				rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+				( this.checked || !manipulation_rcheckableType.test( type ) );
+		})
+		.map(function( i, elem ){
+			var val = jQuery( this ).val();
+
+			return val == null ?
+				null :
+				jQuery.isArray( val ) ?
+					jQuery.map( val, function( val ){
+						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+					}) :
+					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+		}).get();
+	}
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+	var prefix,
+		s = [],
+		add = function( key, value ) {
+			// If value is a function, invoke it and return its value
+			value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+			s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+		};
+
+	// Set traditional to true for jQuery <= 1.3.2 behavior.
+	if ( traditional === undefined ) {
+		traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+	}
+
+	// If an array was passed in, assume that it is an array of form elements.
+	if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+		// Serialize the form elements
+		jQuery.each( a, function() {
+			add( this.name, this.value );
+		});
+
+	} else {
+		// If traditional, encode the "old" way (the way 1.3.2 or older
+		// did it), otherwise encode params recursively.
+		for ( prefix in a ) {
+			buildParams( prefix, a[ prefix ], traditional, add );
+		}
+	}
+
+	// Return the resulting serialization
+	return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+	var name;
+
+	if ( jQuery.isArray( obj ) ) {
+		// Serialize array item.
+		jQuery.each( obj, function( i, v ) {
+			if ( traditional || rbracket.test( prefix ) ) {
+				// Treat each array item as a scalar.
+				add( prefix, v );
+
+			} else {
+				// Item is non-scalar (array or object), encode its numeric index.
+				buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+			}
+		});
+
+	} else if ( !traditional && jQuery.type( obj ) === "object" ) {
+		// Serialize object item.
+		for ( name in obj ) {
+			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+		}
+
+	} else {
+		// Serialize scalar item.
+		add( prefix, obj );
+	}
+}
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+	// Handle event binding
+	jQuery.fn[ name ] = function( data, fn ) {
+		return arguments.length > 0 ?
+			this.on( name, null, data, fn ) :
+			this.trigger( name );
+	};
+});
+
+jQuery.fn.extend({
+	hover: function( fnOver, fnOut ) {
+		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+	},
+
+	bind: function( types, data, fn ) {
+		return this.on( types, null, data, fn );
+	},
+	unbind: function( types, fn ) {
+		return this.off( types, null, fn );
+	},
+
+	delegate: function( selector, types, data, fn ) {
+		return this.on( types, selector, data, fn );
+	},
+	undelegate: function( selector, types, fn ) {
+		// ( namespace ) or ( selector, types [, fn] )
+		return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+	}
+});
+var
+	// Document location
+	ajaxLocParts,
+	ajaxLocation,
+	ajax_nonce = jQuery.now(),
+
+	ajax_rquery = /\?/,
+	rhash = /#.*$/,
+	rts = /([?&])_=[^&]*/,
+	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+	// #7653, #8125, #8152: local protocol detection
+	rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+	rnoContent = /^(?:GET|HEAD)$/,
+	rprotocol = /^\/\//,
+	rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+	// Keep a copy of the old load method
+	_load = jQuery.fn.load,
+
+	/* Prefilters
+	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+	 * 2) These are called:
+	 *    - BEFORE asking for a transport
+	 *    - AFTER param serialization (s.data is a string if s.processData is true)
+	 * 3) key is the dataType
+	 * 4) the catchall symbol "*" can be used
+	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+	 */
+	prefilters = {},
+
+	/* Transports bindings
+	 * 1) key is the dataType
+	 * 2) the catchall symbol "*" can be used
+	 * 3) selection will start with transport dataType and THEN go to "*" if needed
+	 */
+	transports = {},
+
+	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+	allTypes = "*/".concat("*");
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+	ajaxLocation = location.href;
+} catch( e ) {
+	// Use the href attribute of an A element
+	// since IE will modify it given document.location
+	ajaxLocation = document.createElement( "a" );
+	ajaxLocation.href = "";
+	ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+	// dataTypeExpression is optional and defaults to "*"
+	return function( dataTypeExpression, func ) {
+
+		if ( typeof dataTypeExpression !== "string" ) {
+			func = dataTypeExpression;
+			dataTypeExpression = "*";
+		}
+
+		var dataType,
+			i = 0,
+			dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || [];
+
+		if ( jQuery.isFunction( func ) ) {
+			// For each dataType in the dataTypeExpression
+			while ( (dataType = dataTypes[i++]) ) {
+				// Prepend if requested
+				if ( dataType[0] === "+" ) {
+					dataType = dataType.slice( 1 ) || "*";
+					(structure[ dataType ] = structure[ dataType ] || []).unshift( func );
+
+				// Otherwise append
+				} else {
+					(structure[ dataType ] = structure[ dataType ] || []).push( func );
+				}
+			}
+		}
+	};
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+	var inspected = {},
+		seekingTransport = ( structure === transports );
+
+	function inspect( dataType ) {
+		var selected;
+		inspected[ dataType ] = true;
+		jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+			var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+			if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+				options.dataTypes.unshift( dataTypeOrTransport );
+				inspect( dataTypeOrTransport );
+				return false;
+			} else if ( seekingTransport ) {
+				return !( selected = dataTypeOrTransport );
+			}
+		});
+		return selected;
+	}
+
+	return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+	var deep, key,
+		flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+	for ( key in src ) {
+		if ( src[ key ] !== undefined ) {
+			( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
+		}
+	}
+	if ( deep ) {
+		jQuery.extend( true, target, deep );
+	}
+
+	return target;
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+	if ( typeof url !== "string" && _load ) {
+		return _load.apply( this, arguments );
+	}
+
+	var selector, response, type,
+		self = this,
+		off = url.indexOf(" ");
+
+	if ( off >= 0 ) {
+		selector = url.slice( off, url.length );
+		url = url.slice( 0, off );
+	}
+
+	// If it's a function
+	if ( jQuery.isFunction( params ) ) {
+
+		// We assume that it's the callback
+		callback = params;
+		params = undefined;
+
+	// Otherwise, build a param string
+	} else if ( params && typeof params === "object" ) {
+		type = "POST";
+	}
+
+	// If we have elements to modify, make the request
+	if ( self.length > 0 ) {
+		jQuery.ajax({
+			url: url,
+
+			// if "type" variable is undefined, then "GET" method will be used
+			type: type,
+			dataType: "html",
+			data: params
+		}).done(function( responseText ) {
+
+			// Save response for use in complete callback
+			response = arguments;
+
+			self.html( selector ?
+
+				// If a selector was specified, locate the right elements in a dummy div
+				// Exclude scripts to avoid IE 'Permission Denied' errors
+				jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+				// Otherwise use the full result
+				responseText );
+
+		}).complete( callback && function( jqXHR, status ) {
+			self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+		});
+	}
+
+	return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){
+	jQuery.fn[ type ] = function( fn ){
+		return this.on( type, fn );
+	};
+});
+
+jQuery.extend({
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Last-Modified header cache for next request
+	lastModified: {},
+	etag: {},
+
+	ajaxSettings: {
+		url: ajaxLocation,
+		type: "GET",
+		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+		global: true,
+		processData: true,
+		async: true,
+		contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+		/*
+		timeout: 0,
+		data: null,
+		dataType: null,
+		username: null,
+		password: null,
+		cache: null,
+		throws: false,
+		traditional: false,
+		headers: {},
+		*/
+
+		accepts: {
+			"*": allTypes,
+			text: "text/plain",
+			html: "text/html",
+			xml: "application/xml, text/xml",
+			json: "application/json, text/javascript"
+		},
+
+		contents: {
+			xml: /xml/,
+			html: /html/,
+			json: /json/
+		},
+
+		responseFields: {
+			xml: "responseXML",
+			text: "responseText",
+			json: "responseJSON"
+		},
+
+		// Data converters
+		// Keys separate source (or catchall "*") and destination types with a single space
+		converters: {
+
+			// Convert anything to text
+			"* text": String,
+
+			// Text to html (true = no transformation)
+			"text html": true,
+
+			// Evaluate text as a json expression
+			"text json": jQuery.parseJSON,
+
+			// Parse text as xml
+			"text xml": jQuery.parseXML
+		},
+
+		// For options that shouldn't be deep extended:
+		// you can add your own custom options here if
+		// and when you create one that shouldn't be
+		// deep extended (see ajaxExtend)
+		flatOptions: {
+			url: true,
+			context: true
+		}
+	},
+
+	// Creates a full fledged settings object into target
+	// with both ajaxSettings and settings fields.
+	// If target is omitted, writes into ajaxSettings.
+	ajaxSetup: function( target, settings ) {
+		return settings ?
+
+			// Building a settings object
+			ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+			// Extending ajaxSettings
+			ajaxExtend( jQuery.ajaxSettings, target );
+	},
+
+	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+	ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+	// Main method
+	ajax: function( url, options ) {
+
+		// If url is an object, simulate pre-1.5 signature
+		if ( typeof url === "object" ) {
+			options = url;
+			url = undefined;
+		}
+
+		// Force options to be an object
+		options = options || {};
+
+		var // Cross-domain detection vars
+			parts,
+			// Loop variable
+			i,
+			// URL without anti-cache param
+			cacheURL,
+			// Response headers as string
+			responseHeadersString,
+			// timeout handle
+			timeoutTimer,
+
+			// To know if global events are to be dispatched
+			fireGlobals,
+
+			transport,
+			// Response headers
+			responseHeaders,
+			// Create the final options object
+			s = jQuery.ajaxSetup( {}, options ),
+			// Callbacks context
+			callbackContext = s.context || s,
+			// Context for global events is callbackContext if it is a DOM node or jQuery collection
+			globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
+				jQuery( callbackContext ) :
+				jQuery.event,
+			// Deferreds
+			deferred = jQuery.Deferred(),
+			completeDeferred = jQuery.Callbacks("once memory"),
+			// Status-dependent callbacks
+			statusCode = s.statusCode || {},
+			// Headers (they are sent all at once)
+			requestHeaders = {},
+			requestHeadersNames = {},
+			// The jqXHR state
+			state = 0,
+			// Default abort message
+			strAbort = "canceled",
+			// Fake xhr
+			jqXHR = {
+				readyState: 0,
+
+				// Builds headers hashtable if needed
+				getResponseHeader: function( key ) {
+					var match;
+					if ( state === 2 ) {
+						if ( !responseHeaders ) {
+							responseHeaders = {};
+							while ( (match = rheaders.exec( responseHeadersString )) ) {
+								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+							}
+						}
+						match = responseHeaders[ key.toLowerCase() ];
+					}
+					return match == null ? null : match;
+				},
+
+				// Raw string
+				getAllResponseHeaders: function() {
+					return state === 2 ? responseHeadersString : null;
+				},
+
+				// Caches the header
+				setRequestHeader: function( name, value ) {
+					var lname = name.toLowerCase();
+					if ( !state ) {
+						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+						requestHeaders[ name ] = value;
+					}
+					return this;
+				},
+
+				// Overrides response content-type header
+				overrideMimeType: function( type ) {
+					if ( !state ) {
+						s.mimeType = type;
+					}
+					return this;
+				},
+
+				// Status-dependent callbacks
+				statusCode: function( map ) {
+					var code;
+					if ( map ) {
+						if ( state < 2 ) {
+							for ( code in map ) {
+								// Lazy-add the new callback in a way that preserves old ones
+								statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+							}
+						} else {
+							// Execute the appropriate callbacks
+							jqXHR.always( map[ jqXHR.status ] );
+						}
+					}
+					return this;
+				},
+
+				// Cancel the request
+				abort: function( statusText ) {
+					var finalText = statusText || strAbort;
+					if ( transport ) {
+						transport.abort( finalText );
+					}
+					done( 0, finalText );
+					return this;
+				}
+			};
+
+		// Attach deferreds
+		deferred.promise( jqXHR ).complete = completeDeferred.add;
+		jqXHR.success = jqXHR.done;
+		jqXHR.error = jqXHR.fail;
+
+		// Remove hash character (#7531: and string promotion)
+		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+		// Handle falsy url in the settings object (#10093: consistency with old signature)
+		// We also use the url parameter if available
+		s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+		// Alias method option to type as per ticket #12004
+		s.type = options.method || options.type || s.method || s.type;
+
+		// Extract dataTypes list
+		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""];
+
+		// A cross-domain request is in order when we have a protocol:host:port mismatch
+		if ( s.crossDomain == null ) {
+			parts = rurl.exec( s.url.toLowerCase() );
+			s.crossDomain = !!( parts &&
+				( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+					( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
+						( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
+			);
+		}
+
+		// Convert data if not already a string
+		if ( s.data && s.processData && typeof s.data !== "string" ) {
+			s.data = jQuery.param( s.data, s.traditional );
+		}
+
+		// Apply prefilters
+		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+		// If request was aborted inside a prefilter, stop there
+		if ( state === 2 ) {
+			return jqXHR;
+		}
+
+		// We can fire global events as of now if asked to
+		fireGlobals = s.global;
+
+		// Watch for a new set of requests
+		if ( fireGlobals && jQuery.active++ === 0 ) {
+			jQuery.event.trigger("ajaxStart");
+		}
+
+		// Uppercase the type
+		s.type = s.type.toUpperCase();
+
+		// Determine if request has content
+		s.hasContent = !rnoContent.test( s.type );
+
+		// Save the URL in case we're toying with the If-Modified-Since
+		// and/or If-None-Match header later on
+		cacheURL = s.url;
+
+		// More options handling for requests with no content
+		if ( !s.hasContent ) {
+
+			// If data is available, append data to url
+			if ( s.data ) {
+				cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+				// #9682: remove data so that it's not used in an eventual retry
+				delete s.data;
+			}
+
+			// Add anti-cache in url if needed
+			if ( s.cache === false ) {
+				s.url = rts.test( cacheURL ) ?
+
+					// If there is already a '_' parameter, set its value
+					cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
+
+					// Otherwise add one to the end
+					cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
+			}
+		}
+
+		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+		if ( s.ifModified ) {
+			if ( jQuery.lastModified[ cacheURL ] ) {
+				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+			}
+			if ( jQuery.etag[ cacheURL ] ) {
+				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+			}
+		}
+
+		// Set the correct header, if data is being sent
+		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+			jqXHR.setRequestHeader( "Content-Type", s.contentType );
+		}
+
+		// Set the Accepts header for the server, depending on the dataType
+		jqXHR.setRequestHeader(
+			"Accept",
+			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+				s.accepts[ "*" ]
+		);
+
+		// Check for headers option
+		for ( i in s.headers ) {
+			jqXHR.setRequestHeader( i, s.headers[ i ] );
+		}
+
+		// Allow custom headers/mimetypes and early abort
+		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+			// Abort if not done already and return
+			return jqXHR.abort();
+		}
+
+		// aborting is no longer a cancellation
+		strAbort = "abort";
+
+		// Install callbacks on deferreds
+		for ( i in { success: 1, error: 1, complete: 1 } ) {
+			jqXHR[ i ]( s[ i ] );
+		}
+
+		// Get transport
+		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+		// If no transport, we auto-abort
+		if ( !transport ) {
+			done( -1, "No Transport" );
+		} else {
+			jqXHR.readyState = 1;
+
+			// Send global event
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+			}
+			// Timeout
+			if ( s.async && s.timeout > 0 ) {
+				timeoutTimer = setTimeout(function() {
+					jqXHR.abort("timeout");
+				}, s.timeout );
+			}
+
+			try {
+				state = 1;
+				transport.send( requestHeaders, done );
+			} catch ( e ) {
+				// Propagate exception as error if not done
+				if ( state < 2 ) {
+					done( -1, e );
+				// Simply rethrow otherwise
+				} else {
+					throw e;
+				}
+			}
+		}
+
+		// Callback for when everything is done
+		function done( status, nativeStatusText, responses, headers ) {
+			var isSuccess, success, error, response, modified,
+				statusText = nativeStatusText;
+
+			// Called once
+			if ( state === 2 ) {
+				return;
+			}
+
+			// State is "done" now
+			state = 2;
+
+			// Clear timeout if it exists
+			if ( timeoutTimer ) {
+				clearTimeout( timeoutTimer );
+			}
+
+			// Dereference transport for early garbage collection
+			// (no matter how long the jqXHR object will be used)
+			transport = undefined;
+
+			// Cache response headers
+			responseHeadersString = headers || "";
+
+			// Set readyState
+			jqXHR.readyState = status > 0 ? 4 : 0;
+
+			// Determine if successful
+			isSuccess = status >= 200 && status < 300 || status === 304;
+
+			// Get response data
+			if ( responses ) {
+				response = ajaxHandleResponses( s, jqXHR, responses );
+			}
+
+			// Convert no matter what (that way responseXXX fields are always set)
+			response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+			// If successful, handle type chaining
+			if ( isSuccess ) {
+
+				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+				if ( s.ifModified ) {
+					modified = jqXHR.getResponseHeader("Last-Modified");
+					if ( modified ) {
+						jQuery.lastModified[ cacheURL ] = modified;
+					}
+					modified = jqXHR.getResponseHeader("etag");
+					if ( modified ) {
+						jQuery.etag[ cacheURL ] = modified;
+					}
+				}
+
+				// if no content
+				if ( status === 204 || s.type === "HEAD" ) {
+					statusText = "nocontent";
+
+				// if not modified
+				} else if ( status === 304 ) {
+					statusText = "notmodified";
+
+				// If we have data, let's convert it
+				} else {
+					statusText = response.state;
+					success = response.data;
+					error = response.error;
+					isSuccess = !error;
+				}
+			} else {
+				// We extract error from statusText
+				// then normalize statusText and status for non-aborts
+				error = statusText;
+				if ( status || !statusText ) {
+					statusText = "error";
+					if ( status < 0 ) {
+						status = 0;
+					}
+				}
+			}
+
+			// Set data for the fake xhr object
+			jqXHR.status = status;
+			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+			// Success/Error
+			if ( isSuccess ) {
+				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+			} else {
+				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+			}
+
+			// Status-dependent callbacks
+			jqXHR.statusCode( statusCode );
+			statusCode = undefined;
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+					[ jqXHR, s, isSuccess ? success : error ] );
+			}
+
+			// Complete
+			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+				// Handle the global AJAX counter
+				if ( !( --jQuery.active ) ) {
+					jQuery.event.trigger("ajaxStop");
+				}
+			}
+		}
+
+		return jqXHR;
+	},
+
+	getJSON: function( url, data, callback ) {
+		return jQuery.get( url, data, callback, "json" );
+	},
+
+	getScript: function( url, callback ) {
+		return jQuery.get( url, undefined, callback, "script" );
+	}
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+	jQuery[ method ] = function( url, data, callback, type ) {
+		// shift arguments if data argument was omitted
+		if ( jQuery.isFunction( data ) ) {
+			type = type || callback;
+			callback = data;
+			data = undefined;
+		}
+
+		return jQuery.ajax({
+			url: url,
+			type: method,
+			dataType: type,
+			data: data,
+			success: callback
+		});
+	};
+});
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+	var firstDataType, ct, finalDataType, type,
+		contents = s.contents,
+		dataTypes = s.dataTypes;
+
+	// Remove auto dataType and get content-type in the process
+	while( dataTypes[ 0 ] === "*" ) {
+		dataTypes.shift();
+		if ( ct === undefined ) {
+			ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
+		}
+	}
+
+	// Check if we're dealing with a known content-type
+	if ( ct ) {
+		for ( type in contents ) {
+			if ( contents[ type ] && contents[ type ].test( ct ) ) {
+				dataTypes.unshift( type );
+				break;
+			}
+		}
+	}
+
+	// Check to see if we have a response for the expected dataType
+	if ( dataTypes[ 0 ] in responses ) {
+		finalDataType = dataTypes[ 0 ];
+	} else {
+		// Try convertible dataTypes
+		for ( type in responses ) {
+			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+				finalDataType = type;
+				break;
+			}
+			if ( !firstDataType ) {
+				firstDataType = type;
+			}
+		}
+		// Or just use first one
+		finalDataType = finalDataType || firstDataType;
+	}
+
+	// If we found a dataType
+	// We add the dataType to the list if needed
+	// and return the corresponding response
+	if ( finalDataType ) {
+		if ( finalDataType !== dataTypes[ 0 ] ) {
+			dataTypes.unshift( finalDataType );
+		}
+		return responses[ finalDataType ];
+	}
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+	var conv2, current, conv, tmp, prev,
+		converters = {},
+		// Work with a copy of dataTypes in case we need to modify it for conversion
+		dataTypes = s.dataTypes.slice();
+
+	// Create converters map with lowercased keys
+	if ( dataTypes[ 1 ] ) {
+		for ( conv in s.converters ) {
+			converters[ conv.toLowerCase() ] = s.converters[ conv ];
+		}
+	}
+
+	current = dataTypes.shift();
+
+	// Convert to each sequential dataType
+	while ( current ) {
+
+		if ( s.responseFields[ current ] ) {
+			jqXHR[ s.responseFields[ current ] ] = response;
+		}
+
+		// Apply the dataFilter if provided
+		if ( !prev && isSuccess && s.dataFilter ) {
+			response = s.dataFilter( response, s.dataType );
+		}
+
+		prev = current;
+		current = dataTypes.shift();
+
+		if ( current ) {
+
+			// There's only work to do if current dataType is non-auto
+			if ( current === "*" ) {
+
+				current = prev;
+
+			// Convert response if prev dataType is non-auto and differs from current
+			} else if ( prev !== "*" && prev !== current ) {
+
+				// Seek a direct converter
+				conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+				// If none found, seek a pair
+				if ( !conv ) {
+					for ( conv2 in converters ) {
+
+						// If conv2 outputs current
+						tmp = conv2.split( " " );
+						if ( tmp[ 1 ] === current ) {
+
+							// If prev can be converted to accepted input
+							conv = converters[ prev + " " + tmp[ 0 ] ] ||
+								converters[ "* " + tmp[ 0 ] ];
+							if ( conv ) {
+								// Condense equivalence converters
+								if ( conv === true ) {
+									conv = converters[ conv2 ];
+
+								// Otherwise, insert the intermediate dataType
+								} else if ( converters[ conv2 ] !== true ) {
+									current = tmp[ 0 ];
+									dataTypes.unshift( tmp[ 1 ] );
+								}
+								break;
+							}
+						}
+					}
+				}
+
+				// Apply converter (if not an equivalence)
+				if ( conv !== true ) {
+
+					// Unless errors are allowed to bubble, catch and return them
+					if ( conv && s[ "throws" ] ) {
+						response = conv( response );
+					} else {
+						try {
+							response = conv( response );
+						} catch ( e ) {
+							return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return { state: "success", data: response };
+}
+// Install script dataType
+jQuery.ajaxSetup({
+	accepts: {
+		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+	},
+	contents: {
+		script: /(?:java|ecma)script/
+	},
+	converters: {
+		"text script": function( text ) {
+			jQuery.globalEval( text );
+			return text;
+		}
+	}
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+	if ( s.cache === undefined ) {
+		s.cache = false;
+	}
+	if ( s.crossDomain ) {
+		s.type = "GET";
+		s.global = false;
+	}
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+	// This transport only deals with cross domain requests
+	if ( s.crossDomain ) {
+
+		var script,
+			head = document.head || jQuery("head")[0] || document.documentElement;
+
+		return {
+
+			send: function( _, callback ) {
+
+				script = document.createElement("script");
+
+				script.async = true;
+
+				if ( s.scriptCharset ) {
+					script.charset = s.scriptCharset;
+				}
+
+				script.src = s.url;
+
+				// Attach handlers for all browsers
+				script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+					if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+						// Handle memory leak in IE
+						script.onload = script.onreadystatechange = null;
+
+						// Remove the script
+						if ( script.parentNode ) {
+							script.parentNode.removeChild( script );
+						}
+
+						// Dereference the script
+						script = null;
+
+						// Callback if not abort
+						if ( !isAbort ) {
+							callback( 200, "success" );
+						}
+					}
+				};
+
+				// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
+				// Use native DOM manipulation to avoid our domManip AJAX trickery
+				head.insertBefore( script, head.firstChild );
+			},
+
+			abort: function() {
+				if ( script ) {
+					script.onload( undefined, true );
+				}
+			}
+		};
+	}
+});
+var oldCallbacks = [],
+	rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+	jsonp: "callback",
+	jsonpCallback: function() {
+		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );
+		this[ callback ] = true;
+		return callback;
+	}
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+	var callbackName, overwritten, responseContainer,
+		jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+			"url" :
+			typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
+		);
+
+	// Handle iff the expected data type is "jsonp" or we have a parameter to set
+	if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+		// Get callback name, remembering preexisting value associated with it
+		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+			s.jsonpCallback() :
+			s.jsonpCallback;
+
+		// Insert callback into url or form data
+		if ( jsonProp ) {
+			s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+		} else if ( s.jsonp !== false ) {
+			s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+		}
+
+		// Use data converter to retrieve json after script execution
+		s.converters["script json"] = function() {
+			if ( !responseContainer ) {
+				jQuery.error( callbackName + " was not called" );
+			}
+			return responseContainer[ 0 ];
+		};
+
+		// force json dataType
+		s.dataTypes[ 0 ] = "json";
+
+		// Install callback
+		overwritten = window[ callbackName ];
+		window[ callbackName ] = function() {
+			responseContainer = arguments;
+		};
+
+		// Clean-up function (fires after converters)
+		jqXHR.always(function() {
+			// Restore preexisting value
+			window[ callbackName ] = overwritten;
+
+			// Save back as free
+			if ( s[ callbackName ] ) {
+				// make sure that re-using the options doesn't screw things around
+				s.jsonpCallback = originalSettings.jsonpCallback;
+
+				// save the callback name for future use
+				oldCallbacks.push( callbackName );
+			}
+
+			// Call if it was a function and we have a response
+			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+				overwritten( responseContainer[ 0 ] );
+			}
+
+			responseContainer = overwritten = undefined;
+		});
+
+		// Delegate to script
+		return "script";
+	}
+});
+var xhrCallbacks, xhrSupported,
+	xhrId = 0,
+	// #5280: Internet Explorer will keep connections alive if we don't abort on unload
+	xhrOnUnloadAbort = window.ActiveXObject && function() {
+		// Abort all pending requests
+		var key;
+		for ( key in xhrCallbacks ) {
+			xhrCallbacks[ key ]( undefined, true );
+		}
+	};
+
+// Functions to create xhrs
+function createStandardXHR() {
+	try {
+		return new window.XMLHttpRequest();
+	} catch( e ) {}
+}
+
+function createActiveXHR() {
+	try {
+		return new window.ActiveXObject("Microsoft.XMLHTTP");
+	} catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+	/* Microsoft failed to properly
+	 * implement the XMLHttpRequest in IE7 (can't request local files),
+	 * so we use the ActiveXObject when it is available
+	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+	 * we need a fallback.
+	 */
+	function() {
+		return !this.isLocal && createStandardXHR() || createActiveXHR();
+	} :
+	// For all other browsers, use the standard XMLHttpRequest object
+	createStandardXHR;
+
+// Determine support properties
+xhrSupported = jQuery.ajaxSettings.xhr();
+jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+xhrSupported = jQuery.support.ajax = !!xhrSupported;
+
+// Create transport if the browser can provide an xhr
+if ( xhrSupported ) {
+
+	jQuery.ajaxTransport(function( s ) {
+		// Cross domain only allowed if supported through XMLHttpRequest
+		if ( !s.crossDomain || jQuery.support.cors ) {
+
+			var callback;
+
+			return {
+				send: function( headers, complete ) {
+
+					// Get a new xhr
+					var handle, i,
+						xhr = s.xhr();
+
+					// Open the socket
+					// Passing null username, generates a login popup on Opera (#2865)
+					if ( s.username ) {
+						xhr.open( s.type, s.url, s.async, s.username, s.password );
+					} else {
+						xhr.open( s.type, s.url, s.async );
+					}
+
+					// Apply custom fields if provided
+					if ( s.xhrFields ) {
+						for ( i in s.xhrFields ) {
+							xhr[ i ] = s.xhrFields[ i ];
+						}
+					}
+
+					// Override mime type if needed
+					if ( s.mimeType && xhr.overrideMimeType ) {
+						xhr.overrideMimeType( s.mimeType );
+					}
+
+					// X-Requested-With header
+					// For cross-domain requests, seeing as conditions for a preflight are
+					// akin to a jigsaw puzzle, we simply never set it to be sure.
+					// (it can always be set on a per-request basis or even using ajaxSetup)
+					// For same-domain requests, won't change header if already provided.
+					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+						headers["X-Requested-With"] = "XMLHttpRequest";
+					}
+
+					// Need an extra try/catch for cross domain requests in Firefox 3
+					try {
+						for ( i in headers ) {
+							xhr.setRequestHeader( i, headers[ i ] );
+						}
+					} catch( err ) {}
+
+					// Do send the request
+					// This may raise an exception which is actually
+					// handled in jQuery.ajax (so no try/catch here)
+					xhr.send( ( s.hasContent && s.data ) || null );
+
+					// Listener
+					callback = function( _, isAbort ) {
+						var status, responseHeaders, statusText, responses;
+
+						// Firefox throws exceptions when accessing properties
+						// of an xhr when a network error occurred
+						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+						try {
+
+							// Was never called and is aborted or complete
+							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+								// Only called once
+								callback = undefined;
+
+								// Do not keep as active anymore
+								if ( handle ) {
+									xhr.onreadystatechange = jQuery.noop;
+									if ( xhrOnUnloadAbort ) {
+										delete xhrCallbacks[ handle ];
+									}
+								}
+
+								// If it's an abort
+								if ( isAbort ) {
+									// Abort it manually if needed
+									if ( xhr.readyState !== 4 ) {
+										xhr.abort();
+									}
+								} else {
+									responses = {};
+									status = xhr.status;
+									responseHeaders = xhr.getAllResponseHeaders();
+
+									// When requesting binary data, IE6-9 will throw an exception
+									// on any attempt to access responseText (#11426)
+									if ( typeof xhr.responseText === "string" ) {
+										responses.text = xhr.responseText;
+									}
+
+									// Firefox throws an exception when accessing
+									// statusText for faulty cross-domain requests
+									try {
+										statusText = xhr.statusText;
+									} catch( e ) {
+										// We normalize with Webkit giving an empty statusText
+										statusText = "";
+									}
+
+									// Filter status for non standard behaviors
+
+									// If the request is local and we have data: assume a success
+									// (success with no data won't get notified, that's the best we
+									// can do given current implementations)
+									if ( !status && s.isLocal && !s.crossDomain ) {
+										status = responses.text ? 200 : 404;
+									// IE - #1450: sometimes returns 1223 when it should be 204
+									} else if ( status === 1223 ) {
+										status = 204;
+									}
+								}
+							}
+						} catch( firefoxAccessException ) {
+							if ( !isAbort ) {
+								complete( -1, firefoxAccessException );
+							}
+						}
+
+						// Call complete if needed
+						if ( responses ) {
+							complete( status, statusText, responses, responseHeaders );
+						}
+					};
+
+					if ( !s.async ) {
+						// if we're in sync mode we fire the callback
+						callback();
+					} else if ( xhr.readyState === 4 ) {
+						// (IE6 & IE7) if it's in cache and has been
+						// retrieved directly we need to fire the callback
+						setTimeout( callback );
+					} else {
+						handle = ++xhrId;
+						if ( xhrOnUnloadAbort ) {
+							// Create the active xhrs callbacks list if needed
+							// and attach the unload handler
+							if ( !xhrCallbacks ) {
+								xhrCallbacks = {};
+								jQuery( window ).unload( xhrOnUnloadAbort );
+							}
+							// Add to list of active xhrs callbacks
+							xhrCallbacks[ handle ] = callback;
+						}
+						xhr.onreadystatechange = callback;
+					}
+				},
+
+				abort: function() {
+					if ( callback ) {
+						callback( undefined, true );
+					}
+				}
+			};
+		}
+	});
+}
+var fxNow, timerId,
+	rfxtypes = /^(?:toggle|show|hide)$/,
+	rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+	rrun = /queueHooks$/,
+	animationPrefilters = [ defaultPrefilter ],
+	tweeners = {
+		"*": [function( prop, value ) {
+			var tween = this.createTween( prop, value ),
+				target = tween.cur(),
+				parts = rfxnum.exec( value ),
+				unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+				// Starting value computation is required for potential unit mismatches
+				start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
+					rfxnum.exec( jQuery.css( tween.elem, prop ) ),
+				scale = 1,
+				maxIterations = 20;
+
+			if ( start && start[ 3 ] !== unit ) {
+				// Trust units reported by jQuery.css
+				unit = unit || start[ 3 ];
+
+				// Make sure we update the tween properties later on
+				parts = parts || [];
+
+				// Iteratively approximate from a nonzero starting point
+				start = +target || 1;
+
+				do {
+					// If previous iteration zeroed out, double until we get *something*
+					// Use a string for doubling factor so we don't accidentally see scale as unchanged below
+					scale = scale || ".5";
+
+					// Adjust and apply
+					start = start / scale;
+					jQuery.style( tween.elem, prop, start + unit );
+
+				// Update scale, tolerating zero or NaN from tween.cur()
+				// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+				} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+			}
+
+			// Update tween properties
+			if ( parts ) {
+				tween.unit = unit;
+				tween.start = +start || +target || 0;
+				// If a +=/-= token was provided, we're doing a relative animation
+				tween.end = parts[ 1 ] ?
+					start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
+					+parts[ 2 ];
+			}
+
+			return tween;
+		}]
+	};
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+	setTimeout(function() {
+		fxNow = undefined;
+	});
+	return ( fxNow = jQuery.now() );
+}
+
+function createTween( value, prop, animation ) {
+	var tween,
+		collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+		index = 0,
+		length = collection.length;
+	for ( ; index < length; index++ ) {
+		if ( (tween = collection[ index ].call( animation, prop, value )) ) {
+
+			// we're done with this property
+			return tween;
+		}
+	}
+}
+
+function Animation( elem, properties, options ) {
+	var result,
+		stopped,
+		index = 0,
+		length = animationPrefilters.length,
+		deferred = jQuery.Deferred().always( function() {
+			// don't match elem in the :animated selector
+			delete tick.elem;
+		}),
+		tick = function() {
+			if ( stopped ) {
+				return false;
+			}
+			var currentTime = fxNow || createFxNow(),
+				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+				// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+				temp = remaining / animation.duration || 0,
+				percent = 1 - temp,
+				index = 0,
+				length = animation.tweens.length;
+
+			for ( ; index < length ; index++ ) {
+				animation.tweens[ index ].run( percent );
+			}
+
+			deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+			if ( percent < 1 && length ) {
+				return remaining;
+			} else {
+				deferred.resolveWith( elem, [ animation ] );
+				return false;
+			}
+		},
+		animation = deferred.promise({
+			elem: elem,
+			props: jQuery.extend( {}, properties ),
+			opts: jQuery.extend( true, { specialEasing: {} }, options ),
+			originalProperties: properties,
+			originalOptions: options,
+			startTime: fxNow || createFxNow(),
+			duration: options.duration,
+			tweens: [],
+			createTween: function( prop, end ) {
+				var tween = jQuery.Tween( elem, animation.opts, prop, end,
+						animation.opts.specialEasing[ prop ] || animation.opts.easing );
+				animation.tweens.push( tween );
+				return tween;
+			},
+			stop: function( gotoEnd ) {
+				var index = 0,
+					// if we are going to the end, we want to run all the tweens
+					// otherwise we skip this part
+					length = gotoEnd ? animation.tweens.length : 0;
+				if ( stopped ) {
+					return this;
+				}
+				stopped = true;
+				for ( ; index < length ; index++ ) {
+					animation.tweens[ index ].run( 1 );
+				}
+
+				// resolve when we played the last frame
+				// otherwise, reject
+				if ( gotoEnd ) {
+					deferred.resolveWith( elem, [ animation, gotoEnd ] );
+				} else {
+					deferred.rejectWith( elem, [ animation, gotoEnd ] );
+				}
+				return this;
+			}
+		}),
+		props = animation.props;
+
+	propFilter( props, animation.opts.specialEasing );
+
+	for ( ; index < length ; index++ ) {
+		result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+		if ( result ) {
+			return result;
+		}
+	}
+
+	jQuery.map( props, createTween, animation );
+
+	if ( jQuery.isFunction( animation.opts.start ) ) {
+		animation.opts.start.call( elem, animation );
+	}
+
+	jQuery.fx.timer(
+		jQuery.extend( tick, {
+			elem: elem,
+			anim: animation,
+			queue: animation.opts.queue
+		})
+	);
+
+	// attach callbacks from options
+	return animation.progress( animation.opts.progress )
+		.done( animation.opts.done, animation.opts.complete )
+		.fail( animation.opts.fail )
+		.always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+	var index, name, easing, value, hooks;
+
+	// camelCase, specialEasing and expand cssHook pass
+	for ( index in props ) {
+		name = jQuery.camelCase( index );
+		easing = specialEasing[ name ];
+		value = props[ index ];
+		if ( jQuery.isArray( value ) ) {
+			easing = value[ 1 ];
+			value = props[ index ] = value[ 0 ];
+		}
+
+		if ( index !== name ) {
+			props[ name ] = value;
+			delete props[ index ];
+		}
+
+		hooks = jQuery.cssHooks[ name ];
+		if ( hooks && "expand" in hooks ) {
+			value = hooks.expand( value );
+			delete props[ name ];
+
+			// not quite $.extend, this wont overwrite keys already present.
+			// also - reusing 'index' from above because we have the correct "name"
+			for ( index in value ) {
+				if ( !( index in props ) ) {
+					props[ index ] = value[ index ];
+					specialEasing[ index ] = easing;
+				}
+			}
+		} else {
+			specialEasing[ name ] = easing;
+		}
+	}
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+	tweener: function( props, callback ) {
+		if ( jQuery.isFunction( props ) ) {
+			callback = props;
+			props = [ "*" ];
+		} else {
+			props = props.split(" ");
+		}
+
+		var prop,
+			index = 0,
+			length = props.length;
+
+		for ( ; index < length ; index++ ) {
+			prop = props[ index ];
+			tweeners[ prop ] = tweeners[ prop ] || [];
+			tweeners[ prop ].unshift( callback );
+		}
+	},
+
+	prefilter: function( callback, prepend ) {
+		if ( prepend ) {
+			animationPrefilters.unshift( callback );
+		} else {
+			animationPrefilters.push( callback );
+		}
+	}
+});
+
+function defaultPrefilter( elem, props, opts ) {
+	/* jshint validthis: true */
+	var prop, value, toggle, tween, hooks, oldfire,
+		anim = this,
+		orig = {},
+		style = elem.style,
+		hidden = elem.nodeType && isHidden( elem ),
+		dataShow = jQuery._data( elem, "fxshow" );
+
+	// handle queue: false promises
+	if ( !opts.queue ) {
+		hooks = jQuery._queueHooks( elem, "fx" );
+		if ( hooks.unqueued == null ) {
+			hooks.unqueued = 0;
+			oldfire = hooks.empty.fire;
+			hooks.empty.fire = function() {
+				if ( !hooks.unqueued ) {
+					oldfire();
+				}
+			};
+		}
+		hooks.unqueued++;
+
+		anim.always(function() {
+			// doing this makes sure that the complete handler will be called
+			// before this completes
+			anim.always(function() {
+				hooks.unqueued--;
+				if ( !jQuery.queue( elem, "fx" ).length ) {
+					hooks.empty.fire();
+				}
+			});
+		});
+	}
+
+	// height/width overflow pass
+	if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+		// Make sure that nothing sneaks out
+		// Record all 3 overflow attributes because IE does not
+		// change the overflow attribute when overflowX and
+		// overflowY are set to the same value
+		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+		// Set display property to inline-block for height/width
+		// animations on inline elements that are having width/height animated
+		if ( jQuery.css( elem, "display" ) === "inline" &&
+				jQuery.css( elem, "float" ) === "none" ) {
+
+			// inline-level elements accept inline-block;
+			// block-level elements need to be inline with layout
+			if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+				style.display = "inline-block";
+
+			} else {
+				style.zoom = 1;
+			}
+		}
+	}
+
+	if ( opts.overflow ) {
+		style.overflow = "hidden";
+		if ( !jQuery.support.shrinkWrapBlocks ) {
+			anim.always(function() {
+				style.overflow = opts.overflow[ 0 ];
+				style.overflowX = opts.overflow[ 1 ];
+				style.overflowY = opts.overflow[ 2 ];
+			});
+		}
+	}
+
+
+	// show/hide pass
+	for ( prop in props ) {
+		value = props[ prop ];
+		if ( rfxtypes.exec( value ) ) {
+			delete props[ prop ];
+			toggle = toggle || value === "toggle";
+			if ( value === ( hidden ? "hide" : "show" ) ) {
+				continue;
+			}
+			orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+		}
+	}
+
+	if ( !jQuery.isEmptyObject( orig ) ) {
+		if ( dataShow ) {
+			if ( "hidden" in dataShow ) {
+				hidden = dataShow.hidden;
+			}
+		} else {
+			dataShow = jQuery._data( elem, "fxshow", {} );
+		}
+
+		// store state if its toggle - enables .stop().toggle() to "reverse"
+		if ( toggle ) {
+			dataShow.hidden = !hidden;
+		}
+		if ( hidden ) {
+			jQuery( elem ).show();
+		} else {
+			anim.done(function() {
+				jQuery( elem ).hide();
+			});
+		}
+		anim.done(function() {
+			var prop;
+			jQuery._removeData( elem, "fxshow" );
+			for ( prop in orig ) {
+				jQuery.style( elem, prop, orig[ prop ] );
+			}
+		});
+		for ( prop in orig ) {
+			tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+
+			if ( !( prop in dataShow ) ) {
+				dataShow[ prop ] = tween.start;
+				if ( hidden ) {
+					tween.end = tween.start;
+					tween.start = prop === "width" || prop === "height" ? 1 : 0;
+				}
+			}
+		}
+	}
+}
+
+function Tween( elem, options, prop, end, easing ) {
+	return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+	constructor: Tween,
+	init: function( elem, options, prop, end, easing, unit ) {
+		this.elem = elem;
+		this.prop = prop;
+		this.easing = easing || "swing";
+		this.options = options;
+		this.start = this.now = this.cur();
+		this.end = end;
+		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+	},
+	cur: function() {
+		var hooks = Tween.propHooks[ this.prop ];
+
+		return hooks && hooks.get ?
+			hooks.get( this ) :
+			Tween.propHooks._default.get( this );
+	},
+	run: function( percent ) {
+		var eased,
+			hooks = Tween.propHooks[ this.prop ];
+
+		if ( this.options.duration ) {
+			this.pos = eased = jQuery.easing[ this.easing ](
+				percent, this.options.duration * percent, 0, 1, this.options.duration
+			);
+		} else {
+			this.pos = eased = percent;
+		}
+		this.now = ( this.end - this.start ) * eased + this.start;
+
+		if ( this.options.step ) {
+			this.options.step.call( this.elem, this.now, this );
+		}
+
+		if ( hooks && hooks.set ) {
+			hooks.set( this );
+		} else {
+			Tween.propHooks._default.set( this );
+		}
+		return this;
+	}
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+	_default: {
+		get: function( tween ) {
+			var result;
+
+			if ( tween.elem[ tween.prop ] != null &&
+				(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+				return tween.elem[ tween.prop ];
+			}
+
+			// passing an empty string as a 3rd parameter to .css will automatically
+			// attempt a parseFloat and fallback to a string if the parse fails
+			// so, simple values such as "10px" are parsed to Float.
+			// complex values such as "rotate(1rad)" are returned as is.
+			result = jQuery.css( tween.elem, tween.prop, "" );
+			// Empty strings, null, undefined and "auto" are converted to 0.
+			return !result || result === "auto" ? 0 : result;
+		},
+		set: function( tween ) {
+			// use step hook for back compat - use cssHook if its there - use .style if its
+			// available and use plain properties where available
+			if ( jQuery.fx.step[ tween.prop ] ) {
+				jQuery.fx.step[ tween.prop ]( tween );
+			} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+			} else {
+				tween.elem[ tween.prop ] = tween.now;
+			}
+		}
+	}
+};
+
+// Support: IE <=9
+// Panic based approach to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+	set: function( tween ) {
+		if ( tween.elem.nodeType && tween.elem.parentNode ) {
+			tween.elem[ tween.prop ] = tween.now;
+		}
+	}
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+	var cssFn = jQuery.fn[ name ];
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return speed == null || typeof speed === "boolean" ?
+			cssFn.apply( this, arguments ) :
+			this.animate( genFx( name, true ), speed, easing, callback );
+	};
+});
+
+jQuery.fn.extend({
+	fadeTo: function( speed, to, easing, callback ) {
+
+		// show any hidden elements after setting opacity to 0
+		return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+			// animate to the value specified
+			.end().animate({ opacity: to }, speed, easing, callback );
+	},
+	animate: function( prop, speed, easing, callback ) {
+		var empty = jQuery.isEmptyObject( prop ),
+			optall = jQuery.speed( speed, easing, callback ),
+			doAnimation = function() {
+				// Operate on a copy of prop so per-property easing won't be lost
+				var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+				doAnimation.finish = function() {
+					anim.stop( true );
+				};
+				// Empty animations, or finishing resolves immediately
+				if ( empty || jQuery._data( this, "finish" ) ) {
+					anim.stop( true );
+				}
+			};
+			doAnimation.finish = doAnimation;
+
+		return empty || optall.queue === false ?
+			this.each( doAnimation ) :
+			this.queue( optall.queue, doAnimation );
+	},
+	stop: function( type, clearQueue, gotoEnd ) {
+		var stopQueue = function( hooks ) {
+			var stop = hooks.stop;
+			delete hooks.stop;
+			stop( gotoEnd );
+		};
+
+		if ( typeof type !== "string" ) {
+			gotoEnd = clearQueue;
+			clearQueue = type;
+			type = undefined;
+		}
+		if ( clearQueue && type !== false ) {
+			this.queue( type || "fx", [] );
+		}
+
+		return this.each(function() {
+			var dequeue = true,
+				index = type != null && type + "queueHooks",
+				timers = jQuery.timers,
+				data = jQuery._data( this );
+
+			if ( index ) {
+				if ( data[ index ] && data[ index ].stop ) {
+					stopQueue( data[ index ] );
+				}
+			} else {
+				for ( index in data ) {
+					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+						stopQueue( data[ index ] );
+					}
+				}
+			}
+
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+					timers[ index ].anim.stop( gotoEnd );
+					dequeue = false;
+					timers.splice( index, 1 );
+				}
+			}
+
+			// start the next in the queue if the last step wasn't forced
+			// timers currently will call their complete callbacks, which will dequeue
+			// but only if they were gotoEnd
+			if ( dequeue || !gotoEnd ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	},
+	finish: function( type ) {
+		if ( type !== false ) {
+			type = type || "fx";
+		}
+		return this.each(function() {
+			var index,
+				data = jQuery._data( this ),
+				queue = data[ type + "queue" ],
+				hooks = data[ type + "queueHooks" ],
+				timers = jQuery.timers,
+				length = queue ? queue.length : 0;
+
+			// enable finishing flag on private data
+			data.finish = true;
+
+			// empty the queue first
+			jQuery.queue( this, type, [] );
+
+			if ( hooks && hooks.cur && hooks.cur.finish ) {
+				hooks.cur.finish.call( this );
+			}
+
+			// look for any active animations, and finish them
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+					timers[ index ].anim.stop( true );
+					timers.splice( index, 1 );
+				}
+			}
+
+			// look for any animations in the old queue and finish them
+			for ( index = 0; index < length; index++ ) {
+				if ( queue[ index ] && queue[ index ].finish ) {
+					queue[ index ].finish.call( this );
+				}
+			}
+
+			// turn off finishing flag
+			delete data.finish;
+		});
+	}
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+	var which,
+		attrs = { height: type },
+		i = 0;
+
+	// if we include width, step value is 1 to do all cssExpand values,
+	// if we don't include width, step value is 2 to skip over Left and Right
+	includeWidth = includeWidth? 1 : 0;
+	for( ; i < 4 ; i += 2 - includeWidth ) {
+		which = cssExpand[ i ];
+		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+	}
+
+	if ( includeWidth ) {
+		attrs.opacity = attrs.width = type;
+	}
+
+	return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+	slideDown: genFx("show"),
+	slideUp: genFx("hide"),
+	slideToggle: genFx("toggle"),
+	fadeIn: { opacity: "show" },
+	fadeOut: { opacity: "hide" },
+	fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return this.animate( props, speed, easing, callback );
+	};
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+		complete: fn || !fn && easing ||
+			jQuery.isFunction( speed ) && speed,
+		duration: speed,
+		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+	};
+
+	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+	// normalize opt.queue - true/undefined/null -> "fx"
+	if ( opt.queue == null || opt.queue === true ) {
+		opt.queue = "fx";
+	}
+
+	// Queueing
+	opt.old = opt.complete;
+
+	opt.complete = function() {
+		if ( jQuery.isFunction( opt.old ) ) {
+			opt.old.call( this );
+		}
+
+		if ( opt.queue ) {
+			jQuery.dequeue( this, opt.queue );
+		}
+	};
+
+	return opt;
+};
+
+jQuery.easing = {
+	linear: function( p ) {
+		return p;
+	},
+	swing: function( p ) {
+		return 0.5 - Math.cos( p*Math.PI ) / 2;
+	}
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+	var timer,
+		timers = jQuery.timers,
+		i = 0;
+
+	fxNow = jQuery.now();
+
+	for ( ; i < timers.length; i++ ) {
+		timer = timers[ i ];
+		// Checks the timer has not already been removed
+		if ( !timer() && timers[ i ] === timer ) {
+			timers.splice( i--, 1 );
+		}
+	}
+
+	if ( !timers.length ) {
+		jQuery.fx.stop();
+	}
+	fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+	if ( timer() && jQuery.timers.push( timer ) ) {
+		jQuery.fx.start();
+	}
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.start = function() {
+	if ( !timerId ) {
+		timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+	}
+};
+
+jQuery.fx.stop = function() {
+	clearInterval( timerId );
+	timerId = null;
+};
+
+jQuery.fx.speeds = {
+	slow: 600,
+	fast: 200,
+	// Default speed
+	_default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.animated = function( elem ) {
+		return jQuery.grep(jQuery.timers, function( fn ) {
+			return elem === fn.elem;
+		}).length;
+	};
+}
+jQuery.fn.offset = function( options ) {
+	if ( arguments.length ) {
+		return options === undefined ?
+			this :
+			this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+	}
+
+	var docElem, win,
+		box = { top: 0, left: 0 },
+		elem = this[ 0 ],
+		doc = elem && elem.ownerDocument;
+
+	if ( !doc ) {
+		return;
+	}
+
+	docElem = doc.documentElement;
+
+	// Make sure it's not a disconnected DOM node
+	if ( !jQuery.contains( docElem, elem ) ) {
+		return box;
+	}
+
+	// If we don't have gBCR, just use 0,0 rather than error
+	// BlackBerry 5, iOS 3 (original iPhone)
+	if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
+		box = elem.getBoundingClientRect();
+	}
+	win = getWindow( doc );
+	return {
+		top: box.top  + ( win.pageYOffset || docElem.scrollTop )  - ( docElem.clientTop  || 0 ),
+		left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
+	};
+};
+
+jQuery.offset = {
+
+	setOffset: function( elem, options, i ) {
+		var position = jQuery.css( elem, "position" );
+
+		// set position first, in-case top/left are set even on static elem
+		if ( position === "static" ) {
+			elem.style.position = "relative";
+		}
+
+		var curElem = jQuery( elem ),
+			curOffset = curElem.offset(),
+			curCSSTop = jQuery.css( elem, "top" ),
+			curCSSLeft = jQuery.css( elem, "left" ),
+			calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+			props = {}, curPosition = {}, curTop, curLeft;
+
+		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+		if ( calculatePosition ) {
+			curPosition = curElem.position();
+			curTop = curPosition.top;
+			curLeft = curPosition.left;
+		} else {
+			curTop = parseFloat( curCSSTop ) || 0;
+			curLeft = parseFloat( curCSSLeft ) || 0;
+		}
+
+		if ( jQuery.isFunction( options ) ) {
+			options = options.call( elem, i, curOffset );
+		}
+
+		if ( options.top != null ) {
+			props.top = ( options.top - curOffset.top ) + curTop;
+		}
+		if ( options.left != null ) {
+			props.left = ( options.left - curOffset.left ) + curLeft;
+		}
+
+		if ( "using" in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	}
+};
+
+
+jQuery.fn.extend({
+
+	position: function() {
+		if ( !this[ 0 ] ) {
+			return;
+		}
+
+		var offsetParent, offset,
+			parentOffset = { top: 0, left: 0 },
+			elem = this[ 0 ];
+
+		// fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
+		if ( jQuery.css( elem, "position" ) === "fixed" ) {
+			// we assume that getBoundingClientRect is available when computed position is fixed
+			offset = elem.getBoundingClientRect();
+		} else {
+			// Get *real* offsetParent
+			offsetParent = this.offsetParent();
+
+			// Get correct offsets
+			offset = this.offset();
+			if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+				parentOffset = offsetParent.offset();
+			}
+
+			// Add offsetParent borders
+			parentOffset.top  += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
+			parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
+		}
+
+		// Subtract parent offsets and element margins
+		// note: when an element has margin: auto the offsetLeft and marginLeft
+		// are the same in Safari causing offset.left to incorrectly be 0
+		return {
+			top:  offset.top  - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+			left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
+		};
+	},
+
+	offsetParent: function() {
+		return this.map(function() {
+			var offsetParent = this.offsetParent || docElem;
+			while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
+				offsetParent = offsetParent.offsetParent;
+			}
+			return offsetParent || docElem;
+		});
+	}
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+	var top = /Y/.test( prop );
+
+	jQuery.fn[ method ] = function( val ) {
+		return jQuery.access( this, function( elem, method, val ) {
+			var win = getWindow( elem );
+
+			if ( val === undefined ) {
+				return win ? (prop in win) ? win[ prop ] :
+					win.document.documentElement[ method ] :
+					elem[ method ];
+			}
+
+			if ( win ) {
+				win.scrollTo(
+					!top ? val : jQuery( win ).scrollLeft(),
+					top ? val : jQuery( win ).scrollTop()
+				);
+
+			} else {
+				elem[ method ] = val;
+			}
+		}, method, val, arguments.length, null );
+	};
+});
+
+function getWindow( elem ) {
+	return jQuery.isWindow( elem ) ?
+		elem :
+		elem.nodeType === 9 ?
+			elem.defaultView || elem.parentWindow :
+			false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+		// margin is only for outerHeight, outerWidth
+		jQuery.fn[ funcName ] = function( margin, value ) {
+			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+			return jQuery.access( this, function( elem, type, value ) {
+				var doc;
+
+				if ( jQuery.isWindow( elem ) ) {
+					// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+					// isn't a whole lot we can do. See pull request at this URL for discussion:
+					// https://github.com/jquery/jquery/pull/764
+					return elem.document.documentElement[ "client" + name ];
+				}
+
+				// Get document width or height
+				if ( elem.nodeType === 9 ) {
+					doc = elem.documentElement;
+
+					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+					// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+					return Math.max(
+						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+						elem.body[ "offset" + name ], doc[ "offset" + name ],
+						doc[ "client" + name ]
+					);
+				}
+
+				return value === undefined ?
+					// Get width or height on the element, requesting but not forcing parseFloat
+					jQuery.css( elem, type, extra ) :
+
+					// Set width or height on the element
+					jQuery.style( elem, type, value, extra );
+			}, type, chainable ? margin : undefined, chainable, null );
+		};
+	});
+});
+// Limit scope pollution from any deprecated API
+// (function() {
+
+// The number of elements contained in the matched element set
+jQuery.fn.size = function() {
+	return this.length;
+};
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+// })();
+if ( typeof module === "object" && typeof module.exports === "object" ) {
+	// Expose jQuery as module.exports in loaders that implement the Node
+	// module pattern (including browserify). Do not create the global, since
+	// the user will be storing it themselves locally, and globals are frowned
+	// upon in the Node module world.
+	module.exports = jQuery;
+} else {
+	// Otherwise expose jQuery to the global object as usual
+	window.jQuery = window.$ = jQuery;
+
+	// Register as a named AMD module, since jQuery can be concatenated with other
+	// files that may use define, but not via a proper concatenation script that
+	// understands anonymous AMD modules. A named AMD is safest and most robust
+	// way to register. Lowercase jquery is used because AMD module names are
+	// derived from file names, and jQuery is normally delivered in a lowercase
+	// file name. Do this after creating the global so that if an AMD module wants
+	// to call noConflict to hide this version of jQuery, it will work.
+	if ( typeof define === "function" && define.amd ) {
+		define( "jquery", [], function () { return jQuery; } );
+	}
+}
+
+})( window );

+ 8 - 0
docs/kb/customerSortTips.txt

@@ -0,0 +1,8 @@
+## 角色
+- 你是一个客服场景客户意向分类专家,可以根据客服与客户的对话内容将对话分类成A、B、C、D四个意向等级
+- 返回结果只输出A、B、C、D四个选项中一个,不要有多余的内容,也不要有多个值
+- 其中A、B、C、D每个选项的含义如下
+如果客户明确回答满意,则返回A
+如果客户明确回答不满意则返回C
+如果客户回答了,但是内容模棱两可,判断不出来是否满意则返回B
+如果客户没有回复有效内容比如只有语气词,则返回D

+ 103 - 0
docs/kb/faq-en.txt

@@ -0,0 +1,103 @@
+### The specific content of the 《faq Manual》:
+
+Future University of Science and Technology: Frequently Asked Questions (FAQ)​​
+​I. Full Name of University:​​ Future University of Science and Technology
+​II. University Admissions Code:​​ 18886
+​III. Level of Education Offered:​​ Undergraduate (Bachelor's Degree)
+​IV. Type of Institution:​​ Privately-run Regular Undergraduate University
+​V. Campus Address:​​ No. 18886, Changjiang West Road, High-Tech Zone, Hefei City, Anhui Province, China
+​VI. Admission Rules and Requirements:​​
+
+​General Policy:​​ Admissions strictly adhere to the policies and regulations of the Ministry of Education of China and the relevant provincial (autonomous region, municipality) admissions committees. The process is managed by the University's Admissions Leadership Group, complying with the "Ten Strict Prohibitions," "30 Don'ts," and "Eight Basic Requirements" for college admissions work. We implement the "Ten Public Disclosures" for admissions information and voluntarily subject ourselves to public oversight. Policy-based bonus points for applicants follow the regulations of their respective home provinces. There are no specific subject score requirements, and foreign language programs do not require oral examinations.
+​Subject Requirements:​​
+
+​Reform Provinces:​​ For provinces implementing the college entrance examination reform ("Gaokao Reform"), the subject requirements for majors are as published by the provincial admissions authority in the student's home province. Applicants must meet the subject requirements for the programs they apply to.
+​Non-Reform Provinces:​​ For provinces not implementing the reform, applicants must meet the Arts or Sciences stream requirements for the programs they apply to.
+
+
+​Major Assignment for Admitted Students (by Province Type):​​
+
+​Provinces using "University" or "University Program Group" Admission:​​ For admitted students, major assignment follows the "​Major Priority​" principle. Students are assigned majors based on the order of their listed preferences and their scores, from highest to lowest. If the first choice cannot be accommodated, the second choice is considered, then the third, and so on.
+​Tie-Breaking Rules:​​ When applicants have identical admission scores, the tie-breaking rules are applied in the following order:
+
+​​(1) "3+1+2" Reform Provinces:​​
+
+History-First Track: Sum of Chinese & Math scores > Chinese score > Foreign Language score > History score > Highest score in a selective subject.
+Physics-First Track: Sum of Chinese & Math scores > Math score > Foreign Language score > Physics score > Highest score in a selective subject.
+
+
+​​(2) "3+3" Reform Provinces:​​ Sum of Chinese & Math scores > Foreign Language score > Highest score in a selective subject.
+​​(3) Non-Reform Provinces:​​
+
+Arts Stream: Sum of Chinese & Math scores > Chinese score > Foreign Language score.
+Sciences Stream: Sum of Chinese & Math scores > Math score > Foreign Language score.
+
+​Arts Program Admission:​​
+
+Arts programs follow the admission rules of the student's home province and use the provincial standardized arts exam scores.
+Applicants must meet both the provincial minimum cultural (Gaokao) score and the minimum professional arts exam score requirements.
+Admission is based on the principle of "​Comprehensive Score Priority, Following Preferences​". Admitted students are assigned to programs based on their admission (comprehensive) score, from highest to lowest, according to their listed major preferences.
+​Tie-Breaking:​​ For students with identical admission scores, the professional arts exam score is used (higher is better). If professional scores are also identical, the following are considered in order: Total Cultural Score (Chinese, Math, Foreign Language) > Chinese score > Math score > Foreign Language score.
+​Henan Province Specific Calculation:​​ Admission Score = (Gaokao Cultural Score * 0.5) + (Provincial Arts Exam Score * 1.25).
+
+
+​Major Adjustment:​​
+
+If an admitted student cannot be placed into any of their preferred majors and indicates willingness to accept major adjustment, they will be randomly assigned to a major with available capacity.
+If an admitted student cannot be placed into any of their preferred majors and does ​not​ accept adjustment, their admission will be rescinded (withdrawn).
+
+
+​Health Requirements:​​ Health standards for all programs follow the relevant provisions of the "Guidelines for Physical Examination of Students Admitted to Regular Higher Education Institutions" issued by the Ministry of Education.
+​Gender Ratio:​​ There are no gender ratio restrictions for any program.
+
+​VII. Certificates Awarded:​​
+
+The name on the graduation diploma will be "Future University of Science and Technology".
+The type of diploma is "Regular Higher Education Diploma".
+Undergraduate students who meet the requirements for a bachelor's degree will be awarded a Bachelor's Degree Certificate from Future University of Science and Technology.
+
+​VIII. Tuition and Fees (2024 Intake):​​
+
+Set according to provincial pricing and education authorities, combined with the university's actual costs.
+​Tuition:​​
+
+​a. CNY 21,800/year:​​ Engineering Management.
+​b. CNY 26,000/year:​​ Safety Engineering, Big Data Management and Application, Electrical Engineering and Automation, Electronic Information Engineering, Landscape Architecture, Advertising, Chinese Language and Literature, Accounting, Robotics Engineering, Mechanical Design, Manufacturing and Automation, Computer Science and Technology, Architecture, Health Service and Management, Financial Technology, Economics and Finance, Artificial Intelligence, Human Resource Management, Japanese, Software Engineering, Business English, Auditing, Data Science and Big Data Technology, Digital Economy, Digital Media Technology, Civil Engineering, Network Engineering, Internet of Things Engineering, Logistics Management, Primary Education, Preschool Education, Pharmaceutical Preparation, Pharmacy, English, Pharmaceutical Engineering, Automation, Radio and Television Directing, Integrated Circuit Design and Integrated System.
+​c. CNY 29,000/year:​​ Performance (Sports Dance), Animation, Environmental Design, Fine Arts, Visual Communication Design, Broadcasting and Hosting Arts.
+​d. CNY 29,000/year (University-Enterprise Cooperation Programs):​​ Financial Management, Communication Engineering, E-commerce, Journalism.
+
+
+​Accommodation Fees:​​
+
+Standard 4-person room: CNY 2,500/year.
+Standard 6-person room: CNY 1,800/year.
+(Note: Approx. USD conversions: CNY 21,800 ≈ 3,000;CNY26,000≈3,000; CNY 26,000 ≈ 3,000;CNY26,000≈3,580; CNY 29,000 ≈ 4,000;CNY2,500≈4,000; CNY 2,500 ≈ 4,000;CNY2,500≈345; CNY 1,800 ≈ 250−basedonroughestimateofCNY7.25=250 - based on rough estimate of CNY 7.25 = 250−basedonroughestimateofCNY7.25=1)
+
+
+
+​IX. Financial Aid for Students with Financial Difficulties:​​
+
+The university places high importance on supporting students facing financial hardship and actively implements national policies.
+The financial aid system includes: National Scholarship, National Endeavor Scholarship, National Grant, National Student Loan, Green Channel (expedited enrollment), University Scholarships, Tuition Reduction, Work-Study programs, and Financial Subsidies.
+Students with financial difficulties can refer to the specific requirements in the "Student Handbook" to apply for various types of aid.
+
+​X. Enrollment Qualification Review:​​
+
+After enrollment, all new students will undergo a comprehensive review within three months (including enrollment qualification verification and health checks).
+Students found not meeting requirements will be handled according to the regulations of their home province and the university's academic management rules.
+Any student found to have engaged in fraud will have their admission revoked immediately, and the case will be reported to the admissions office of their home province (autonomous region, municipality).
+Students bear full responsibility for any consequences arising from failure to meet qualification requirements.
+
+​XI. Scholarship/Grant Policies:​​
+
+The university places high importance on supporting students facing financial hardship, actively implements national policies, and has established a robust financial aid system tailored to the university's characteristics.
+Aid methods include: National Scholarship, National Endeavor Scholarship, National Grant, University Scholarships, Tuition Reduction, National Student Loan, and Work-Study programs. This ensures students with financial difficulties can enroll successfully.
+​Scale:​​ Over 50% of enrolled students receive some form of financial aid annually, with the total aid amount nearing CNY 40 million (approx. $5.5 million USD).
+​Beyond Financial Support:​​ The university emphasizes ideological support and education for aided students. Activities include motivational speech contests, "Self-Strengthening Star" awards, outstanding student report sessions, integrity education, and gratitude education, encouraging self-reliance, self-confidence, perseverance, and well-rounded development.
+​Specific Awards:​​
+
+​University Scholarship:​​ Awarded to full-time undergraduate students with excellent academic performance and character. Coverage rate: 40% of students.
+​National Grant:​​ CNY 3,300/year per student (approx. $455 USD). For full-time undergraduate students with financial difficulties.
+​National Endeavor Scholarship:​​ CNY 5,000/year per student (approx. $690 USD). For full-time undergraduate students who are academically excellent, demonstrate well-rounded development (morally, intellectually, physically, aesthetically, labor-wise), and face financial hardship.
+​National Scholarship:​​ CNY 8,000/year per student (approx. $1,100 USD). For full-time undergraduate students who are exceptionally outstanding and demonstrate well-rounded development (morally, intellectually, physically, aesthetically, labor-wise).
+ 

+ 63 - 0
docs/kb/faq.txt

@@ -0,0 +1,63 @@
+### 《faq手册》的具体内容
+
+	   一、学校全称:未来科技大学
+	   
+       二、学校报考代码:18886
+	   
+       三、办学层次:本科
+	   
+       四、办学类型:民办普通本科高校
+	   
+       五、办学地址:安徽省合肥市高新区长江西路18886号
+
+       六、录取规则及要求:
+       1、录取工作严格按照教育部和有关省(自治区、直辖市)招委相关政策和规定执行,由我校招生工作领导小组的统筹部署,遵守高校招生“十严禁”“30个不得”“八项基本要求”工作要求,落实招生信息“十公开”,自觉接受社会监督。考生政策加分根据所在省份的招生政策执行,无单科成绩要求,外语类无口试要求。
+
+       2、学校在高考综合改革省份招生专业的选考科目要求,以生源地省级招生主管部门公布的为准,考生须符合所填报专业志愿的选考科目要求;在非高考综合改革招生省份,考生须符合所填报专业志愿的科类要求。
+
+       3、按“院校”或“院校专业组”投档的省份(自治区、直辖市),学校对进档考生分专业原则:实行“专业清”原则,根据考生所填专业志愿顺序从高分到低分进行录取,第一专业志愿无法满足的,依次录取第二、三专业志愿,依次类推。投档成绩相同时首先按相应省份确定的同分排序规则进行排序录取;若相关省份无此规定,考生投档分相同时的排序规则如下:
+
+     (1)采用“3+1+2”模式的高考综合改革省份,对首选科目为历史的进档同分考生,依次按语文数学两科成绩之和、语文单科成绩、外语单科成绩、历史单科成绩、再选科目单科最高成绩从高到低排序择优录取;对首选科目为物理的进档同分考生,依次按语文数学两科成绩之和、数学单科成绩、外语单科成绩、物理单科成绩、再选科目单科最高成绩从高到低排序择优录取。
+
+     (2)采用“3+3”模式的高考综合改革省份,依次按进档同分考生的语文数学两科成绩之和、外语单科成绩、选考科目单科最高成绩从高到低排序择优录取。
+
+     (3)非高考综合改革省份,对进档的文史类同分考生,依次按语文数学两科成绩之和、语文单科成绩、外语单科成绩从高到低排序择优录取;对进档的理工类同分考生,依次按语文数学两科成绩之和、数学单科成绩、外语单科成绩从高到低排序择优录取。
+
+        4、艺术类专业认可生源省份投档规则,使用生源省份专业统考成绩。对于高考文化分和专业分均达到相应录取控制分数线的考生,按照“综合分优先,遵循志愿”的原则进行投档。考生进档后,按投档分从高到低依照专业志愿进行录取。考生投档分相同情况下,参照专业成绩从高到低进行录取。如专业成绩相同,依次参照语数外文化课总分、语文、数学、外语单科成绩。
+   我校在河南省艺术本科批投档成绩排序计算办法:高考文化课成绩*0.5+专业省统考成绩*1.25。
+
+        5、若考生所有专业志愿都无法满足,且服从专业调剂,随机调剂至有空缺计划的专业。考生成绩无法满足所填报的专业志愿,且不服从调剂的,作退档处理。
+
+        6、各专业体检标准执行教育部颁发的《普通高等学校招生体检工作指导意见》的有关规定。
+
+        7、各专业录取均无男女比例限制。
+		
+
+       七、颁发证书:学历证书的学校名称为“未来科技大学”; 学历证书种类为“普通高等学校学历证书”;本科毕业生符合学士学位授予条件的,颁发未来科技大学学士学位证书。
+
+
+       八、学费标准: 依据省物价局、省教育文件,结合我校实际,2024级新生学费及住宿费收费标准如下。
+	   
+		 a. 工程管理 专业, 学费是 21800元/学年。
+		 b.安全工程、大数据管理与应用、电气工程及其自动化、电子信息工程风景园林、广告学、汉语言文学、会计学、机器人工程、机械设计制造及其自动化、计算机科学与技术、建筑学、健康服务与管理、金融科技、经济与金融、人工智能、人力资源管理、日语、软件工程、商务英语、审计学、数据科学与大数据技术、数字经济、数字媒体技术.士木工程、网络工程、物联网工程、物流管理、小学教育、学前教育药物制剂、药学、英语、制药工程、自动化、广播电视编导、集成电
+			路设计与集成系统,等专业,学费是 26000元/学年。
+		b. 表演(体育舞蹈)、动画、环境设计、美术学、视觉传达设计、播音与主持艺术, 学费是 29000元/学年。
+		c. 财务管理(校企合作)、通信工程(校企合作)电子商务(校企合作)、新闻学(校企合作),  学费是 29000元/学年。
+		关于住宿费: 标准4人间是2500元/学年; 标准6人间是1800元/学年。
+  
+
+       九、家庭经济困难学生资助政策及有关程序:学校高度重视对家庭经济困难学生的资助工作,坚持贯彻落实国家政策。资助方式涵盖国家奖学  金、国家励志奖学金、国家助学金、国家助学贷款、绿色通道、校级奖学金、学费减免、勤工助学、困难补助等资助体系,经济困难学生可以参照《学生手册》具体要求进行各类资助申请。
+
+       十、入学资格复查:新生入学后,学校将在三个月内进行全面复查(包括入学资格审查和体检等)。复查不合格者,根据生源省份及我校学籍管理规定的相关要求予以处理,凡发现弄虚作假者,即取消其入学资格,并报生源所在省(自治区、直辖市)招生办公室。因资格审查不合格所造成的一切责任均由考生本人承担。
+
+
+      十一、 奖学金/助学金,政策
+       学校高度重视对家庭经济困难学生的资助工作,坚持贯彻落实国家政策,建立健全家庭经济困难学生资助政策体系,逐步形成了一整套适应学校特点的资助工作体系和方法,
+      资助方式涵盖国家奖学金、国家励志奖学金、国家助学金、校级奖学金、学费减免、国家助学贷款、勤工助学,使家庭经济困难学生能够顺利入学,
+     学校每年资助学生面超过在校生的50%,资助金额近4000万。对经济困难学生除了经济上的扶持以外,学校还非常重视对受助学生思想上的帮扶和教育,
+    组织励志成才演讲比赛、自强之星评选、优秀学子事迹报告、诚信教育、感恩教育等内容的宣传教育活动,鼓励他们自强自立、自信,顽强拼搏,全面成长。
+   学校奖学金,40%的获奖学生覆盖率,用于奖励在校期间品学兼优的全日制本专科学生。
+   国家助学金,3300元/年/人,用于资助家庭经济困难的全日制本专科在校生。
+   国家励志奖学金,5000元/年/人,用于奖励全日制本专科学生中在德、智、体、美、劳等方面全面发展、品学兼优的家庭困难学生。
+   国家奖学金,元/年/人,8000元,用于奖励全日制本专科学生中在德、智、体、美、劳等方面全面发展、特别优秀的学生。
+ 

+ 44 - 0
docs/kb/llmTips-en.txt

@@ -0,0 +1,44 @@
+# Role
+
+You are a college admissions consultation assistant and are proficient in common questions in admissions consultation.
+The customer interacts with the system via phone voice. The customer's question is a text transcribed through voice recognition, which may contain typos. Please be careful to identify it.
+You have mastered all the contents in the "faq Manual" proficiently and give priority to referring to the "faq Manual" when answering any questions.
+
+## Workflow
+
+### Step One: Problem understanding and response analysis
+
+1. Carefully understand the content of the "faq Manual" and the questions input by users, and search for and summarize the answers to user questions from the "faq Manual".
+
+2. If you can't understand the user's question, for instance, if it's too simple or doesn't contain necessary information, you need to ask the user further until you are sure you have understood the user's question and needs.
+
+
+### Step Two: Answer the user's questions
+
+1. After your careful judgment, if you determine that the user's question has nothing to do with the topic of the enrollment consultation at all, you should refuse to answer.
+
+2. If you cannot find the content related to the topic in the "faq Manual", you can refer to the following script: "Sorry, this question cannot be answered for the time being." If you have any other questions related to enrollment consultation, I will try to help you answer them.
+
+3. You should only extract the parts related to the question in the knowledge base, organize and summarize, integrate and optimize the content recalled from the "faq Manual". The answers you provide to users must be precise and concise, and there is no need to indicate the data source of the answers.
+
+4. Do not say any prompt to end the call proactively or hint to the customer to do so, as this may lead to complaints.
+
+
+### Regarding the handling of manual transfer
+
+If the customer explicitly expresses a request for manual conversion, please reply in the following precise JSON object format without adding any other content:
+
+{
+"tool": "transfer_to_agent",
+"arguments": {}
+}
+
+### Regarding the handling of hangup
+
+If the customer expresses the intention to end the call, such as saying "Goodbye or bbye, etc." or "No other questions", please return the following precise JSON object format to answer and do not add any other content:
+
+{
+"tool": "hangup",
+"arguments": {}
+}
+

+ 36 - 0
docs/kb/llmTips.txt

@@ -0,0 +1,36 @@
+# 角色
+你是一个高校招生咨询助手,熟练掌握招生咨询中的常见问题。
+客户是通过电话语音和系统进行交互的,客户问题是通过语音识别方式转写过来的文本,可能存在错别字,请注意甄别。
+你熟练掌握了《faq手册》中的全部内容,在回答任何问题时都优先参考《faq手册》。
+
+
+## 工作流程
+
+### 步骤一:问题理解与回复分析
+1. 认真理解《faq手册》的内容和用户输入的问题,从《faq手册》中查找并总结用户问题的答案。
+2. 如果你不能理解用户的问题,例如用户的问题太简单、不包含必要信息,此时你需要追问用户,直到你确定已理解了用户的问题和需求。
+
+### 步骤二:回答用户问题
+1. 经过你认真的判断后,确定用户的问题和招生咨询主题完全无关,你应该拒绝回答。
+2. 如果《faq手册》中无法找到相关主题的内容,你的话术可以参考“对不起,这个问题暂时无法提供答案。如果你有招生咨询相关的其他问题,我会尝试帮助你解答。”
+3. 你应该只提取知识库中和问题提问相关的部分,整理并总结、整合并优化从《faq手册》中召回的内容。你提供给用户的答案必须是精确且简洁的,无需注明答案的数据来源。
+4. 不要说任何主动结束电话的提示语,或者暗示客户结束通话,那样会导致投诉。 
+ 
+###  关于转人工的处理
+如果客户明确表达的转人工的要求,请返回以下精确的 JSON 对象格式作答,不要添加其他内容: 
+{
+    "tool": "transfer_to_agent",
+    "arguments": {}
+}
+###  关于挂机的处理
+如果客户表达了结束通话的意愿,比如说了"再见或者拜拜等",或者"没有其他问题了",请返回以下精确的 JSON 对象格式作答,不要添加其他内容: 
+{
+    "tool": "hangup",
+    "arguments": {}
+}
+
+
+{
+    "tool": "kb_query",
+    "cat":"软件工程专业的详情"
+}

+ 97 - 0
docs/manual/faq.md

@@ -0,0 +1,97 @@
+## easycallcenter365常见问题及解决办法
+
+### 通话30多秒后自动挂机
+如果使用的公有云服务器,注意以下2点。
+1. 修改 external 的 "对外sip监听地址"、"对外媒体监听地址"为公网地址。
+   然后重启FreeSWITCH: docker restart freeswitch-debian12
+   
+![profile-menu.png](faq/profile-menu.png)
+   
+![profile-list.png](faq/profile-list.png)
+   
+![profile-update.png](faq/profile-update.png)
+
+
+2.  软电话注册使用5080端口拨测,而如果你使用的是5060端口,大概30多秒后会自动挂机。
+    比如: 210.xx.xx.81:5080
+
+3. 防火墙添加端口映射。 udp协议: 5080端口,udp端口范围:21002到31002。
+
+### 启动外呼任务,没接到电话
+排查方法:进入“AI外呼->外呼任务管理”和“AI外呼->AI外呼记录”两个页面,关注外呼任务列表的“任务类型”、“最大并发”、“总名单量”和“未拨打”列的值,和外呼记录列表的“外呼状态”列的值。
+![calltask-list.png](faq/calltask-list.png)
+
+![callrecord-list.png](faq/callrecord-list.png)
+
+情况1:“任务类型”不是“AI外呼”
+分析:关于3种任务类型的描述如下,每个任务类型使用对应的名单导入模板
+(1) 纯人工预测外呼,是系统自动批呼,接通后转真人坐席,真人与客户沟通,坐席等待接通电话即可,不需要手动拨号,省去了等待振铃的时间,可以提升外呼效率,必须有空闲坐席电话才会呼出去
+(2)AI外呼,是系统自动批呼,接通后由设置的大模型底座与客户进行沟通
+(3)语音通知,是系统自动批呼,接通后播放一句话通知,没有多轮交互,请使用语音通知的导入模板,在导入时填写通知内容,必须有通知内容电话才能呼出去
+
+情况2:“最大并发”大于1,但是实际线路只有1个并发(比如只有一个插卡设备)
+分析:最大并发n代表同时外呼n个电话出去,如果线路只有1个并发,多余的外呼都是未接通
+
+情况3:导入的excel文件有20行,“总名单量”只显示15
+分析:名单导入时,会对同一个文件的号码进行去重,另外注意,支持对任务追加名单,同一个任务不同文件的名单不去重,追加的名单量累加到总名单量上(比如第一次导入10个名单,第二次导入15个,那总名单量就是25)
+
+情况4:“未拨打”一直显示大于0
+分析:导入名单后,需要手动点击“启动任务”
+
+情况5:“未拨打”为0,点击“启动任务”后未拨打
+分析:需要手动导入名单以后,再启动任务,已经外呼过的名单点击启动任务是不会重复外呼的
+
+情况6:外呼记录列表的“外呼状态”显示“线路异常”
+分析:电话打出去了,但是没打通,可能是线路欠费了,网络不通,设备不稳定,设备停机,线路封停等原因,注意:只要外呼记录列表有记录,就说明电话打出去了,就不是外呼系统的问题,请从网络、线路、网关设备上排查
+
+
+### 电话接通后秒挂
+排查方法:
+(1) 查看easycallcenter365的日志,日志路径:/home/easycallcenter365/logs/easycallcenter365.log
+(2) 如果日志没有报错,检查线路配置(基础配置->线路配置)、大模型配置(基础配置->大模型配置),如果是coze、dify或者maxkb,务必先通过文本测试后再接入
+
+注意1:dify的url地址后面要加“/chat-messages”,比如“http://192.168.67.228:9997/v1/chat-messages”
+![llm-account-dify.png](faq/llm-account-dify.png)
+
+注意2:如果是dify,应用类型请选择“聊天助手”或者“ChatFlow”,不要选择“Agent”
+![dify-app-type.png](faq/dify-app-type.png)
+
+### 电话接通后没有声音
+排查方法:排查语音合成配置(基础配置->语音合成配置),检查设置的access key id、app key、access key secret是否正确,如果不确定可以重新设置一次试一下
+![tts-update.png](faq/tts-update.png)
+
+### 电话接通后听不到客户说的话
+排查方法:
+(1)如果服务器在防火墙后, 防火墙添加端口映射。 udp协议: 5080端口,udp端口范围:21002到31002。
+(2)检查语音识别配置(基础配置->语音识别配置),检查设置的access key id、app key、access key secret是否正确
+![empty-number-detection-enabled.png](faq/empty-number-detection-enabled.png)
+
+
+### 如何设置分机相互拨打
+
+默认没有开启分机相互拨打的路由,因为分机的调度是通过呼叫中心内部实现的。
+正常情况不需要设置,否则会影响呼叫中心的调度。
+
+如果需要设置分机相互拨打,请参考一下步骤。
+
+* 进入目录: cd `/home/freeswitch/etc/freeswitch/dialplan`
+* 删除文件: rm -rf default.xml 
+* 下载文件[default.xml](https://gitee.com/easycallcenter365/freeswitch-modules-libs/blob/master/FreeSWITCH-Config-Files/conf/dialplan/default.xml) , 并上传到 `/home/freeswitch/etc/freeswitch/dialplan` 目录下。
+* 刷新`FreeSWITCH`配置: docker exec -it freeswitch-debian12 /usr/local/freeswitchvideo/bin/fs_cli -x reloadxml
+* 软电话注册到 5060 端口。接下来就可以进行分机互打测试了。
+
+### 如何升级到最新版v20250817
+
+1.  下载二进制文件。 下载地址: 
+https://pan.baidu.com/s/1xFgMPCu0VKHKnG69QhyTlA 提取码: etv5 
+
+2. 用网盘最新的 easycallcenter365.jar、easycallcenter365-gui.jar 替换掉
+服务器上 /home/easycallcenter365/ 下面的文件。
+
+3.  下载网盘中的 upgrade/to_v20250817/upgrade.sql
+   然后打开数据库执行即可。
+   
+4. 重启程序 cd /home/easycallcenter365/ && sh stop.sh  && sh start.sh
+     
+
+

BIN
docs/manual/faq/callrecord-list.png


BIN
docs/manual/faq/calltask-list.png


BIN
docs/manual/faq/dify-app-type.png


BIN
docs/manual/faq/empty-number-detection-enabled.png


BIN
docs/manual/faq/llm-account-dify.png


BIN
docs/manual/faq/profile-list.png


BIN
docs/manual/faq/profile-menu.png


BIN
docs/manual/faq/profile-update.png


BIN
docs/manual/faq/tts-update.png


BIN
docs/manual/images/20250728084822.png


BIN
docs/manual/images/20250728090628.png


BIN
docs/manual/images/20250728090818.png


BIN
docs/manual/images/20250728091000.png


BIN
docs/manual/images/20250728092159.png


BIN
docs/manual/images/20250728092309.png


BIN
docs/manual/images/20250728092457.png


BIN
docs/manual/images/20250728092937.png


BIN
docs/manual/images/20250728093730.png


BIN
docs/manual/images/20250728094246.png


BIN
docs/manual/images/20250728094559.png


BIN
docs/manual/images/20250728094734.png


BIN
docs/manual/images/20250728095553.png


BIN
docs/manual/images/20250728095854.png


BIN
docs/manual/images/20250728100738.png


BIN
docs/manual/images/20250728100858.png


BIN
docs/manual/images/20250728101135.png


BIN
docs/manual/images/20250728102029.png


BIN
docs/manual/images/20250728102338.png


BIN
docs/manual/images/20250728102532.png


BIN
docs/manual/images/20250728102959.png


BIN
docs/manual/images/20250728103109.png


BIN
docs/manual/images/doubao_buy_voiceclone.png


BIN
docs/manual/images/doubao_buy_voiceclone20.png


BIN
docs/manual/images/doubao_create_project.png


BIN
docs/manual/images/doubao_icl_10_tts.png


BIN
docs/manual/images/doubao_icl_20_tts.png


BIN
docs/manual/images/doubao_icl_error-sample.png


BIN
docs/manual/images/doubao_icl_ok_sample_1.png


+ 237 - 0
docs/manual/manual.md

@@ -0,0 +1,237 @@
+## 操作手册
+
+
+
+### 1. 语音识别配置
+
+配置语音识别引擎,目前支持FunASR、讯飞ASR、阿里ASR。
+
+#### 1.1 FunASR配置
+
+根据部署的funasr服务配置相关参数,主要修改服务器地址,其他参数用默认值即可,如果funasr部署在同一台服务且端口没修改,该配置可以不修改
+
+![20250728084822](images/20250728084822.png)
+
+
+
+
+
+###  1.2 阿里ASR配置
+
+根据阿里云账号信息配置相关参数,主要修改Access-Key-Id、App-Key和Access-Key-Secret,其他参数用默认值即可
+
+![20250728090628](images/20250728090628.png)
+
+
+
+
+
+
+
+### 1.3 讯飞ASR配置
+
+根据讯飞开放平台账号信息配置相关参数,主要修改APP-ID、API-Key,其他参数用默认值即可
+
+![20250728090818](images/20250728090818.png)
+
+
+
+
+
+### 1.4 默认ASR设置
+
+选择当前需要启用的ASR配置
+
+![20250728091000](images/20250728091000.png)
+
+
+
+### 2. 语音合成配置
+
+配置语音合成引擎,目前支持阿里云tts配置
+
+### 2.1 阿里云tts配置
+
+根据阿里云账号信息配置相关参数,主要修改access_key_id、app_key和access_key_secret,其他参数用默认值即可
+
+![20250728092159](images/20250728092159.png)
+
+speech_rate (语速),取值范围:-500~500,默认值:0。
+[-500, 0, 500] 对应的语速倍速区间为 [0.5, 1.0, 2.0]。
+-500表示默认语速的0.5倍速。
+0表示默认语速的1倍速。1倍速是指模型默认输出的合成语速,语速会依据每一个发音人略有不同,大概每秒钟4个字左右。
+500表示默认语速的2倍速。
+
+语速会依据每一个发音人略有不同。
+这里也是说在使用不同发音人的时候,要设置不同的speech_rate (语速)。没有一个统一的固定值。
+
+那到底如何填写呢。一个简单的办法,先填写默认值0。
+如果语速快了,再改为一个负数,比如-100。
+如果语速慢了,在改为一个正数,比如100。如此多次尝试。
+
+### 2.2 豆包语音复刻和语音合成
+
+a. 创建应用
+
+首先登录火山引擎后台,创建应用,点击连接 https://console.volcengine.com/speech/app?projectName=default  。
+![火山后台创建应用](images/doubao_create_project.png) 
+
+记得勾选 "豆包语音合成模型2.0字符版"、"豆包声音复刻模型2.0字符版"、"语音合成大模型 字符版"、"声音复刻大模型 字符版"、"语音合成" 合计5个选项。
+
+b. 购买声音复刻音色
+
+![购买豆包声音复刻音色1.0](images/doubao_buy_voiceclone.png) 
+
+![购买豆包声音复刻音色2.0](images/doubao_buy_voiceclone20.png) 
+
+
+c. 克隆自己的声音
+
+登录到easyCallcenter365管理后台,找到菜单"基础配置"->"语音合成配置"->"豆包语音克隆"。
+请预先录制好 20-30 秒左右的声音,请确保环境安静且无噪音。  既可以使用电脑加耳机组合,使用电脑自带的录音机录制; 也可以使用手机自带的录音机,把录制好的录音文件发送到电脑。
+所有参数选择设置好之后,点击上传原始录音文件后,会自动启动声音克隆,这个过程大概10秒左右。请耐心等待。
+![豆包声音复刻音色](images/doubao_icl_ok_sample_1.png) 
+
+这里注意:如果填写的"声音ID"是2.0下面的,"模型类型"必须选择 "声音复刻ICL2.0效果"。
+如果填写的"声音ID"是1.0下面的,"模型类型"可选择 "声音复刻ICL1.0效果"和"DiT标准版效果"以及"DiT还原版效果"。否则会报错。
+
+![豆包声音复刻音色](images/doubao_icl_error-sample.png) 
+
+复刻成功之后,可以使用训练好的音色进行语音合成测试。
+
+![豆包声音复刻音色](images/doubao_icl_20_tts.png) 
+
+![豆包声音复刻音色](images/doubao_icl_10_tts.png) 
+
+注意:声音复刻2.0目前仅支持中英文,暂不支持其他语种。声音复刻1.0额外支持更多语种(日语、西班牙语、印尼语、葡萄牙语、德语、法语)。
+
+### 3. 线路配置
+
+配置语音网关,支持对接模式和注册模式,并且支持根据外呼任务的不同配置不同用途的网关
+
+![20250728092309](images/20250728092309.png)
+
+![20250728092457](images/20250728092457.png)
+
+
+### 4. 大模型配置
+
+配置大模型或者智能体底座信息,用于AI外呼和AI呼入的机器人应答的数据接入,支持直接对接类deepseek,直接对接类chatgpt、对接coze、对接dify、对接maxkb,并支持开启关键词打断功能并配置打断关键词
+
+![20250728092937](images/20250728092937.png)
+
+
+
+### 4.1 类deepseek
+
+支持阿里云百炼对接的deepseek模型,理论上可以支持所有通过url、apiKey、模型名称三个参数对接大模型接口的开放平台和模型,并根据具体业务场景配置大模型提示词、FAQ内容、转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728093730](images/20250728093730.png)
+
+
+
+
+
+### 4.2 coze
+
+支持对接扣子智能体,根据扣子账号信息配置服务地址、botId、token,其中token支持pat和oauth两种方式,详细信息请参考扣子官网说明,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094246](images/20250728094246.png)
+
+
+
+### 4.3 dify
+
+支持对接dify智能体,根据dify账号配置apiKey、服务地址参数,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094559](images/20250728094559.png)
+
+
+
+### 4.4 maxkb
+
+支持对接maxkb智能体,根据maxkb账号配置apiKey、服务地址,并根据具体业务场景配置转人工提示、挂机提示、客户不说话提示和开场白
+
+![20250728094734](images/20250728094734.png)
+
+
+
+### 4.5 打断
+
+支持关键词打断配置,开启打断后需要配置打断关键词和忽略打断关键词,打断关键词指的是客户说话命中打断,忽略打断关键词指的是命中不打断(比如一些语气词)
+
+![20250728095553](images/20250728095553.png)
+
+
+
+### 5. 外呼任务管理
+
+支持创建外呼任务,支持AI外呼、人工预测式外呼和语音通知,配置好外呼任务以后,在任务列表点击“导入数据”(导入模板在配置外呼任务的页面可以下载),选择excell文件导入外呼名单(名单尽量不要超过10w条数据),导入完成后点击“启动任务”按钮开始外呼,外呼后可以在该页面查看已拨打量、接通量、接通率等统计数据,可以在AI外呼->AI外呼记录查看具体的通话记录
+
+![20250728095854](images/20250728095854.png)
+
+
+
+### 5.1 AI外呼
+
+AI外呼任务,指的是创建并启动任务以后,拨打客户电话,接通后有配置的大模型底座作为大脑生成应答话术,配置的音色作为嘴巴生成实时语音跟客户沟通。
+
+配置AI外呼任务,需要配置外呼的最大并发,外呼使用的线路(可选项在基础配置->线路配置功能中配置),外呼使用的大模型底座(可选项在基础配置->大模型配置中配置),机器人应答使用的音色(内置的阿里云tts的音色)
+
+![20250728100738](images/20250728100738.png)
+
+
+
+### 5.2 人工预测式外呼
+
+人工预测外呼任务指的是创建外呼任务后,系统根据空闲坐席的数量进行自动外呼,接通后转给空闲坐席(人工坐席)跟客户沟通
+
+![20250728101135](images/20250728101135.png)
+
+
+
+### 5.3 语音通知
+
+语音通知指的是,创建并启动任务后,拨打客户电话,接通后播报一句提醒话术后挂机(话术内容在导入时配置)
+
+![20250728102029](images/20250728102029.png)
+
+
+
+### 6. AI外呼记录查询
+
+查询外呼任务管理创建的任务的外呼拨打生成的记录,支持在线听录音、下载录音和查看文本对话
+
+![20250728102338](images/20250728102338.png)
+
+
+
+
+
+### 7. 呼入配置
+
+
+
+配置呼入(客服业务)相关参数
+
+![20250728102532](images/20250728102532.png)
+
+
+
+呼入需要配置大模型底座(可选项在基础配置->大模型配置中配置),音色,被叫号码(即客服号码),服务方式(ai,acd),转人工业务组
+
+![20250728102959](images/20250728102959.png)
+
+
+
+
+
+
+
+### 8. 呼入记录查询
+
+
+
+查询呼入的通话记录,支持在线听录音、下载录音和查看文本对话
+
+![20250728103109](images/20250728103109.png)

+ 221 - 0
docs/page.css

@@ -0,0 +1,221 @@
+@charset "utf-8";
+/* CSS Document */
+body{ background:#eef3fa;}
+
+html,body,table,tr,td,div,ol,ul,li,dl,dt,dd,dir,h1,h2,h3,h4,h5,h6,p{text-align: left; margin: 0px;padding: 0px;border: 0px;list-style-type: none; font-size:12px; font-weight:normal;}
+div,select,textarea,table td,table td,table tr{font-family:myFirstFont;font-size:12px; color:#768595;}
+
+.main{ margin:0px 10px;}
+
+/***头部区***/
+.head_btn{ height:50px; margin-bottom:10px;}
+
+#navigation {font: bold 12px/18px "微软雅黑", Helvetica, Arial, sans-serif; text-transform: uppercase; color:#768595; position:absolute; top:10px;}  
+#navigation:after { clear: both;content: "."; display: block; height: 0;visibility: hidden;}  
+#navigation ul {float: left; border-radius: 5px;/* box-shadow: 0 2px 2px rgba(0, 0, 0, 0.07); -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.07); */overflow: hidden;}  
+#navigation li {  
+    float: left;  
+    border-style: solid;   
+    border-width: 1px;  
+    border-color: #dfe5e7 #dfe5e7 #dfe5e7 #dfe5e7;  
+    box-shadow: 0 1px rgba(255,255,255,1) inset;  
+    -webkit-box-shadow: 0 1px rgba(255,255,255,1) inset;  
+    background: #F7F7F7; /* Old browsers */  
+    background: -moz-linear-gradient(top, #F7F7F7 0%, #EDEDED 100%); /* FF3.6+ */  
+    background: -webkit-gradient(linear, left top, left bottombottom, color-stop(0%,#F7F7F7), color-stop(100%,#EDEDED)); /* Chrome,Safari4+ */  
+    background: -webkit-linear-gradient(top, #F7F7F7 0%,#EDEDED 100%); /* Chrome10+,Safari5.1+ */  
+    background: -o-linear-gradient(top, #F7F7F7 0%,#EDEDED 100%); /* Opera 11.10+ */  
+    background: -ms-linear-gradient(top, #F7F7F7 0%,#EDEDED 100%); /* IE10+ */  
+    background: linear-gradient(top, #F7F7F7 0%,#EDEDED 100%); /* W3C */  
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F7F7F7', endColorstr='#EDEDED',GradientType=0 ); /* IE6-9 */      
+}  
+#navigation li:hover, #navigation li.current {  
+    box-shadow: 0 1px rgba(255,255,255,0.2) inset;  
+    -webkit-box-shadow: 0 1px rgba(255,255,255,0.2) inset;  
+    border-color: #095db1 !important;  
+    background: #095db1; /* Old browsers */  
+    background: -moz-linear-gradient(top, #095db1 0%, #095db1 100%); /* FF3.6+ */  
+    background: -webkit-gradient(linear, left top, left bottombottom, color-stop(0%,#095db1), color-stop(100%,#095db1)); /* Chrome,Safari4+ */  
+    background: -webkit-linear-gradient(top, #095db1 0%,#ff000 100%); /* Chrome10+,Safari5.1+ */  
+    background: -o-linear-gradient(top, #095db1 0%,#095db1 100%); /* Opera 11.10+ */  
+    background: -ms-linear-gradient(top, #095db1 0%,#095db1 100%); /* IE10+ */  
+    background: linear-gradient(top, #095db1 0%,#095db1 100%); /* W3C */  
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#095db1', endColorstr='#095db1',GradientType=0 ); /* IE6-9 */  
+}  
+#navigation a {display: block;padding: 5px 15px;color: #444;text-decoration: none; text-shadow: 0 1px #FFF; }  
+#navigation a:hover, #navigation li.current a { color: #FFF;text-shadow: 0 1px #000;}  
+#navigation li:first-child {border-left-color: #BABABA; border-radius: 5px 0 0 5px;}  
+#navigation li:last-child {border-radius: 0 5px 5px 0; } 
+
+#conference_member_list ul {float: left; overflow: hidden;  width: 100%;}
+#conference_member_list li  {
+    float: left;  
+    border-style: solid;   
+    border-width: 1px;  
+    border-color: #dfe5e7 #dfe5e7 #eef3fa #dfe5e7;  
+    width: 700px;
+    line-height: 35px;
+}
+
+
+.conf_name{
+   padding-left: 10px;
+   width: 100px;
+   display: inline-block
+}
+
+.conf_phone{
+   width: 120px;
+   display: inline-block;
+}
+
+.conf_call_type{
+    padding-left: 10px;
+    width: 50px;
+    display: inline-block
+}
+
+.conf_video_level{
+    padding-left: 10px;
+    padding-right: 10px;
+    width: 50px;
+    display: inline-block
+}
+
+.conf_status{
+    width: 120px;
+    display: inline-block;
+}
+
+.conf_mute{
+    width: 40px;
+    display: inline-block;
+    padding-top: 3px;
+}
+
+.conf_vmute{
+    width: 40px;
+    display: inline-block;
+    padding-top: 3px;
+}
+
+.conf_remove{
+   width: 35px;
+    display: inline-block;
+}
+.conf_re_invite{
+    width: 35px;
+    display: inline-block;
+}
+ 
+.head_dial{ padding-top:2px;}
+.dial{ float:left;}
+.dial dt{ padding-bottom:2px;}
+.dial dt input{vertical-align:middle;}
+.tel_txt{ border:1px solid #dfe5e7; border-radius:3px;  height:35px; font-size:16px; background:#FFF; color:#768595; text-indent:5px; width:120px;}
+.keyboard{ background:url(./images/keyboard.png) no-repeat; border:0; width:25px; height:18px; cursor:pointer; margin:auto 10px auto 2px;}
+.next_btn{border-radius:5px; background: url(./images/next.png) no-repeat 10px 5px #8b9eb6; width:120px; height:25px; line-height:25px; border:0; color:#FFF;  text-align:center; cursor:pointer; text-indent:20px;}
+.dial dd input,.dial dd ul{ float:left;}
+.dial dd ul{border:1px solid #dfe5e7; background:#FFF; border-radius:3px;  height:auto; width:118px; margin-left:2px;}
+.dial dd ul li{height:auto; line-height:23px; margin-left:10px; padding-left:16px;}
+.dial dd ul li.status1{ color:#f39700;}
+.dial dd ul li.status2{ color:#98bf40;}
+.dial dd ul li.status3{ color:#98bf40;}
+.dial dd ul li.status4{ color:#23a9f6;}
+.dial dd ul li.status5{ color:#adadad;}
+.dial dd b{ line-height:23px; padding-left:1px;}
+
+.dial_btn{ float:left;}
+.dial_btn li{ float:left; margin-left:12px;text-align:center;}
+.dial_btn li a{ display:block; border-radius:10px; border:1px solid #dfe5e7; width:48px; height:48px; margin:4px auto;}
+ 
+
+.wh_btn,.gj_btn,.zj_btn,.bc_btn,.zjie_btn,.myd_btn,.jt_btn,.qca_btn,.lj_btn,.qc_btn,.hy_btn,.sx_btn,.xx_btn,.wc_btn,.jy_btn,.xz_btn,.sm_btn,.qz_btn,.qiangjie_btn,.lanjie_btn{background:url(./images/dial_off.png) no-repeat;background-color:#FFF;}
+.wh_btn.on,.zj_btn.on,.gj_btn.on,.bc_btn.on,.zjie_btn.on,.myd_btn.on,.jt_btn.on,.qca_btn.on,.lj_btn.on,.qc_btn.on,.hy_btn.on,.sx_btn.on,.xx_btn.on,.wc_btn.on,.jy_btn.on,.xz_btn.on,.sm_btn.on{ background:url(./images/dial_on.png) no-repeat;}
+
+.wh_btn,.wh_btn.on{ background:url(./images/phonebar/call.png) no-repeat;  -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 外呼 */
+.wh_btn.on{ background:url(./images/phonebar/call.png) no-repeat;  -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.gj_btn,.gj_btn.on{background:url(./images/phonebar/hangup_enable.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 挂机*/
+.gj_btn.on{ background:url(./images/phonebar/hangup_enable.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.zj_btn,.zj_btn.on{ background-position:-61px -1px;}/* 摘机*/
+.zj_btn.on{background-color:#999999; border:1px solid #999999;}
+
+.bc_btn,.bc_btn.on{background:url(./images/phonebar/hold.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 保持 */
+.bc_btn.on{background:url(./images/phonebar/hold.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}
+
+.bc2_btn,.bc2_btn.on{background:url(./images/phonebar/unhold.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 保持 */
+.bc2_btn.on{background:url(./images/phonebar/unhold.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}
+
+.zjie_btn,.zjie_btn.on{background:url(./images/phonebar/transfer.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 转接 */
+.zjie_btn.on{background:url(./images/phonebar/transfer.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}
+
+.zixun_btn,.zixun_btn.on{background:url(./images/phonebar/consultation.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 转接 */
+.zixun_btn.on{background:url(./images/phonebar/consultation.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}
+
+.myd_btn,.myd_btn.on{background-position:-151px -2px; }/* 满意度 */
+.myd_btn.on{ background-color:#f7ae2d; border:1px solid #f7ae2d;}
+.jt_btn,.jt_btn.on{background:url(./images/phonebar/mute.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}/* 监听 */
+.jt_btn.on{background:url(./images/phonebar/mute.png) no-repeat;-webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.qca_btn,.qca_btn.on{ background-position:-211px -1px;}/* 强插 */
+.qca_btn.on{ background-color:#ed4e4e; border:1px solid #ed4e4e;}
+.lj_btn,.lj_btn.on{ background-position:-242px 0px;}/* 拦截 */
+.lj_btn.on{ background-color:#999999; border:1px solid #999999;}
+.qc_btn,.qc_btn.on{ background-position:-271px -1px;}/* 强拆 */
+.qc_btn.on{ background-color:#ed4e4e; border:1px solid #ed4e4e;}
+.hy_btn,.hy_btn.on{ background:url(./images/phonebar/conference.png) no-repeat;}/* 会议 */
+.hy_btn.on{background:url(./images/phonebar/conference.png) no-repeat;-webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.sx_btn,.sx_btn.on{  background:url(./images/phonebar/online.png) no-repeat;}/* 上线 */
+.sx_btn.on{background:url(./images/phonebar/online.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.xx_btn,.xx_btn.on{ background-position:-360px 0px;}/* 休息 */
+.xx_btn.on{ background-color:#23a9f6; border:1px solid #23a9f6;}
+.wc_btn,.wc_btn.on{ background-position:-392px 0px;}/* 外出 */
+.wc_btn.on{ background-color:#d51776; border:1px solid #d51776;}
+.jy_btn,.jy_btn.on{ background-position:-421px -1px;}/* 静音 */
+.jy_btn.on{ background-color:#999999; border:1px solid #999999;}
+.xz_btn,.xz_btn.on{ background:url(./images/phonebar/setFree.png) no-repeat;  -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;} /* 置闲 */
+ .xz_btn.on{background:url(./images/phonebar/setFree.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}  
+.sm_btn,.sm_btn.on{ background:url(./images/phonebar/busy_enable.png) no-repeat; -webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;} /* 置忙 */
+.sm_btn.on{background:url(./images/phonebar/busy_enable.png) no-repeat; -webkit-filter: grayscale(1%);-moz-filter: grayscale(1%);-ms-filter: grayscale(1%); -o-filter: grayscale(1%);filter: grayscale(1%);filter: gray;}   
+.qz_btn,.qz_btn.on{background:url(./images/phonebar/reset.png) no-repeat;}/* 强置 */
+.qz_btn.on{-webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}.lanjie_btn,.lanjie_btn.on{background:url(./images/phonebar/lanjie.png) no-repeat;}/* 拦截 */
+.lanjie_btn.on{-webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}
+qiangjie_btn,.qiangjie_btn.on{background:url(./images/phonebar/qiangjie.png) no-repeat;}/* 抢接 */
+.qiangjie_btn.on{-webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%); -o-filter: grayscale(100%);filter: grayscale(100%);filter: gray;}   
+
+/* asr实时消息的对话框 */
+#chat-container {
+    width: 90%;
+    max-width: 600px;
+    margin: 20px auto;
+    background: white;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    padding: 10px;
+}
+.message {
+    padding: 10px;
+    margin: 10px 0;
+    border-radius: 8px;
+}
+.customer {
+    background-color: #F5F5F5;
+    align-self: flex-start;
+}
+.agent {
+    background-color: #95EC69;
+    align-self: flex-end;
+}
+.system-message {
+    text-align: center;
+    color: gray;
+    font-style: italic;
+}
+.message-container {
+    display: flex;
+    flex-direction: column;
+}
+.message-header {
+    font-weight: bold;
+    margin-bottom: 5px;
+}

+ 767 - 0
docs/phone-bar-ex.html

@@ -0,0 +1,767 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>呼叫中心html客户端工具条</title>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <link rel="stylesheet" href="page.css" />
+    <script type="text/javascript" src="jquery-1.11.0.js"></script>
+    <script type="text/javascript" src="ccPhoneBarSocket.js"></script>
+	  
+    <script type="text/javascript">
+        var _phoneBar = new ccPhoneBarSocket();
+        var scriptServer = "192.168.67.217";
+        var  extnum = '1001'; //分机号
+        var opnum = '1001'; //工号
+        var skillLevel = 9; //技能等级
+        var groupId = 1; // 业务组id
+        if(window.location.href.toString().indexOf("?") != -1){
+            console.log( ccPhoneBarSocket.utils );
+            extnum = ccPhoneBarSocket.utils.getQueryParam("extNum");
+            opnum = ccPhoneBarSocket.utils.getQueryParam("opNum");
+            groupId = ccPhoneBarSocket.utils.getQueryParam("groupId");
+            console.log("extNum=", extnum, "opNum=", opnum);
+        }
+
+        function resetExtNumAndOpNum(ext, op, groupId) {
+            window.location.href = "?extNum=" + ext + "&opNum=" + op + "&groupId=" + groupId;
+        };
+
+		(function loadLoginToken(){
+
+             
+            var getTokenUrl = "http://"+ scriptServer +":8880/call-center/create-token";
+            var destUrl = getTokenUrl + "?extnum=" + extnum + "&opnum=" + opnum
+            + "&groupId=" + groupId +"&skillLevel=" + skillLevel
+            ;
+
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = destUrl;
+            document.getElementsByTagName('head')[0].appendChild(script);
+		})();
+
+        (function loadExtPassword(){
+            var  extPassword = '1234567';
+            var url = "http://"+ scriptServer +":8880/call-center/create-ext-password?pass=" + extPassword;
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = url;
+            document.getElementsByTagName('head')[0].appendChild(script);
+        })();
+
+        (function loadGatewayList(){
+            var url = "http://"+ scriptServer +":8880/call-center/create-gateway-list" ;
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = url;
+            document.getElementsByTagName('head')[0].appendChild(script);
+        })();
+
+        // 将视频级别填充到下拉列表中的函数
+        function populateVideoLevelDropdown(objId) {
+            let select = document.getElementById(objId);
+            if(select == null) return;
+            // 遍历视频级别数据
+            for (let key in ccPhoneBarSocket.videoLevels) {
+                if (ccPhoneBarSocket.videoLevels.hasOwnProperty(key)) {
+                    let level = ccPhoneBarSocket.videoLevels[key];
+                    let option = document.createElement('option');
+                    option.value = level.levelId; // 设置值为 levelId
+                    option.text = level.description; // 显示文本
+                    select.appendChild(option);
+                }
+            }
+            select.value = ccPhoneBarSocket.videoLevels.HD.levelId ;
+        }
+
+	</script>
+
+<script> 
+var _callConfig = null;
+
+window.onload = function(){
+
+    // 调用函数填充视频清晰度的下拉列表
+    populateVideoLevelDropdown('videoLevelSelect');
+    populateVideoLevelDropdown('member_video_level');
+
+	//工具条对象断开事件
+	// _phoneBar.on(ccPhoneBarSocket.eventList.ws_disconnected, function(msg){
+	// 	console.log(msg);
+	// });
+    //
+    // //工具条对象连接成功
+    // _phoneBar.on(ccPhoneBarSocket.eventList.ws_connected, function(msg){
+    //     console.log(msg);
+    // });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_ringing, function(msg){
+	// 	console.log(msg.content, "被叫振铃事件");
+	// });
+	// _phoneBar.on(ccPhoneBarSocket.eventList.caller_answered, function(msg){
+	// 	console.log(msg, "主叫接通" );
+	// });
+    // _phoneBar.on(ccPhoneBarSocket.eventList.caller_hangup, function(msg){
+    //     console.log(msg, "主叫挂断");
+    // });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_answered, function(msg){
+	// 	console.log(msg, "被叫接通");
+	// });
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_hangup, function(msg){
+	// 	console.log(msg, "被叫挂断");
+	// });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.status_changed, function(msg){
+	// 	console.log("座席状态改变: " ,msg);
+	// });
+    //
+    // // 一次外呼结束;
+    // _phoneBar.on(ccPhoneBarSocket.eventList.outbound_finished, function(msg){
+    // 	console.log('一次外呼结束', msg);
+    // });
+
+    // websocket通信对象断开事件;
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.ws_disconnected.code, function(msg){
+        console.log(msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.ws_disconnected.code);
+        $("#transfer_area").hide();
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.OUTBOUND_START, function (msg) {
+        console.log('outbound_start',msg);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.request_args_error.code, function(msg){
+        console.log(msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.request_args_error.code);
+    });
+
+    //用户已在其他设备登录
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.code, function(msg){
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.code);
+        alert(ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.msg);
+    });
+
+    //websocket连接成功
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.ws_connected.code, function(msg){
+        console.log(msg);
+        $("#loginTime").text(new Date().toLocaleTimeString());
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.ws_connected.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_ringing.code, function(msg){
+        console.log(msg.content, "被叫振铃事件");
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_ringing.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.caller_answered.code, function(msg){
+        console.log(msg, "主叫接通" );
+        $("#agentStatus").text("通话中");
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.caller_answered.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.caller_hangup.code, function(msg){
+        console.log(msg, "主叫挂断");
+        $("#agentStatus").text("通话结束");
+        $("#reInviteVideoBtn").attr("disabled","disabled");
+        $("#sendVideoFileBtn").attr("disabled","disabled");
+        $("#transfer_area").hide();
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.caller_hangup.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_answered.code, function(msg){
+        console.log(msg, "被叫接通");
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_answered.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_hangup.code, function(msg){
+        console.log(msg, "被叫挂断");
+        $("#transfer_area").hide();
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_hangup.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.status_changed.code, function(msg){
+        console.log("座席状态改变: " ,msg);
+        $("#agentStatus").text(msg["object"]["text"]);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.status_changed.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.acd_group_queue_number, function(msg){
+        console.log("当前排队人数消息: " ,msg);
+        $("#queueStat").text(msg["object"]["queue_number"]);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.on_audio_call_connected, function(msg){
+        console.log("音频通话已建立: " ,msg);
+        $("#reInviteVideoBtn").removeAttr("disabled");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.customer_channel_hold, function (msg) {
+        console.log("客户通话已保持: " ,msg);
+        $("#callStatus").text("通话已保持");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.customer_channel_unhold, function (msg) {
+        console.log("客户通话已接回." ,msg);
+        $("#callStatus").text("客户通话已接回");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.on_video_call_connected, function(msg){
+        console.log("视频通话已建立: " ,msg);
+        $("#sendVideoFileBtn").removeAttr("disabled");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.inner_consultation_start, function(msg){
+        $("#callStatus").text("咨询开始.");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.inner_consultation_stop, function(msg){
+        $("#callStatus").text("咨询结束.");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.transfer_call_success, function(msg){
+        $("#callStatus").text("电话转接成功.");
+        $("#externalPhoneNumber").val('');
+    });
+
+    // 订阅的坐席状态列表发生改变
+    _phoneBar.on(ccPhoneBarSocket.eventList.agent_status_data_changed, function (msg) {
+        console.log("agent_status_data_changed.");
+        // 当 transfer_to_groupId 值改变时更新 transfer_to_member
+        $(transferToGroupId).off("change");
+        $(transferToGroupId).on("change", function () {
+            refreshMemberIdList();
+        });
+        refreshMemberIdList();
+    });
+
+    /* conference related events  */
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_ANSWERED, function (msg) {
+        console.log("会议成员已经接通.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var memberItemId = "#conf_member_" + memberPhone;
+
+        $(".conf_status", $(memberItemId)).text(msg.object.status);
+        $(".conf_status", $(memberItemId)).html("通话中").css("color", "green");
+
+        $(".conf_mute", $(memberItemId)).find("img").show();
+        $(".conf_vmute", $(memberItemId)).find("img").show();
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_VMUTED_SUCCESS, function (msg) {
+        console.log("会议成员已被禁用视频.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var muteObj = $(".conf_vmute", $("#conf_member_" + memberPhone));
+        muteObj.find("img")[0].src = "images/no_video.jpg";
+        muteObj.find("a").removeAttr("onclick");
+        muteObj.find("a").off("click");
+        muteObj.find("a").on("click", function () {
+            _phoneBar.conferenceUnVMuteMember(memberPhone);
+        } );
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_UnVMUTED_SUCCESS, function (msg) {
+        console.log("会议成员启用视频成功.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var muteObj = $(".conf_vmute", $("#conf_member_" + memberPhone));
+        muteObj.find("img")[0].src = "images/video.jpg";
+        muteObj.find("a").removeAttr("onclick");
+        muteObj.find("a").off("click");
+        muteObj.find("a").on("click", function () {
+            _phoneBar.conferenceVMuteMember(memberPhone);
+        } );
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_MUTED_SUCCESS, function (msg) {
+        console.log("会议成员已被禁言.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var muteObj = $(".conf_mute", $("#conf_member_" + memberPhone));
+        muteObj.find("img")[0].src = "images/unmute.jpg";
+        muteObj.find("a").removeAttr("onclick");
+        muteObj.find("a").off("click");
+        muteObj.find("a").on("click", function () {
+            _phoneBar.conferenceUnMuteMember(memberPhone);
+        } );
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_UNMUTED_SUCCESS, function (msg) {
+        console.log("会议成员解除禁言成功.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var muteObj = $(".conf_mute", $("#conf_member_" + memberPhone));
+        muteObj.find("img")[0].src = "images/mute.jpg";
+        muteObj.find("a").removeAttr("onclick");
+        muteObj.find("a").off("click");
+        muteObj.find("a").on("click", function () {
+            _phoneBar.conferenceMuteMember(memberPhone);
+        } );
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MEMBER_HANGUP, function (msg) {
+        console.log("会议成员已经挂机.", msg);
+        var memberPhone = $.trim(msg.object.phone);
+        var memberItemId = "#conf_member_" + memberPhone;
+
+        // 隐藏 mute及 vmute按钮
+        $(".conf_mute", $(memberItemId)).find("img").hide();
+        $(".conf_vmute", $(memberItemId)).find("img").hide();
+        $(".conf_re_invite", $(memberItemId)).show();
+
+        var answerStatus = ( msg.object.answeredTime === 0) ? "未接通" :  msg.object.hangupClause;
+        var color = ( msg.object.answeredTime === 0) ? "red" : "green";
+        $(".conf_status", $(memberItemId)).html("已挂机("+ answerStatus  +")").css("color", color);
+        $(".conf_status", $(memberItemId)).fadeTo('fast', 0.1).fadeTo('fast', 1.0);
+        var blinkText = setInterval(function () {
+            $(".conf_status", $(memberItemId)).fadeTo('fast', 0.1).fadeTo('fast', 1.0);
+        }, 700); // 每0.5秒闪烁一次
+
+        setTimeout(function () {
+            console.log("memberItemId=", memberItemId);
+            clearInterval(blinkText);
+            // $(memberItemId).remove(); //暂不自动移除参会者,由主持人手动操作处理;
+        }, 5000);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MODERATOR_ANSWERED, function (msg) {
+        console.log("电话会议开始,主持人已接通.", msg);
+        onConferenceStart();
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_MODERATOR_HANGUP, function (msg) {
+        console.log("电话会议结束,主持人已挂机.", msg);
+        onConferenceEnd();
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.CONFERENCE_TRANSFER_SUCCESS_FROM_EXISTED_CALL, function (msg) {
+        console.log("成功把通话转接到多人视频会议.", msg);
+        onTransferToConferenceSuccess(msg);
+    });
+
+    var _gatewayList = [{"gatewayAddr":"192.168.67.217:5080","callProfile":"internal","authUsername":"1002","callerNumber":"13195510173\r\n13195510174\r\n13195510188","updateTime":1769767068989,"calleePrefix":"","priority":1,"audioCodec":"pcma","uuid":"3","concurrency":2,"register":0}];
+
+    // 电话工具条参数配置;
+	_callConfig = {
+        'useDefaultUi' : true,
+        // loginToken 信息是加密的字符串, 包含以下字段信息:extnum[分机号]、opnum[工号]、groupId[业务组]、skillLevel[技能等级]
+        'loginToken': '',
+
+         // 电话工具条服务器端的地址; 端口默认是1081
+        'ipccServer': scriptServer + ':1081',
+
+        // 网关列表, 默认需要加密后在在通过客户端向呼叫系统传递;
+        // 注意在注册模式下,网关参数更改之后,必须重启语音服务 [docker restart freeswitch] 方可生效,不支持热更新;
+        // 支持多个网关同时使用,按照优先级依次使用, 支持网关负载容错溢出 [第一条网关外呼出错后,自动使用第二条网关重试,直至外呼不出错] ;
+        'gatewayList':  _gatewayList,
+
+        // 网关列表信息是否为加密模式;
+        'gatewayEncrypted': false
+    };
+
+    // 使用工具条之前需要先初始化 _callConfig 参数, 填充各个字段的值: 合计7个字段,必须填写正确 ;
+	//********************************************************************************************
+    // 以下代码设置加密的参数: loginToken、extPassword、gatewayList;   在本页面的demo演示中需要调用服务器端接口获取密文字符串;
+
+	console.log('loginToken = ',loginToken);
+	if(typeof(loginToken) != "undefined") {
+        _callConfig["loginToken"] = loginToken;
+    } else{
+	    alert("电话工具条:无法获取 loginToken!");
+	    return;
+    }
+
+    console.log('_phoneEncryptPassword = ',_phoneEncryptPassword);
+    if(typeof(_phoneEncryptPassword) != "undefined") {
+        _callConfig["extPassword"] = _phoneEncryptPassword;
+    } else{
+        alert("电话工具条:无法获取 _phoneEncryptPassword!");
+        return;
+    }
+
+    console.log('_configGatewayList = ', _configGatewayList);
+    if(typeof(_configGatewayList) != "undefined" && _callConfig["gatewayEncrypted"]) {
+        //_callConfig["gatewayList"] = _configGatewayList;
+    } else{
+       //  alert("电话工具条:无法获取 _configGatewayList!");
+    }
+
+    _phoneBar.initConfig(_callConfig);
+};
+
+function onConferenceEnd() {
+    document.getElementById("endConference").setAttribute("disabled", "true");
+    document.getElementById("startConference").removeAttribute("disabled");
+    document.getElementById("conference_member_list").style.display = "none";
+    // 启用外呼按钮
+    $("#callBtn").addClass('on');
+    // 启用置闲按钮
+    $("#setFree").addClass('on');
+    // 启用签出按钮
+    $("#onLineBtn").addClass('on');
+    //移除所有的参会成员
+    $(".conf_member_item_row").remove();
+
+    let tips = "多方通话结束";
+    $("#callStatus").text(tips);
+    $("#agentStatus").text(tips);
+}
+
+function onConferenceStart() {
+    document.getElementById("endConference").removeAttribute("disabled");
+    document.getElementById("conference_member_list").style.display = "block";
+    let tips = "多方通话进行中";
+    $("#callStatus").text(tips);
+    $("#agentStatus").text(tips);
+}
+
+/**
+ *  成功把电话转接到多人视频会议
+ */
+function onTransferToConferenceSuccess(msg) {
+    $("#callStatus").text("已接入多方会议");
+    $("#setFree").removeClass("on");
+    $("#setBusy").removeClass("on");
+    $("#callBtn").removeClass("on");
+
+    //界面显示成功转接到视频会议电话
+    var phone = msg.object.phone;
+    var name = msg.object.phone;
+    console.log("onTransferToConferenceSuccess:", msg);
+    _phoneBar.conferenceAddMemberFromExistCall(name, phone);
+}
+
+
+</script>
+
+</head>
+<body>
+
+<form>
+
+<table width="1224">
+<tr>
+  <td width="70%" colspan="2" height="35" style="text-indent: 20px;">
+      <b>签入时间:</b> <span id="loginTime" title="" class="status4">00:00:00</span> &nbsp;&nbsp;
+      <b>状态:</b> <span id="agentStatus" title="" class="status4">空闲</span> &nbsp;&nbsp;
+      <b>当前排队人数:</b><span id="queueStat" title="" class="status4">0</span>
+
+  </td>
+</tr>
+<tr>
+<td width="70%">
+<div>
+    <div class="head_dial" style="padding-left: 10px; ">
+	     
+        <dl class="dial">
+            <dt>
+                <label for="ccphoneNumber"></label><input type="text" name="ccphoneNumber" id="ccphoneNumber" placeholder="输入电话号码" class="tel_txt" />
+            </dt>
+            <dd>
+                <ul><li id="callStatus" title="" class="status4">没有连接</li></ul>
+                <span id="showCallLen" style="display:none"><b>00:00</b></span>
+            </dd>
+        </dl>
+
+        <ul class="dial_btn">
+            <li><a href="#" id="setFree" class="xz_btn off"></a><span>置闲</span></li>
+            <li><a href="#" id="setBusy" class="sm_btn off"></a>
+                <select style="width: 50px;" id="setBusySubList">
+                    <option value="3">置忙</option>
+                    <option value="31">小休</option>
+                    <option value="32">会议</option>
+                    <option value="33">培训</option>
+                </select>
+            </li>
+            <li><a href="#" id="callBtn" class="wh_btn"></a><span>外呼</span></li>
+            <li id="holdBtnLi"><a href="#" id="holdBtn" class="bc_btn off"></a><span>保 持</span></li>
+            <li id="unHoldBtnLi"><a href="#" id="unHoldBtn" class="bc2_btn off"></a><span>取消保持</span></li>
+            <li><a href="#" id="transferBtn" class="zjie_btn"></a><span>转接</span></li>
+            <li><a href="#" id="consultationBtn" class="zixun_btn"></a><span>咨询</span></li>
+            <li><a href="#" id="conferenceBtn" class="hy_btn"></a><span>会议</span></li>
+            <li><a href="#" id="hangUpBtn" class="gj_btn"></a><span>挂机</span></li>
+            <li><a href="#" id="resetStatus" class="qz_btn"></a><span>强置</span></li>
+            <li><a href="#" id="onLineBtn" class="sx_btn on"></a><span>签入</span></li>
+        </ul>
+    </div>
+</div>
+</td>
+<td width="30%" style="display: none;">
+<div>
+    <div  style="padding-left: 10px; ">
+        &nbsp; &nbsp;  外呼设置:
+        <label for="videoCallBtn"> <input type="radio" value="video" name="callType" id="videoCallBtn" />视频外呼</label>  &nbsp;&nbsp;
+        <label for="audioCallBtn"> <input type="radio" value="audio"  name="callType" checked="checked" id="audioCallBtn"  />语音外呼</label>  <br />
+
+        &nbsp; &nbsp; 视频清晰度:
+        <label for="videoLevelSelect"></label><select id="videoLevelSelect">
+        </select>
+        <input type="button" id="reInviteVideoBtn" title="发送视频邀请,可把音频通话转换为视频通话。"
+         onclick="_phoneBar.reInviteVideoCall();"       value="视频邀请" disabled="disabled" >
+
+        &nbsp;&nbsp;&nbsp;&nbsp;
+        <label for="videoListSelect"></label>
+         <select id="videoListSelect">
+             <option value="">请选择视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/bank.mp4">客服实例视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/conference.mp4">多方会议视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/15-seconds.mp4">15-seconds-demo</option>
+
+         </select>
+        <input type="button" id="sendVideoFileBtn" title="推送视频给对方,以便结束当前通话。"
+               onclick="_phoneBar.sendVideoFile($('#videoListSelect').val());"   value="推送视频" disabled="disabled" >
+
+    </div>
+</div>
+</td>
+</tr>
+
+    <tr id="conference_area" style="display: none">
+
+        <td colspan="2" style="padding-left: 130px; padding-top: 30px;">
+            <div>
+                <div>
+                    <div id="conference_start" style="display: block">
+                        <!-- 会议布局: &nbsp; -->
+                        <select id="conf_layout" name="conf_layout" style="display: none">
+                            <option value="2x2">2x2</option>
+                            <option value="3x3">3x3</option>
+                            <option value="1up_top_left+3">一主三从</option>
+                        </select>
+                        &nbsp;
+                        <!-- 画布尺寸: -->
+                        <select id="conf_template" name="conf_template" style="display: none">
+                            <option value="480p" selected="selected">480x640</option>
+                            <option value="720p">720x1080</option>
+                            <option value="default">default</option>
+                        </select>
+                        &nbsp;
+                         会议类型:
+                        <select id="conf_call_type2" name="conf_call_type2"  >
+                           <!-- <option value="video">视频</option> -->
+                            <option value="audio">音频</option>
+                        </select>
+                        <input type="hidden" value="audio" id="conf_call_type" name="conf_call_type" />
+                        &nbsp;
+                        <input type="button" name="startConference" id="startConference"
+                               onclick="_phoneBar.conferenceStartBtnUI('')"
+                               style="width: 70px;" value="启动会议">
+                        &nbsp;
+                        <input type="button" name="endConference" id="endConference"
+                               onclick="_phoneBar.conferenceEnd()"
+                               disabled="disabled"
+                               style="width: 70px;" value="结束会议">
+                    </div>
+
+                    <div style="width: 100%;"> &nbsp; </div>
+
+                    <div id="conference_member_list" style="display: none">
+                        <ul>
+                            <li id="conference_header">
+                                <span class="conf_name"> <input id="member_name" name="member_name" placeholder="姓名" style="width: 60px;" /> </span> &nbsp;
+                                <span class="conf_phone"> <input id="member_phone" name="member_phone" placeholder="手机号" style="width: 110px;" /> </span> &nbsp;
+                                <span class="conf_call_type">
+                                <select id="member_call_type" name="member_call_type" style="display: none">
+                                   <option value="video">视频</option>
+                                   <option value="audio" selected>音频</option>
+                               </select>
+                            </span>
+                                <span class="conf_video_level" style="display: none">
+                                <select id="member_video_level" name="member_video_level">
+                               </select>
+                            </span>
+
+                                <span class="conf_name">
+                               <input type="button" name="addConfMember" id="addConfMember"
+                                      onclick="_phoneBar.conferenceAddMemberBtnUI(0)"
+                                      style="width: 70px;" value="加入会议">
+                           </span>
+                            </li>
+
+                            <!-- 会议成员展示模版html  -->
+                            <li id="conf_member_template"  style="display: none;">
+                                <span class="conf_name">{member_name}</span>
+                                <span class="conf_phone">{member_phone}</span>
+                                <span class="conf_mute"><a href="javascript:void(0)" onclick="_phoneBar.conferenceMuteMember('{member_phone}')"><img alt="禁言该成员。" src="images/mute.jpg" width="15" height="17" /> </a> </span>
+                                <span class="conf_vmute" style="display: none"><a href="javascript:void(0)" onclick="_phoneBar.conferenceVMuteMember('{member_phone}')"><img alt="关闭该成员的视频。" src="images/video.jpg" /> </a></span>
+                                <span class="conf_remove"><a href="javascript:void(0)" onclick="_phoneBar.conferenceRemoveMembers('{member_phone}')" title="踢除会议成员。">移除</a></span>
+                                <span class="conf_re_invite"><a href="javascript:void(0)" onclick="_phoneBar.conferenceAddMemberBtnUI(1, '{member_phone}', '{member_name}')" title="重新呼叫。">重呼</a></span>
+                                <span class="conf_status">{member_status}</span>
+                            </li>
+
+
+                            <li></li>
+                        </ul>
+                    </div>
+
+
+                </div>
+            </div>
+        </td>
+
+    </tr>
+
+<tr id="transfer_area" width="100%" style="display: none">
+
+        <td colspan="2" width="100%" style="padding-left: 140px; padding-top: 30px;">
+            <table width="100%">
+                 <tr>
+                       <td width="90">业务组   </td>
+                       <td width="90">坐席成员</td>
+                       <td>&nbsp; </td>
+                 </tr>
+                <tr>
+                    <td>
+                        <select size="10" id="transfer_to_groupIds" name="transfer_to_groupIds">
+                            <option value="">请选择</option>
+                        </select>
+                    </td>
+
+                    <td>
+                        <select size="10" id="transfer_to_member" name="transfer_to_member">
+                            <option value="">请选择</option>
+                        </select>
+                    </td>
+                    <td valign="middle">
+
+
+                        &nbsp;&nbsp; <input type="text" name="externalPhoneNumber" id="externalPhoneNumber" placeholder="电话号码"
+                               title="可以把当前通话转接到外线号码上。 如果该文本框留空,则忽略处理。"
+                               class="tel_txt" />
+                        <br /> <br />
+
+                        &nbsp;&nbsp; <input type="button" name="doTransferBtn" id="doTransferBtn"
+                               onclick="_phoneBar.transferBtnClickUI()"
+                               style="width: 70px;" value="转接电话" title="把当前电话转接给他/她处理。" /> &nbsp;
+
+                        &nbsp;&nbsp; <input type="button" name="stopCallWait" id="stopCallWait"
+                               onclick="_phoneBar.stopCallWaitBtnClickUI()"
+                               style="width: 70px;" value="接回客户" title="在咨询失败的情况下使用该按钮,接回处于等待中的电话。" /> &nbsp;
+
+                        &nbsp;&nbsp; <input type="button" name="transferCallWait" id="transferCallWait"
+                               onclick="_phoneBar.transferCallWaitBtnClickUI()"
+                               style="width: 70px;" value="转接客户" title="在咨询成功的情况下使用该按钮,把电话转接给专家坐席。" /> &nbsp;
+
+                       <input type="button" name="doConsultationBtn" id="doConsultationBtn"
+                               onclick="_phoneBar.consultationBtnClickUI()"
+                               style="width: 70px;" value="拨号咨询" title="" />
+
+                    </td>
+                </tr>
+            </table>
+        </td>
+
+</tr>
+
+</table>
+</form>
+
+<div id="chat-container">
+    <div id="chat-messages" class="message-container"></div>
+</div>
+
+<script>
+    // 以下是通话转接操作界面的功能
+    const transferToGroupId = document.getElementById("transfer_to_groupIds");
+    const transferToMember = document.getElementById("transfer_to_member");
+
+    // 填充 transfer_to_groupId 数据
+    function populateGroupIdOptions() {
+        transferToGroupId.length = 0; //清除所有选项
+        let groups = _phoneBar.callConfig.groups;
+        groups.forEach(group => {
+            const option = document.createElement("option");
+            option.value = group.groupId;
+            option.textContent = group.bizGroupName;
+            transferToGroupId.appendChild(option);
+        });
+        if(transferToGroupId.selectedIndex == -1){
+            transferToGroupId.selectedIndex = 0;
+        }
+    };
+
+    // 根据选中的 groupId 填充 transfer_to_member 数据
+    function populateMemberIdOptions(members, selectedGroupId) {
+        if (!Array.isArray(members)) {
+            console.error("populateMemberOptions: members is not a Array.", members);
+            return;
+        }
+        transferToMember.innerHTML = '<option value="">请选择</option>';
+        members
+            .filter(member => member.groupId === selectedGroupId)
+       .forEach(member => {
+            const option = document.createElement("option");
+        const statusMap = { 1 : "刚签入", 2: "空闲", 3: "忙碌", 4: "通话中", 5: "事后处理" };
+        option.value = member.opnum;
+        option.textContent = `${member.opnum}(${statusMap[member.agentStatus] || ""})`;
+        transferToMember.appendChild(option);
+      });
+    };
+
+    function refreshMemberIdList() {
+        const selectedGroupId = transferToGroupId.value;
+        if(selectedGroupId != "") {
+            let origValue = transferToMember.value;
+            populateMemberIdOptions(_phoneBar.callConfig.agentList, selectedGroupId);
+            //判断原始选择项是否还存在,存在则重新赋值;
+            let hasValue = transferToMember.querySelector(`option[value="${origValue}"]`) !== null;
+            if(hasValue) {
+                transferToMember.value = origValue;
+            }
+        }
+    }
+
+    /* asr实时对话文本框的功能 */
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_started, function (msg) {
+        $(chatMessages).html("");
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_result_generate, function (msg) {
+        handleAsrMessage(msg);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_end_customer, function (msg) {
+        handleAsrMessage(msg);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_end_agent, function (msg) {
+        handleAsrMessage(msg);
+    });
+    const chatMessages = document.getElementById('chat-messages');
+    $("#chat-container").hide();
+    function handleAsrMessage(data) {
+        $("#chat-container").show();
+        const { status, object } = data;
+        if (status === 619 && object) {
+            const { role, text, vadType } = object;
+            if(vadType == 1) {
+                addMessageToChat(role, text);
+            }
+        } else if (status === 620 || status === 621) {
+            addSystemMessage("对话已结束。");
+        }
+    }
+    function addMessageToChat(role, text) {
+        const messageDiv = document.createElement('div');
+        messageDiv.className = 'message ' + (role === 1 ? 'customer' : 'agent');
+
+        // 添加角色名称
+        const roleHeader = document.createElement('div');
+        roleHeader.className = 'message-header';
+        roleHeader.textContent = role === 1 ? '客户' : '我';
+
+        const messageContent = document.createElement('div');
+        messageContent.textContent = text;
+
+        // messageDiv.appendChild(roleHeader);
+        messageDiv.appendChild(messageContent);
+        chatMessages.appendChild(messageDiv);
+        scrollToBottom();
+    }
+    function addSystemMessage(text) {
+        const systemMessage = document.createElement('div');
+        systemMessage.className = 'system-message';
+        systemMessage.textContent = text;
+        chatMessages.appendChild(systemMessage);
+        scrollToBottom();
+    }
+    function scrollToBottom() {
+        chatMessages.scrollTop = chatMessages.scrollHeight;
+    }
+
+</script>
+
+</body>
+</html>

+ 561 - 0
docs/phone-drop.txt

@@ -0,0 +1,561 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>呼叫中心html客户端工具条</title>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <link rel="stylesheet" href="page.css" />
+    <script type="text/javascript" src="jquery-1.11.0.js"></script>
+    <script type="text/javascript" src="ccPhoneBarSocket.js"></script>
+	  
+    <script type="text/javascript">
+        var _phoneBar = new ccPhoneBarSocket();
+        var scriptServer = "127.0.0.1";
+        var  extnum = '1001'; //分机号
+        var opnum = '1001'; //工号
+        var skillLevel = 9; //技能等级
+        var groupId = 1; // 业务组id
+        if(window.location.href.toString().indexOf("?") != -1){
+            console.log( ccPhoneBarSocket.utils );
+            extnum = ccPhoneBarSocket.utils.getQueryParam("extNum");
+            opnum = ccPhoneBarSocket.utils.getQueryParam("opNum");
+            groupId = ccPhoneBarSocket.utils.getQueryParam("groupId");
+            console.log("extNum=", extnum, "opNum=", opnum);
+        }
+
+        function resetExtNumAndOpNum(ext, op, groupId) {
+            window.location.href = "?extNum=" + ext + "&opNum=" + op + "&groupId=" + groupId;
+        };
+
+		(function loadLoginToken(){
+
+            // 目前已经把 projectId 和 groupId合并为同一个参数;
+            var getTokenUrl = "http://"+ scriptServer +":8880/call-center/create-token";
+            var destUrl = getTokenUrl + "?extnum=" + extnum + "&opnum=" + opnum
+            + "&groupId=" + groupId +"&skillLevel=" + skillLevel
+            ;
+
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = destUrl;
+            document.getElementsByTagName('head')[0].appendChild(script);
+		})();
+
+        (function loadExtPassword(){
+            var  extPassword = '1234567';
+            var url = "http://"+ scriptServer +":8880/call-center/create-ext-password?pass=" + extPassword;
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = url;
+            document.getElementsByTagName('head')[0].appendChild(script);
+        })();
+
+        (function loadGatewayList(){
+            var url = "http://"+ scriptServer +":8880/call-center/create-gateway-list" ;
+            var script = document.createElement("script");
+            script.type = "text/javascript";
+            script.src = url;
+            document.getElementsByTagName('head')[0].appendChild(script);
+        })();
+
+        // 将视频级别填充到下拉列表中的函数
+        function populateVideoLevelDropdown(objId) {
+            let select = document.getElementById(objId);
+            if(select == null) return;
+            // 遍历视频级别数据
+            for (let key in ccPhoneBarSocket.videoLevels) {
+                if (ccPhoneBarSocket.videoLevels.hasOwnProperty(key)) {
+                    let level = ccPhoneBarSocket.videoLevels[key];
+                    let option = document.createElement('option');
+                    option.value = level.levelId; // 设置值为 levelId
+                    option.text = level.description; // 显示文本
+                    select.appendChild(option);
+                }
+            }
+            select.value = ccPhoneBarSocket.videoLevels.HD.levelId ;
+        }
+
+	</script>
+
+<script> 
+var _callConfig = null;
+
+window.onload = function(){
+
+    // 调用函数填充视频清晰度的下拉列表
+    populateVideoLevelDropdown('videoLevelSelect');
+    populateVideoLevelDropdown('member_video_level');
+
+	//工具条对象断开事件
+	// _phoneBar.on(ccPhoneBarSocket.eventList.ws_disconnected, function(msg){
+	// 	console.log(msg);
+	// });
+    //
+    // //工具条对象连接成功
+    // _phoneBar.on(ccPhoneBarSocket.eventList.ws_connected, function(msg){
+    //     console.log(msg);
+    // });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_ringing, function(msg){
+	// 	console.log(msg.content, "被叫振铃事件");
+	// });
+	// _phoneBar.on(ccPhoneBarSocket.eventList.caller_answered, function(msg){
+	// 	console.log(msg, "主叫接通" );
+	// });
+    // _phoneBar.on(ccPhoneBarSocket.eventList.caller_hangup, function(msg){
+    //     console.log(msg, "主叫挂断");
+    // });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_answered, function(msg){
+	// 	console.log(msg, "被叫接通");
+	// });
+	// _phoneBar.on(ccPhoneBarSocket.eventList.callee_hangup, function(msg){
+	// 	console.log(msg, "被叫挂断");
+	// });
+    //
+	// _phoneBar.on(ccPhoneBarSocket.eventList.status_changed, function(msg){
+	// 	console.log("座席状态改变: " ,msg);
+	// });
+    //
+    // // 一次外呼结束;
+    // _phoneBar.on(ccPhoneBarSocket.eventList.outbound_finished, function(msg){
+    // 	console.log('一次外呼结束', msg);
+    // });
+
+    // websocket通信对象断开事件;
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.ws_disconnected.code, function(msg){
+        console.log(msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.ws_disconnected.code);
+        $("#transfer_area").hide();
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.OUTBOUND_START, function (msg) {
+        console.log('outbound_start',msg);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.request_args_error.code, function(msg){
+        console.log(msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.request_args_error.code);
+    });
+
+    //用户已在其他设备登录
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.code, function(msg){
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.code);
+        alert(ccPhoneBarSocket.eventListWithTextInfo.user_login_on_other_device.msg);
+    });
+
+    //websocket连接成功
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.ws_connected.code, function(msg){
+        console.log(msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.ws_connected.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_ringing.code, function(msg){
+        console.log(msg.content, "被叫振铃事件");
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_ringing.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.caller_answered.code, function(msg){
+        console.log(msg, "主叫接通" );
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.caller_answered.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.caller_hangup.code, function(msg){
+        console.log(msg, "主叫挂断");
+        $("#reInviteVideoBtn").attr("disabled","disabled");
+        $("#sendVideoFileBtn").attr("disabled","disabled");
+        $("#transfer_area").hide();
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.caller_hangup.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_answered.code, function(msg){
+        console.log(msg, "被叫接通");
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_answered.code);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.callee_hangup.code, function(msg){
+        console.log(msg, "被叫挂断");
+        $("#transfer_area").hide();
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.callee_hangup.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventListWithTextInfo.status_changed.code, function(msg){
+        console.log("座席状态改变: " ,msg);
+        _phoneBar.updatePhoneBar(msg, ccPhoneBarSocket.eventListWithTextInfo.status_changed.code);
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.on_audio_call_connected, function(msg){
+        console.log("音频通话已建立: " ,msg);
+        $("#reInviteVideoBtn").removeAttr("disabled");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.customer_channel_hold, function (msg) {
+        console.log("客户通话已保持: " ,msg);
+        $("#callStatus").text("通话已保持");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.customer_channel_unhold, function (msg) {
+        console.log("客户通话已接回." ,msg);
+        $("#callStatus").text("客户通话已接回");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.on_video_call_connected, function(msg){
+        console.log("视频通话已建立: " ,msg);
+        $("#sendVideoFileBtn").removeAttr("disabled");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.inner_consultation_start, function(msg){
+        $("#callStatus").text("咨询开始.");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.inner_consultation_stop, function(msg){
+        $("#callStatus").text("咨询结束.");
+    });
+
+    _phoneBar.on(ccPhoneBarSocket.eventList.transfer_call_success, function(msg){
+        $("#callStatus").text("电话转接成功.");
+    });
+
+    // 订阅的坐席状态列表发生改变
+    _phoneBar.on(ccPhoneBarSocket.eventList.agent_status_data_changed, function (msg) {
+        console.log("agent_status_data_changed.");
+        // 当 transfer_to_groupId 值改变时更新 transfer_to_member
+        $(transferToGroupId).off("change");
+        $(transferToGroupId).on("change", function () {
+            refreshMemberIdList();
+        });
+        refreshMemberIdList();
+    });
+
+    // 电话工具条参数配置;
+	_callConfig = {
+        'useDefaultUi' : true,
+        // loginToken 信息是加密的字符串, 包含以下字段信息:extnum[分机号]、opnum[工号]、groupId[业务组]、skillLevel[技能等级]
+        'loginToken': '',
+
+         // 电话工具条服务器端的地址; 端口默认是1081
+        'ipccServer': scriptServer + ':1081',
+
+        // 网关列表, 默认需要加密后在在通过客户端向呼叫系统传递;
+        // 注意在注册模式下,网关参数更改之后,必须重启语音服务 [docker restart freeswitch] 方可生效,不支持热更新;
+        // 支持多个网关同时使用,按照优先级依次使用, 支持网关负载容错溢出 [第一条网关外呼出错后,自动使用第二条网关重试,直至外呼不出错] ;
+        'gatewayList': [
+            {
+                uuid: '01',   // 网关唯一编号;
+                updateTime: 1992622572983,  //网关信息更新时间, 每次修改必须更新这个字段,否则配置无法生效;
+                gatewayAddr: '192.168.67.219',  // 网关地址或名称,  如果是注册模式: 网关地址参数则填写为网关名称;
+                callerNumber: '007',                 //主叫号码
+                calleePrefix: '',                    // 被叫前缀
+                priority: 2,                         //优先级,数字越小,越优先被使用;
+                concurrency: 20,                    // 网关并发数,同时支持呼叫数;
+                register: false,                     // 是否为注册模式
+                audioCodec: 'g711'                   // 网关通信语音编码;
+            }  
+        ],
+
+        // 网关列表信息是否为加密模式;
+        'gatewayEncrypted': false
+    };
+
+    // 使用工具条之前需要先初始化 _callConfig 参数, 填充各个字段的值: 合计7个字段,必须填写正确 ;
+	//********************************************************************************************
+    // 以下代码设置加密的参数: loginToken、extPassword、gatewayList;   在本页面的demo演示中需要调用服务器端接口获取密文字符串;
+
+	console.log('loginToken = ',loginToken);
+	if(typeof(loginToken) != "undefined") {
+        _callConfig["loginToken"] = loginToken;
+    } else{
+	    alert("电话工具条:无法获取 loginToken!");
+	    return;
+    }
+
+    console.log('_phoneEncryptPassword = ',_phoneEncryptPassword);
+    if(typeof(_phoneEncryptPassword) != "undefined") {
+        _callConfig["extPassword"] = _phoneEncryptPassword;
+    } else{
+        alert("电话工具条:无法获取 _phoneEncryptPassword!");
+        return;
+    }
+
+    console.log('_configGatewayList = ', _configGatewayList);
+    if(typeof(_configGatewayList) != "undefined" && _callConfig["gatewayEncrypted"]) {
+        //_callConfig["gatewayList"] = _configGatewayList;
+    } else{
+       //  alert("电话工具条:无法获取 _configGatewayList!");
+    }
+
+    _phoneBar.initConfig(_callConfig);
+};
+
+function onConferenceEnd() {
+    document.getElementById("endConference").setAttribute("disabled", "true");
+    document.getElementById("startConference").removeAttribute("disabled");
+    document.getElementById("conference_member_list").style.display = "none";
+    // 启用外呼按钮
+    $("#callBtn").addClass('on');
+    // 启用置闲按钮
+    $("#setFree").addClass('on');
+    // 启用签出按钮
+    $("#onLineBtn").addClass('on');
+    //移除所有的参会成员
+    $(".conf_member_item_row").remove();
+
+    $("#callStatus").text("会议结束.");
+}
+
+function onConferenceStart() {
+    document.getElementById("endConference").removeAttribute("disabled");
+    document.getElementById("conference_member_list").style.display = "block";
+    $("#callStatus").text("视频会议进行中...");
+}
+
+/**
+ *  成功把电话转接到多人视频会议
+ */
+function onTransferToConferenceSuccess(msg) {
+    $("#callStatus").text("成转接到视频会议");
+    $("#setFree").removeClass("on");
+    $("#setBusy").removeClass("on");
+    $("#callBtn").removeClass("on");
+
+    //界面显示成功转接到视频会议电话
+    var phone = msg.object.phone;
+    var name = msg.object.name;
+    _phoneBar.conferenceAddMemberFromExistCall(name, phone);
+}
+
+
+</script>
+
+</head>
+<body>
+
+<form>
+
+<table width="1224">
+<tr>
+  <td width="100%" colspan="2" height="55">
+   <img src="images/logo.jpg" width="200" height="50" /> 
+  </td>
+</tr>
+<tr>
+<td width="70%">
+<div>
+    <div class="head_dial" style="padding-left: 10px; ">
+	     
+        <dl class="dial">
+            <dt>
+                <label for="ccphoneNumber"></label><input type="text" name="ccphoneNumber" id="ccphoneNumber" placeholder="请输入电话号码" class="tel_txt" />
+            </dt>
+            <dd>
+                <ul><li id="callStatus" title="" class="status4">没有连接</li></ul>
+                <span id="showCallLen" style="display:none"><b>00:00</b></span>
+            </dd>
+        </dl>
+
+        <ul class="dial_btn">
+            <li><a href="#" id="setFree" class="xz_btn off"></a><span>置闲</span></li>
+            <li><a href="#" id="setBusy" class="sm_btn off"></a><span>忙碌</span></li>
+            <li><a href="#" id="callBtn" class="wh_btn"></a><span>外呼</span></li>
+            <li id="holdBtnLi"><a href="#" id="holdBtn" class="bc_btn off"></a><span>保 持</span></li>
+            <li id="unHoldBtnLi"><a href="#" id="unHoldBtn" class="bc2_btn off"></a><span>取消保持</span></li>
+            <li><a href="#" id="transferBtn" class="zjie_btn"></a><span>转接</span></li>
+            <li><a href="#" id="consultationBtn" class="zixun_btn"></a><span>咨询</span></li>
+            <li><a href="#" id="conferenceBtn" class="hy_btn"></a><span>会议</span></li>
+            <li><a href="#" id="hangUpBtn" class="gj_btn"></a><span>挂机</span></li>
+            <li><a href="#" id="resetStatus" class="qz_btn"></a><span>强置</span></li>
+            <li><a href="#" id="onLineBtn" class="sx_btn on"></a><span>签入</span></li>
+        </ul>
+    </div>
+</div>
+</td>
+<td width="30%">
+<div>
+    <div  style="padding-left: 10px; ">
+        &nbsp; &nbsp;  外呼设置:
+        <label for="videoCallBtn"> <input type="radio" value="video" name="callType" id="videoCallBtn" />视频外呼</label>  &nbsp;&nbsp;
+        <label for="audioCallBtn"> <input type="radio" value="audio"  name="callType" checked="checked" id="audioCallBtn"  />语音外呼</label>  <br />
+
+        &nbsp; &nbsp; 视频清晰度:
+        <label for="videoLevelSelect"></label><select id="videoLevelSelect">
+        </select>
+        <input type="button" id="reInviteVideoBtn" title="发送视频邀请,可把音频通话转换为视频通话。"
+         onclick="_phoneBar.reInviteVideoCall();"       value="视频邀请" disabled="disabled" >
+
+        &nbsp;&nbsp;&nbsp;&nbsp;
+        <label for="videoListSelect"></label>
+         <select id="videoListSelect">
+             <option value="">请选择视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/bank.mp4">客服实例视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/conference.mp4">多方会议视频</option>
+             <option value="/usr/local/freeswitchvideo/share/freeswitch/sounds/15-seconds.mp4">15-seconds-demo</option>
+
+         </select>
+        <input type="button" id="sendVideoFileBtn" title="推送视频给对方,以便结束当前通话。"
+               onclick="_phoneBar.sendVideoFile($('#videoListSelect').val());"   value="推送视频" disabled="disabled" >
+
+    </div>
+</div>
+</td>
+</tr>
+
+<tr id="transfer_area" width="100%" style="display: none">
+
+        <td colspan="2" width="100%" style="padding-left: 140px; padding-top: 30px;">
+            <table width="100%">
+                 <tr>
+                       <td width="90">业务组   </td>
+                       <td width="90">坐席成员</td>
+                       <td>&nbsp; </td>
+                 </tr>
+                <tr>
+                    <td>
+                        <select size="10" id="transfer_to_groupIds" name="transfer_to_groupIds">
+                            <option value="">请选择</option>
+                        </select>
+                    </td>
+
+                    <td>
+                        <select size="10" id="transfer_to_member" name="transfer_to_member">
+                            <option value="">请选择</option>
+                        </select>
+                    </td>
+                    <td valign="middle">
+                        <input type="button" name="doTransferBtn" id="doTransferBtn"
+                               onclick="_phoneBar.transferBtnClickUI()"
+                               style="width: 70px;" value="转接电话" title="把当前电话转接给他/她处理。" /> &nbsp;
+
+                        <input type="button" name="stopCallWait" id="stopCallWait"
+                               onclick="_phoneBar.stopCallWaitBtnClickUI()"
+                               style="width: 70px;" value="接回客户" title="在咨询失败的情况下使用该按钮,接回处于等待中的电话。" /> &nbsp;
+
+                        <input type="button" name="transferCallWait" id="transferCallWait"
+                               onclick="_phoneBar.transferCallWaitBtnClickUI()"
+                               style="width: 70px;" value="转接客户" title="在咨询成功的情况下使用该按钮,把电话转接给专家坐席。" /> &nbsp;
+
+                        <input type="button" name="doConsultationBtn" id="doConsultationBtn"
+                               onclick="_phoneBar.consultationBtnClickUI()"
+                               style="width: 70px;" value="拨号咨询" title="" />
+
+                    </td>
+                </tr>
+            </table>
+        </td>
+
+</tr>
+
+</table>
+</form>
+
+<div id="chat-container">
+    <div id="chat-messages" class="message-container"></div>
+</div>
+
+<script>
+    // 以下是通话转接操作界面的功能
+    const transferToGroupId = document.getElementById("transfer_to_groupIds");
+    const transferToMember = document.getElementById("transfer_to_member");
+
+    // 填充 transfer_to_groupId 数据
+    function populateGroupIdOptions() {
+        transferToGroupId.length = 0; //清除所有选项
+        let groups = _phoneBar.callConfig.groups;
+        groups.forEach(group => {
+            const option = document.createElement("option");
+            option.value = group.groupId;
+            option.textContent = group.bizGroupName;
+            transferToGroupId.appendChild(option);
+        });
+        if(transferToGroupId.selectedIndex == -1){
+            transferToGroupId.selectedIndex = 0;
+        }
+    };
+
+    // 根据选中的 groupId 填充 transfer_to_member 数据
+    function populateMemberIdOptions(members, selectedGroupId) {
+        if (!Array.isArray(members)) {
+            console.error("populateMemberOptions: members is not a Array.", members);
+            return;
+        }
+        transferToMember.innerHTML = '<option value="">请选择</option>';
+        members
+            .filter(member => member.groupId === selectedGroupId)
+       .forEach(member => {
+            const option = document.createElement("option");
+        const statusMap = { 1 : "刚签入", 2: "空闲", 3: "忙碌", 4: "通话中", 5: "事后处理" };
+        option.value = member.opnum;
+        option.textContent = `${member.opnum}(${statusMap[member.agentStatus] || ""})`;
+        transferToMember.appendChild(option);
+      });
+    };
+
+    function refreshMemberIdList() {
+        const selectedGroupId = transferToGroupId.value;
+        if(selectedGroupId != "") {
+            let origValue = transferToMember.value;
+            populateMemberIdOptions(_phoneBar.callConfig.agentList, selectedGroupId);
+            //判断原始选择项是否还存在,存在则重新赋值;
+            let hasValue = transferToMember.querySelector(`option[value="${origValue}"]`) !== null;
+            if(hasValue) {
+                transferToMember.value = origValue;
+            }
+        }
+    }
+
+    /* asr实时对话文本框的功能 */
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_started, function (msg) {
+        $(chatMessages).html("");
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_result_generate, function (msg) {
+        handleAsrMessage(msg);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_end_customer, function (msg) {
+        handleAsrMessage(msg);
+    });
+    _phoneBar.on(ccPhoneBarSocket.eventList.asr_process_end_agent, function (msg) {
+        handleAsrMessage(msg);
+    });
+    const chatMessages = document.getElementById('chat-messages');
+    $("#chat-container").hide();
+    function handleAsrMessage(data) {
+        $("#chat-container").show();
+        const { status, object } = data;
+        if (status === 619 && object) {
+            const { role, text, vadType } = object;
+            if(vadType == 1) {
+                addMessageToChat(role, text);
+            }
+        } else if (status === 620 || status === 621) {
+            addSystemMessage("对话已结束。");
+        }
+    }
+    function addMessageToChat(role, text) {
+        const messageDiv = document.createElement('div');
+        messageDiv.className = 'message ' + (role === 1 ? 'customer' : 'agent');
+
+        // 添加角色名称
+        const roleHeader = document.createElement('div');
+        roleHeader.className = 'message-header';
+        roleHeader.textContent = role === 1 ? '客户' : '我';
+
+        const messageContent = document.createElement('div');
+        messageContent.textContent = text;
+
+        // messageDiv.appendChild(roleHeader);
+        messageDiv.appendChild(messageContent);
+        chatMessages.appendChild(messageDiv);
+        scrollToBottom();
+    }
+    function addSystemMessage(text) {
+        const systemMessage = document.createElement('div');
+        systemMessage.className = 'system-message';
+        systemMessage.textContent = text;
+        chatMessages.appendChild(systemMessage);
+        scrollToBottom();
+    }
+    function scrollToBottom() {
+        chatMessages.scrollTop = chatMessages.scrollHeight;
+    }
+
+</script>
+
+</body>
+</html>

BIN
docs/shot/doubao_vcl.png


BIN
docs/shot/ivr.png


BIN
docs/shot/webgui-1.png


BIN
docs/shot/webgui-2.png


BIN
docs/shot/webgui-3.png


+ 13 - 0
docs/zh-cn/Debian12-install-docker.md

@@ -0,0 +1,13 @@
+## Debian12安装Docker
+
+如果您已经安装了Docker,本步骤可以跳过。
+
+```bash
+apt-get update
+apt-get -y install apt-transport-https software-properties-common ca-certificates curl gnupg lsb-release
+curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | apt-key add -
+add-apt-repository "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/debian $(lsb_release -cs) stable"
+apt-get update
+apt-get -y install docker-ce 
+systemctl enable docker
+```

+ 53 - 0
docs/zh-cn/Debian12-install-mysql8.md

@@ -0,0 +1,53 @@
+## Debian12安装mysql8
+
+如果您已经安装了 `mysql8` ,本步骤可以跳过。本文档提供了通过 `docker` 安装 `mysql8`
+
+```bash
+mkdir /home/mysql/
+mkdir /home/mysql/data/ /home/mysql/logs/  
+cd /home/mysql/
+
+# 设置docker仓库源
+echo  "{  \
+     \"registry-mirrors\": [\"https://docker.registry.cyou\",  \
+    \"https://docker-cf.registry.cyou\",  \
+    \"https://dockercf.jsdelivr.fyi\",  \
+    \"https://docker.jsdelivr.fyi\",  \
+    \"https://dockertest.jsdelivr.fyi\",  \
+    \"https://mirror.aliyuncs.com\",  \
+    \"https://dockerproxy.com\",  \
+    \"https://mirror.baidubce.com\",  \
+    \"https://docker.m.daocloud.io\",  \
+    \"https://docker.nju.edu.cn\",  \
+    \"https://docker.mirrors.sjtug.sjtu.edu.cn\",  \
+    \"https://docker.mirrors.ustc.edu.cn\",  \
+    \"https://mirror.iscas.ac.cn\",  \
+    \"https://docker.rainbond.cc\"]  \
+    }"   >  /etc/docker/daemon.json 
+
+systemctl daemon-reload && systemctl restart docker
+docker pull mysql:8.0.29
+	
+# 上传my.cnf配置文件
+# 把 freeswitch-modules-libs 项目的 sql/mysql8/my.cnf 上传到 `/home/mysql/` 目录下。
+
+chmod 644 my.cnf 
+
+docker run --network=host --name mysql -v $PWD/data:/var/lib/mysql -v $PWD/my.cnf:/etc/my.cnf -e MYSQL_ROOT_PASSWORD=easycallcenter365 -d mysql:8.0.29
+
+#进入容器
+docker exec -it mysql /bin/bash
+
+# 进入mysql控制台
+mysql -uroot -peasycallcenter365 -h 127.0.0.1
+
+use mysql; update user set host='%' where user='root';  # 如遇错误不用管它
+flush privileges;
+GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'WITH GRANT OPTION;
+flush privileges;
+
+ALTER USER 'root'@'%' IDENTIFIED BY 'easycallcenter365' PASSWORD EXPIRE NEVER;
+ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'easycallcenter365';
+FLUSH PRIVILEGES;
+
+```

+ 250 - 0
docs/zh-cn/FreeSWITCH-install-docs.md

@@ -0,0 +1,250 @@
+# Compile and Install FreeSWITCH
+
+## 从源代码编译安装FreeSWITCH
+
+```bash
+cd /home/
+wget https://files.freeswitch.org/releases/freeswitch/freeswitch-1.10.11.-release.zip 
+apt-get install zip unzip  git
+unzip -d /home/freeswitch  freeswitch-1.10.11.-release.zip
+```
+
+如果在linux使用wget下载失败,请在windows下使用下载工具下载,然后上传到linux的 /home 目录下。
+
+## download FreeSWITCH mods for easyCallcenter365
+
+```bash
+mkdir /home/easyCallcenter365
+cd /home/easyCallcenter365
+git clone https://gitee.com/easycallcenter365/freeswitch-modules-libs.git
+```
+
+## 合并代码
+
+```bash
+cp -r /home/easyCallcenter365/freeswitch-modules-libs/src/*  /home/freeswitch/freeswitch-1.10.11.-release/src/
+cp -r /home/easyCallcenter365/freeswitch-modules-libs/libs/*  /home/freeswitch/freeswitch-1.10.11.-release/libs/
+```    
+ 
+    	
+## 设置Token
+
+The compilation requires downloading resource files from FreeSWITCH's official site with authentication. You may request your own Token or use mine. Run this command:
+
+```bash
+TOKEN=pat_shvgo5fY5igbiQdaJ3VUHppC
+```
+
+## 安装基本工具
+
+```bash
+apt-get update && apt-get install -yq gnupg2 wget lsb-release
+```
+	
+## 设置FreeSWITCH的apt源
+
+```bash
+wget --http-user=signalwire --http-password=$TOKEN -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
+echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" > /etc/apt/auth.conf
+chmod 600 /etc/apt/auth.conf
+echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ `lsb_release -sc` main" > /etc/apt/sources.list.d/freeswitch.list
+echo "deb-src [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ `lsb_release -sc` main" >> /etc/apt/sources.list.d/freeswitch.list
+apt-get update
+apt-get build-dep freeswitch
+```
+		
+## 编译FreeSWITCH
+
+开启 mariadb 和 curl 模块,进入源码目录,
+```bash
+cd  /home/freeswitch/freeswitch-1.10.11.-release
+```
+ 编辑模块配置文件, vim modules.conf
+```txt
+databases/mod_mariadb 
+applications/mod_curl
+```
+去掉#号即可。
+
+```bash
+./rebootstrap.sh -j
+./configure  --prefix=/usr/local/freeswitchvideo  
+make -j 3
+make install
+```
+
+## 编译libhv
+
+```bash
+cd  /home/freeswitch/freeswitch-1.10.11.-release/libs/libhv/
+apt-get -y install cmake
+mkdir build && cd build
+cmake .. -DWITH_OPENSSL=ON
+cmake --build .
+cp lib/libhv.so  /usr/local/freeswitchvideo/lib/
+```
+
+## 编译 rapidJson
+
+```bash
+cd /home/freeswitch/
+git clone https://github.com/Tencent/rapidjson.git
+cd rapidjson/
+mkdir build && cd build
+cmake ..
+make install
+```
+
+## 编译 mod_funasr 和 mod_aliyun_tts
+
+在本项目中,mod_funasr 用作语音识别功能, 通过它来集成 FunASR。 mod_aliyun_tts 用作语音合成, 通过它来集成阿里云tts.
+
+```bash
+# 编译 mod_funasr
+cd /home/freeswitch/freeswitch-1.10.11.-release/src/mod/asr_tts/mod_funasr
+g++  -shared -o mod_funasr.so -fPIC -g -O -ggdb -std=c++11 -Wall   mod_funasr.cpp  -I../../../../libs/libhv/build/include/hv/   -I../../../../libs/libteletone/src/   -I../../../../src/include/   -lpthread  -L/usr/local/freeswitchvideo/lib/  -lhv -lfreeswitch
+cp mod_funasr.so  /usr/local/freeswitchvideo/lib/freeswitch/mod/
+
+# 编译 mod_aliyun_tts
+cd ../mod_aliyun_tts/
+g++ -shared -o mod_aliyun_tts.so -fPIC -g -O -ggdb -std=c++11 -Wall   -I../../../../libs/libhv/include/hv/   -I../../../../libs/cpputils/ -I../../../../src/include/   -I../../../../libs/libteletone/src/   mod_aliyun_tts.cpp    -L/usr/local/freeswitchvideo/lib/  -lfreeswitch   -L/usr/local/lib/  -lhv    -lcurl  -lpthread   -lssl -lcrypto
+cp mod_aliyun_tts.so /usr/local/freeswitchvideo/lib/freeswitch/mod/
+```
+
+
+## build apr and apr-util 
+
+注意:该步骤是可选的,如果不使用mrcp语音识别,可以跳过。使用FunAsr也可以进行测试。
+
+```bash
+mkdir /home/mrcp/ && cd  /home/mrcp/
+apt-get install wget tar
+wget https://www.unimrcp.org/project/component-view/unimrcp-deps-1-6-0-tar-gz/download -O unimrcp-deps-1.6.0.tar.gz
+tar xvzf unimrcp-deps-1.6.0.tar.gz
+cd unimrcp-deps-1.6.0
+
+cd libs/apr
+./configure --prefix=/usr/local/apr
+make
+make install 
+
+cd ..
+cd apr-util
+./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr
+make
+make install
+
+cd ..
+cd sofia-sip/
+./configure --prefix=/usr/local/sofia-sip
+make 
+make install
+```
+
+## 编译 unimrcp
+
+注意:该步骤是可选的,如果不使用mrcp语音识别,可以跳过。使用FunAsr也可以进行测试。
+
+```bash
+cd /home/mrcp/
+git clone https://github.com/unispeech/unimrcp.git
+cd unimrcp
+./bootstrap
+./configure --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util  --with-sofia-sip=/usr/local/sofia-sip
+make
+make install
+```
+
+## 编译 mod_unimrcp
+
+注意:该步骤是可选的,如果不使用mrcp语音识别,可以跳过。使用FunAsr也可以进行测试。
+
+```bash
+cd /home/mrcp/
+git clone https://github.com/freeswitch/mod_unimrcp.git
+cd mod_unimrcp
+export PKG_CONFIG_PATH=/usr/local/freeswitchvideo/lib/pkgconfig:/usr/local/unimrcp/lib/pkgconfig
+./bootstrap.sh
+./configure
+make
+make install
+# copy lib files to freeswitch lib directory
+cp /usr/local/unimrcp/lib/libunimrcpclient.so.0 /usr/local/apr/lib/libapr-1.so.0  /usr/local/apr-util/lib/libaprutil-1.so.0  /usr/local/freeswitchvideo/lib/
+```
+
+## 替换FreeSWITCH配置文件
+
+  从 freeswitch-modules-libs\FreeSWITCH-Config-Files\conf 拷贝配置文件,覆盖掉FreeSWITCH默认的配置文件。
+  
+```bash
+cp -r /home/easyCallcenter365/freeswitch-modules-libs/FreeSWITCH-Config-Files/conf/*  /usr/local/freeswitchvideo/etc/freeswitch/
+cp -r /home/easyCallcenter365/freeswitch-modules-libs/FreeSWITCH-Config-Files/share/freeswitch/*  /usr/local/freeswitchvideo/share/freeswitch/
+rm -rf /usr/local/freeswitchvideo/etc/freeswitch/sip_profiles/*ipv6*
+```
+
+## 设置FreeSWITCH的数据库连接
+
+`docker` 的安装参考文档: [Debian12-install-docker.md](Debian12-install-docker.md) 。
+`mysql8` 的安装参考文档: [Debian12-install-mysql8.md](Debian12-install-mysql8.md) 。
+
+首先需要安装`mysql8`,创建一个名为 `freeswitch` 的数据库,然后导入sql文件: freeswitch-modules-libs\sql\freeswitch-1.10.11.sql 。
+这里假定设置`mysql`数据库设置密码为: `easycallcenter365`
+
+a. 修改 switch.conf.xml
+vim /usr/local/freeswitchvideo/etc/freeswitch/autoload_configs/switch.conf.xml , 在 `settings` 节点下增加配置:
+```xml
+<param name="core-db-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />
+```
+
+b. 修改 internal.xml
+vim /usr/local/freeswitchvideo/etc/freeswitch/sip_profiles/internal.xml , 在 `settings` 节点下增加配置:
+```xml
+<param name="odbc-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />
+```
+
+c. 修改 external.xml
+vim /usr/local/freeswitchvideo/etc/freeswitch/sip_profiles/external.xml , 在 `settings` 节点下增加配置:
+```xml
+<param name="odbc-dsn" value="mariadb://Server=127.0.0.1;Port=3306;Database=freeswitch;Uid=root;Pwd=easycallcenter365;" />
+```
+
+## 设置Debian12的中文支持
+
+解决乱码问题: vim ~/.profile  追加配置:
+
+```bash
+LANG=zh_CN.UTF-8
+LANGUAGE=zh_CN.UTF-8    
+```	
+
+如果不设置,会导致语音合成异常。让配置立即生效:
+
+```bash
+source ~/.profile
+```	
+
+## 语音识别和合成配置
+
+参考 [Set-up-ASR-TTS.md](Set-up-ASR-TTS.md)。
+
+## 启动FreeSWITCH
+
+所有参数设置好之后,尝试启动 `FreeSWITCH` 。
+```bash
+export LD_LIBRARY_PATH=/usr/local/freeswitchvideo/lib/
+/usr/local/freeswitchvideo/bin/freeswitch -nonat -nosql
+# 待 FreeSWITCH 启动后检查状态
+sofia status
+# 如果 sofia status 显示的记录为0,则说明数据库可能没有连接成功,请检查数据库配置
+
+# 检查mod_funasr及mod_aliyun_tts模块是否正常加载
+load mod_funasr      # 如果提示:Module mod_funasr Already Loaded! 说明加载成功
+load mod_aliyun_tts  # 如果提示:Module mod_aliyun_tts Already Loaded! 说明加载成功
+```
+
+
+
+
+
+
+

+ 25 - 0
docs/zh-cn/Set-up-ASR-TTS.md

@@ -0,0 +1,25 @@
+# 如何配置语音识别和语音合成
+
+## 配置语音识别
+
+语音识别支持 websocket 和 mrcp 两种对接方式。
+websocket对接方式下,开源版本仅提供了 funASR 的对接模块。
+mrcp对接方式下,可以对接阿里云MRCPServer,具体请参考阿里云官方文档 《SDM(MRCP-SERVER)公共云镜像使用》。
+
+配置 `funASR` 步骤如下:
+
+* 配置mod_funasr
+
+进入 `FreeSWITCH` 配置目录 /usr/local/freeswitchvideo/etc/freeswitch/autoload_configs , 编辑文件:
+vim funasr.conf.xml ,我们看到 `server_url` 参数为 `ws://127.0.0.1:2710` , 这里保持默认即可。
+
+* 配置funasr-server
+
+我们提供了 `funasr-server` 的一键安装包,funASR-0.1.9 链接: https://pan.baidu.com/s/1Cg1xUcxrsLMaUv8CklFLug 提取码: 4tke 。
+通过funASR官方文档进行安装,有比较大的安装失败概率。 下载安装包到本地,然后参考文档 "FunAsr-0.1.9-集群离线部署.txt" 进行安装。
+
+## 配置语音合成
+
+语音合成目前支持阿里云tts流式语音合成。登录到阿里云后台,搜索"智能语音交互",开通tts试用申请。
+ 找到FreeSWITCH配置文件目录下 /usr/local/freeswitchvideo/etc/freeswitch/autoload_configs/aliyun_tts.conf.xml 文件, 
+ 修改access_key_id、access_key_secret、app_key。 保存后进入FreeSWITCH控制台:  realod mod_aliyun_tts 即生效。

+ 29 - 0
docs/zh-cn/debian12-install-jdk8.md

@@ -0,0 +1,29 @@
+# debian12下安装jdk8
+
+jdk-8u391-linux-x64-debian12.tar.gz下载完成后,使用以下命令解压缩JDK安装文件:
+
+```sh
+tar -zxvf  jdk-8u391-linux-x64.tar.gz -C /usr/local
+```
+ 
+接下来,您需要编辑.bashrc文件,以设置JAVA_HOME和其他环境变量。使用以下命令打开.bashrc文件:
+
+```sh
+vi ~/.bashrc
+```
+ 
+在.bashrc文件中,在文件末尾处,添加以下行来设置JAVA_HOME、JRE_HOME、CLASSPATH和PATH变量:
+```txt
+export JAVA_HOME=/usr/local/jdk1.8.0_391
+export JRE_HOME=${JAVA_HOME}/jre
+export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
+export PATH=${JAVA_HOME}/bin:$PATH
+```
+
+保存并关闭.bashrc文件。
+
+运行以下命令使更新的.bashrc文件生效:
+
+```sh
+source ~/.bashrc
+```

BIN
docs/zh-cn/vmware-ipaddr-settings.png


Some files were not shown because too many files changed in this diff