// ==UserScript== // @name Confluence Plus // @namespace https://docs.scriptcat.org/ // @version 0.6.6 // @description Confluence helpers using the current browser session // @author Shu Su // @homepageURL https://scriptcat.org/en/script-show-page/6271 // @match https://*.atlassian.net/wiki/* // @connect openai.azure.com // @connect *.openai.azure.com // @connect cognitiveservices.azure.com // @connect *.cognitiveservices.azure.com // @icon data:image/png;base64,AAABAAQAQEAAAAEAIAAoQAAARgAAACAgAAABACAAKBAAAG5AAAAYGAAAAQAgACgJAACWUAAAEBAAAAEAIAAoBAAAvlkAACgAAABAAAAAgAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9/f3e/f397v39/e79/f3u/f397v39/e79/f3u/f397v39/e7/////+u7p7v///////////////9xxJu7aZxnu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZxnu3HEm7t+AP+7mmWPu8cGi7vru6e79/f3u/f397v39/e79/f3u//////39/e79/f3u/f397v39/e79/f3e/f399P//////////////////////////////////////6t3//////+N0Jv///////////+BmEf/gaRX/4WoY/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WoY/+BpFf/fZhH/32MM/99jDf/jdCX/76Vy///r3f///////////////////////////////////////f399P39/e7///////////////////////////76+P//////////////////////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/2mQS/9hbBP/aZhP/66p9//77+f////////////////////////////39/e79/f3u////////////////////////////////////////////////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2mUU/9daAf/hgT7////////////////////////////9/f3u/f397v//////////////////////////////////////////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2F0G///////44NH//////////////////f397v39/e7////////////////cbiP/2V8K///////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9tpGf//////3W8j//////////////////39/e79/f3u//////76+P/hgj7/2F0G/9tpGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////////////////////9/f3u/f397v/////rrID/11kB/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP///////////////////////f397v39/e797OD/2mUS/9plFP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj///////////////////////39/e79/f3u8Kd0/9hbA//baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/////////////////+vLq7uN1J//aZBL/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY//////////////////HFo+7fYwz/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/////////////////nmmPu32MM/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY////////////34A67uBmEf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY////////////22gY/9toGP//////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP///////////9xxJ+7gaRb/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP///////////9toGP///////////9toGP//////22cX/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////aahru4WoY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////baBj//////////////////////9lgDP/aZBL/22kZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9tnFv/ZYAz/2mMR/9tpGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FqGP//////2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY///////////////////////////////////////11L3/3nMo/9piD//baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/22YV///////aZBH/4ohJ/952LP/aYxD/22gZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP////////////////////////////////////////////LMsf/YXAb/22cX/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/aZRT/2FwF///////////////////////////////////////////////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/////////////////////////////////////////////////5phg/9haAf/baRn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9pkEv/YWwP/22kZ/+ifbf//////////////////////////////////////////////////////////////////////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baRn/2WEN///////poXD///////////////////////////////////////zz7P/feTT/2FoD/9toGP/baBj/22gY/9toGP/bZxb/2WEN/9hbA//cbiD/6qd5//no3P/////////////////////////////////////////////////////////////////baBj//////9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/2mMR/997M///////////////////////////////////////////////////////+uvi/+KGRf/YWwT/2FsF/9hcBf/YXAX/2mUS/+B/O//ttY3/+/Dn/////////////////////////////////////////////////////////////////////////////////9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9hbA//wvp3/////////////////////////////////////////////////////////////////9NC3/+2yjf/ssov/8cSl//jh0v///v7////////////////////////////////////////////////////////////////////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/ZYQ3/4YRE//78+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9hbA//ts4z//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/aZRP/2mQS//jg0f///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////994Mf/aYxD/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9lgCv/fezP//fbz//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////fdy//dbyH/2mUT/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baRn/2FwF/+WQVf///v3/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////997N/+ecZf/bZhL/2mQR/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/YWwP/55lj///+/f//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////5ZNb/9pjEP/YXAX/2mYV/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9hbBP/kjlL//PDp/////////////////////////////////////////////////////////////////////////////////////////////////////////////////9hdB//bZhb/22gZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/2V0G/950Kv/yx6n////////////////////////////////////////////////////////////////////////////////////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9tpGf/ZYg//2V4H/+KFQ//wwJ7/+uzj///////////////////////////////////////++ff/9+DR/+65lf/jik3/2mYV/9hbBP/aYg//22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/ZYAv/2F0G/9xsH//ig0P/5phh/+qneP/qqXz/6KJw/+WSWP/hgT7/3Gwd/9dYAP/VTwD/1lQA/9leCP/aZRT/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/aZRT/2F0H/9ZTAP/WUgD/2WAJ/952Lv/kjE//6KBu/+usgP/tsor/7LGH/+ifbP/jiEn/3W0g/9hdBv/ZYAz/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9tnF//ZXwv/2FwF/91wJP/nmWD/8ser//rr4v////////////////////////////////////////////vt5f/wwqH/4oVD/9ldBv/aYxD/22kZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2mYU/9hcBv//////4odH//DAn//88ev////////////////////////////////////////////////////////////////////////////wwaH/3G4h/9leCP/baRn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/aZBL/2FsE//////////////////////////////////////////////////////////////////////////////////////////////////////////////////rq3v/hg0L/2FsF/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9pjEf/YWwP/3G0g/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ff0/+OLUP/YXAX/22kZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9piD//dcST/7K+F///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9+Pb/4YRB/9ldBv/baRn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9liD//gfjr//PHq//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////rt4v/cbyH/2mIO/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/ZYAv/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9NG3/9heCP/bZxb/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22cX///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////poXD/2FwE/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////fXw/993MP/aYxD/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/////////////////////////////////////////////////////////////////////////////////9dXB///////////////////////////////////////////////////////////////////////////////////////ponP/2F0G/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY//////////////////////////////////////////////////////////////////ffzf///////////9lgDP/YXAX/2FwG/9hbBP//////////////////////////////////////////////////////////////////////3G0f/9plFP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/////////////////67OL/////////////////////////////////////////////////2FsE/9pjEf/baBf/22gY/9toGP/baBj/22gY/9dZAf/jiEj///////////////////////////////////////jh0////////////9pkE//baRn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////////////////////////////////////00Lf//////9lhDP/YXQb/22YW/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/11kA/+uqfv/////////////////////////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/aZhbu2mYW7uFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY///////baBj////////////77ub//vz7//LIq////////////9leCP/bZxf/22gZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9pmFf/ZYAz/9tvJ/////////////////////////////////////////////////9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/2mYW7tpmFu7haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP///////////9tpGf/aYxD/33Yt/+B9N//ZXwj/2V8K/9toF//baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2WAM/+B8Nv/zzbL/6J5q////////////////////////////////////////////22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9pmFu7aZhbu4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////baBj/22gZ/9pjEf/aYg//22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9tpGf/aYxH/2V8J/9lfCf/bZxf/22gY////////////22gY/////////////////9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FqGP/aahnu228j7uBpFv/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////////////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/gaBX/3HIn7t9+N+7gZhL/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj////////////baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/32YR/9+APe7mmGLu32MM/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/99iC///////8cCg7t9iC//baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/fYwv///////ru6e7jdCX/2mUT/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/aZBL/43Un///////9/f3u/////9hbBP/baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2FsD//Codv///////f397v3r3///////2mUU/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/2mUU/9tmFP/+7eH///////39/e7////////////////baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9dZAP/rrYD////////////9/f3u//////759v/gfzr//////9tpGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22kZ/9hdBv/hgj7//vr3/////////////f397v//////////+efb////////////22kZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22kZ/9lfCv/dcCT/+urf/////////////f397v39/e7////////////////33s/////////////baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9hdBv/dbyT/+OHT//////////////////39/e79/f3u//////////////////////nn2//hgD3/2FkB///////baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2mUU/9dZAP/hgT7/+ejc///////////////////////9/f3u/f397v///////////////////////////vr4/+uqfP/aZhP//////9pkEv/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/aZBL/2FsE/////////////////////////////////////////////f397v39/fT//////////////////////////////////////+ve/++lcf///////////99jDP/gZhL/4GkW/+FqGP/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FqGP/gaRX/32YR/99jDP/fYw3///////////////////////////////////////////////////////39/fT9/f3e/f397v39/e79/f3u/f397v39/e79/f3u/f397v39/e79/f3u+vHp7vHBou7//////////9xxJu7aZxju2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aZhbu2mYW7tpmFu7aahvu3HEm7t+APu7////////////////9/f3u/f397v39/e79/f3u/f397v39/e79/f3u/f397v39/e79/f3eKAAAACAAAABAAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP39/dz9/f3t/f397f39/e322sft55lm7d12Lu3baRnt22cW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22cW7dtpGe3ddi7t55xm7fbbyO39/f3t/f397f39/e39/f3c/f398///////////8Kl4/+BoFP/fYQr/4GgU/+FqGP/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WoY/+BoFP/fYQr/4GgU//Cqef////////////39/fP9/f3t//38/+SOUv/XWAD/22YV/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/bZhX/11gA/+SOUv///fz//f397f39/e3xrH3/11gA/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/11cA//Gsff/9/f3t99vM7eBnE//bZhX/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/bZhX/4GgU//fdzu3nnWjt32EK/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/fYQr/551o7d13Lu3gaBT/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22cW/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+BoFP/ddy7t22kZ7eFqGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gX/9lfCv/aZBH/2mQS/9tpGf/baBj/22gY/9toGP/baBj/22gY/9tnFv/ZXgn/2FwG/9tnF//baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsY/9tpGe3baBbt4WsZ/9toGP/baBj/22gY/9toGP/baBj/22kZ/9lhDv/ZXAb/5pVd//fcyf/ihkb/2F4I/9tpGf/baBj/22gZ/9pmFf/YXAb/2mIP/+aXX//utpD/22cW/9tnFv/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/22gW7dtoFu3haxn/22gY/9toGP/baBj/22gY/9toGf/ZYg//3nUs//LIq/////////////jh0v/aZRP/2FoD/9leCf/YWgP/22YV/+eZYv/33Mr////////////lklf/2F0H/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/2mEN/+GBP//++fb///////////////////////bayP/lkVf/5I9V/+ywhv/55df///////////////////////XXwv/ZXwn/22gX/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/baBj/22gY/9toGP/aZBL/3XEm//vv5////////////////////////////////////////////////////////////////////////////+SNUP/ZYAv/22gY/9toGP/baBj/22gY/9toGP/haxn/22gW7dtoFu3haxn/22gY/9toGP/baBj/22gY/9tpGf/YXQf/44tO////////////////////////////////////////////////////////////////////////////662C/9hcBv/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/YWwT/6aNy////////////////////////////////////////////////////////////9dfC/+WTWf/bZxf/22cX/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9tnF//YXAX/5pdg//vt5P/////////////////////////////////89O//8can/+OISf/ZYQ3/2F0H/9tnFv/baBn/22gY/9toGP/baBj/22gY/9toGP/haxn/22gW7dtoFu3haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/YXQf/3W8i/+mhb//wv57/8seq//HEpf/uto//5ZRb/9toGP/WUQD/2FwF/9toF//baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9tnFv/YWgP/1lMA/9xuIP/nmmT/77uX//LJrP/zzLL/8cSk/+micf/cbiH/2F0H/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gZ/9tmFf/YXAX/2mQR/+WRVv/zz7X//vr3//////////////////////////////////vs4v/lkln/2FsF/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/haxn/22gW7dtoFu3haxn/22gY/9toGP/baBj/22gY/9toGP/bZhb/3Gsc/+idaP/3387////////////////////////////////////////////////////////////nnGj/2FsE/9toGf/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22gY/9hcBv/vupf////////////////////////////////////////////////////////////////////////9/P/ihEL/2V8J/9toGf/baBj/22gY/9toGP/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/baBj/22gY/9toGP/baBj/2V8K/+aVXP////////////////////////////////////////////////////////////////////////////rq3//cbB//2mUT/9toGP/baBj/22gY/9toGP/haxn/22gW7dtoFu3haxn/22gY/9toGP/baBj/22gY/9toGP/bZxb/2WIO//fgz///////////////////////993L/+qoev/jik7/5JBV//ffzf///////////////////////PHq/994Mf/aYxD/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22gY/9toGP/YXAX/55tl////////////9dO8/+SQVP/aYg//2FsD/9lfCv/YWgL/22oa//ro3P///////////++9mv/cbiH/2mMQ/9toGf/baBj/22gY/9toGP/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9tmFf/cahz/7bSM/+SOUf/ZXwr/2F4I/9tnFv/baBj/22gY/9tpGf/YXQf/5I1R//XWwP/ji03/2FsE/9pjEf/baRn/22gY/9toGP/baBj/22gY/9toGP/haxn/22cW7dtpF+3haxn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/9tnFv/YXAX/2WAL/9tnF//baBj/22gY/9toGP/baBj/22gY/9tpGf/aZBH/2mIO/9lgC//baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+FqGP/baxnt3XYt7eBoFP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/bZxf/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/4GgU/913L+3nmWbt32IK/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/fYQr/551o7ffby+3gZxP/22YV/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22YV/+BoFP/33c7t/f397fCre//XWAD/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/XVwD/8a19//39/e39/f3t//37/+SNUf/XWAD/22YV/9toGf/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGf/bZhX/11gA/+SOU////vz//f397f39/fP///////////CpeP/gaBT/32EK/+BoFP/hahj/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FqGP/gaBT/32EK/+BoFP/wqnn////////////9/f3z/f393P39/e39/f3t/f397fbbyO3nnGbt3XYu7dtpGO3bZxbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3bZxbt22sZ7d13Lu3nnGft9tvI7f39/e39/f3t/f397f39/dwoAAAAGAAAADAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/f393P39/e38+fft67KJ7d56NO3baRjt2mcW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3aZxbt22kY7d56NO3rsort/Pn37f39/e39/f3c/f398//48P/oiEb/3l8H/+BmEv/hahj/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WoY/+BmEv/eXwf/6IlG///48P/9/f3z/Pf27eiKSP/YWgP/22gZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2FoD/+iKSP/89/ft7LSP7d5fBv/baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/95fBv/stZDt3ns17eBmEv/baBj/22gY/9toGP/baBj/22gY/9toGP/bZxb/22gX/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/+BmEv/eezXt22kY7eFrGP/baBj/22gY/9toGP/baBj/22gY/9lfCv/aZBH/2mQS/9toGf/baBj/22gY/9toGP/aYxD/2F0H/9lhDv/baRn/22gY/9toGP/baBj/22gY/+FrGP/baRjt22cW7eFrGf/baBj/22gY/9tpGf/aZRT/2FsE/+SNUf/22sf/44xP/9hZAv/aZhT/2WAM/9hbBP/edSz/7rmU/+idaf/ZXgn/22gZ/9toGP/baBj/22gY/+FrGf/bZxbt22gW7eFrGf/baBj/22gY/9plE//dbyL/8cSl////////////+une/+B9OP/baRv/4YI//++8mf/99vL///////rs4v/aZhX/2mYV/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9pjEf/edi3///////////////////////79+//77ub////////////////////////////po3P/2F0H/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGf/YXQf/55li/////////////////////////////////////////////////////v/ssIb/2V4J/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/2FwG/+qldf///fz////////////////////////////99fD/8MGf/+GAPf/aYg7/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22cX/9hdB//hgj//7riS//PNs//00bn/8cWm/+ecaP/baBj/1lUA/9lhDf/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/baBj/22gX/9lgC//XVgD/3G0f/+ihb//yyKv/9dS9//TQt//uuZX/4YA+/9hcBv/baBj/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/bZxf/2mMQ/+KGRv/yx6r//vn2/////////////////////////////vz6/+igbv/YXAb/22gY/9toGP/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/ZXwr/77qW///////////////////////////////////////////////////////lk1n/2F4I/9toGf/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/YXQf/66p9/////////////////////////v7/+uvi//79/P/////////////////++/n/3XAl/9pkEv/baBj/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9toGP/aZRT/22oa//zy6////////PLr/+21jf/gfTj/22YY/+B/O//77uX////////+/v/vu5j/3Gsd/9pmFP/baBj/22gY/+FrGf/baBbt22cW7eFrGf/baBj/22gY/9toGP/baBn/2V4J/+igbv/ts4v/3XEl/9hbA//ZYQ3/22YV/9dZAf/lklj/9dbA/+KGRf/YWwP/22YV/9toGf/baBj/22gY/+FrGf/bZxbt22gX7eFrGP/baBj/22gY/9toGP/baBj/22kZ/9lhDf/YXQf/2mQR/9toGf/baBj/22gY/9toGf/aZBL/2mIP/9lgDP/baBj/22gY/9toGP/baBj/22gY/+FrGP/baRjt3no07eBmEv/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBf/22cX/9toGP/baBj/22gY/9toGP/baBj/22gY/+BmEf/eezXt7LSP7d5fBv/baBn/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gZ/95fBv/stZDt/Pf27eiJR//YWgP/22gZ/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBj/22gY/9toGP/baBn/2FoD/+iKSP/89/bt/f398//47//oiEb/3l8H/+BmEv/hahj/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WoY/+BmEv/eXwf/6IlG///58P/9/f3z/f393P39/e38+fft67KK7d56NO3baRjt2mcW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt22gW7dtoFu3aZxbt22kZ7d57Ne3rsort/Pr37f39/e39/f3cKAAAABAAAAAgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP39/dz659nt5IpN7dtpGO3aZxbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt2mcW7dtpGO3kik3t+ufZ7f39/dz349fz6nos/95hCf/hahj/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/hahj/3mEJ/+p6Lf/35Nfz5ItN7d5gCf/baRn/22gY/9toGP/bZxb/22gX/9toGP/baBj/22gY/9toF//baBj/22gY/9tpGf/eYAn/5ItN7dtpGO3hahj/22gY/9toGf/ZYAv/2mMR/9pkEv/baBj/22cW/9leCf/ZXwr/2mYV/9tpGf/baBj/4WoY/9tpGO3bZxbt4WsZ/9toF//aZRT/44hJ//bYw//mlVz/1lQA/9pjEf/mll7/882y/992Lv/aYxH/22gY/+FrGf/bZxbt22gW7eFrGf/aZBL/3XEl//78+v///////ffy//HFp//44dH////////////vu5f/2FwG/9toGP/haxn/22gW7dtoFu3haxn/22gY/9lfC//qpnf////////////////////////////88uv/66t+/9lhDv/baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/bZxf/2V4J/+WUW//zy7D/997M//XUvf/on2z/22cW/9leCf/baBn/22gY/+FrGf/baBbt22gW7eFrGf/baBj/22gY/9leCP/bahv/6aNy//XWwP/3387/88yy/+WTWf/ZXgn/22gX/9toGP/haxn/22gW7dtoFu3haxn/22gX/9piD//ssYj//fXw////////////////////////////6aNy/9lfC//baBj/4WsZ/9toFu3baBbt4WsZ/9toGP/YXAb/8MCf////////////993L//HEpf/9+PT///////338//cbSD/2mUT/+FrGf/baBbt22cW7eFrGf/baBj/2mMQ/995Mv/yyq7/5ZBW/9piDv/WVAD/55li//XVvv/ihEL/2mUU/9toGP/haxn/22cW7dtpGO3hahj/22gY/9tpGf/aZhT/2V8J/9lfCv/bZxb/22gZ/9pkEv/aYg//2WAM/9toGf/baBj/4WoY/9tpGO3kik3t3mAJ/9tpGf/baBj/22gY/9toF//baBj/22gY/9toGP/baBf/22cW/9toGP/baBj/22kZ/95gCf/ki03t9+PX8+p6LP/eYQn/4WoY/+FrGf/haxn/4WsZ/+FrGf/haxn/4WsZ/+FrGf/haxn/4WoY/95gCf/qey3/9+TX8/39/dz659nt5IpN7dtpGO3aZxbt22gW7dtoFu3baBbt22gW7dtoFu3baBbt2mcW7dtpGO3kik3t+ujZ7f39/dwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_info // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; /******************************************************** * CONFIG ********************************************************/ const Config = { storageKey: 'confluence_plus_state', api: { v1Path: '/wiki/rest/api', v2Path: '/wiki/api/v2', }, request: { timeout: 15000, }, ai: { defaultApiVersion: '2025-03-01-preview', defaultModelsApiVersion: '2024-10-21', }, ui: { defaultWidth: 420, defaultHeight: 700, minWidth: 320, minHeight: 400, }, }; /******************************************************** * UTILS ********************************************************/ const Utils = { extractPageId(url = location.href) { const source = String(url || ''); const match = source.match(/\/pages\/(\d+)/i) || source.match(/\/page\/(\d+)/i) || source.match(/[?&#]pageId=(\d+)/i) || source.match(/[?&#]homepageId=(\d+)/i); if (match) { return match[1]; } try { const queryPageId = new URL(source, location.href).searchParams.get( 'homepageId' ); if (/^\d+$/.test(queryPageId || '')) { return queryPageId; } } catch (error) { // Fall back to the metadata lookup below. } // Confluence also exposes the current content id in page metadata on // some routes where the URL itself does not contain /pages/{id}. const meta = document.querySelector( 'meta[name="ajs-page-id"], meta[name="ajs-content-id"]' ); const dataElement = document.querySelector( '[data-page-id], [data-content-id]' ); const fallback = meta?.content || dataElement?.getAttribute('data-page-id') || dataElement?.getAttribute('data-content-id') || ''; return /^\d+$/.test(fallback) ? fallback : ''; }, isValidPageId(pageId) { return /^\d+$/.test(pageId); }, debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }, }; /******************************************************** * LOGGER ********************************************************/ const Logger = { info(event, data) { if (typeof data === 'undefined') { console.info(`[Confluence Plus] ${event}`); return; } console.info(`[Confluence Plus] ${event}`, data); }, warn(event, data) { console.warn(`[Confluence Plus] ${event}`, data); }, error(event, error) { console.error(`[Confluence Plus] ${event}`, error); }, }; /******************************************************** * STORAGE ********************************************************/ const Storage = { loadData() { try { const raw = GM_getValue(Config.storageKey, '{}'); const data = JSON.parse(raw); // Remove credentials written by pre-session versions. if (data.auth) { delete data.auth; GM_setValue( Config.storageKey, JSON.stringify(data) ); } return data; } catch (error) { Logger.error('storage.load-failed', error); return {}; } }, loadUi() { const ui = this.loadData().ui || {}; // Only the panel size is persistent. Menu state and position // intentionally reset for every page visit. const result = {}; if (typeof ui.width === 'number') { result.width = ui.width; } if (typeof ui.height === 'number') { result.height = ui.height; } return result; }, saveUi(ui) { const data = this.loadData(); data.ui = { width: ui.width, height: ui.height, }; GM_setValue( Config.storageKey, JSON.stringify(data) ); }, loadAi() { return this.loadData().ai || {}; }, saveAi(ai) { const data = this.loadData(); data.ai = ai; GM_setValue( Config.storageKey, JSON.stringify(data) ); }, clearAi() { const data = this.loadData(); delete data.ai; GM_setValue( Config.storageKey, JSON.stringify(data) ); }, }; /******************************************************** * AI CONFIGURATION ********************************************************/ const AIConfig = (() => { const defaults = { endpoint: '', apiKey: '', apiVersion: Config.ai.defaultApiVersion, modelsApiVersion: Config.ai.defaultModelsApiVersion, deployment: '', models: [], lastCheck: null, }; let config = { ...defaults, ...Storage.loadAi(), }; function normalizeEndpoint(endpoint) { const value = endpoint.trim().replace(/\/+$/, ''); const url = new URL(value); if (url.protocol !== 'https:') { throw new Error('Azure OpenAI Endpoint 必须使用 HTTPS'); } if (url.username || url.password || url.search || url.hash) { throw new Error('Endpoint 不应包含账号、查询参数或 hash'); } return url.origin + url.pathname.replace(/\/+$/, ''); } function validate(candidate) { const next = { ...config, ...candidate, }; if (!next.endpoint) { throw new Error('请输入 Azure OpenAI Endpoint'); } if (!next.apiKey) { throw new Error('请输入 Azure OpenAI API Key'); } next.endpoint = normalizeEndpoint(next.endpoint); next.apiVersion = next.apiVersion.trim(); next.modelsApiVersion = next.modelsApiVersion.trim(); next.deployment = next.deployment.trim(); if (!next.apiVersion || !next.modelsApiVersion) { throw new Error('API Version 不能为空'); } return next; } return { get() { return { ...config, models: [...config.models] }; }, validate, save(candidate) { const previous = config; const next = validate(candidate); const changed = previous.endpoint !== next.endpoint || previous.apiKey !== next.apiKey || previous.deployment !== next.deployment; const hasExplicitHealth = Object.prototype.hasOwnProperty.call( candidate, 'lastCheck' ); config = { ...next, lastCheck: hasExplicitHealth ? next.lastCheck : changed ? null : next.lastCheck || null, }; Storage.saveAi(config); return this.get(); }, recordHealth(ok, message) { config = { ...config, lastCheck: { ok, message, endpoint: config.endpoint, checkedAt: Date.now(), }, }; Storage.saveAi(config); }, getStatus() { if (!config.endpoint || !config.apiKey || !config.deployment) { return { key: 'not-configured', label: 'Not configured', }; } if (!config.lastCheck || config.lastCheck.endpoint !== config.endpoint) { return { key: 'unverified', label: 'Configured, not verified', }; } return config.lastCheck.ok ? { key: 'available', label: 'Available', } : { key: 'unavailable', label: 'Unavailable', }; }, clear() { config = { ...defaults }; Storage.clearAi(); }, isConfigured() { return Boolean(config.endpoint && config.apiKey); }, }; })(); /******************************************************** * STORE ********************************************************/ const Store = (() => { const state = { context: { pageId: '', url: location.href, }, ui: { x: 120, y: 120, width: Config.ui.defaultWidth, height: Config.ui.defaultHeight, minimized: true, docked: true, dockRight: 80, dockBottom: 12, maximized: false, }, }; const listeners = []; const persistUi = Utils.debounce(() => { Storage.saveUi(state.ui); }); function notify(type, value) { listeners.forEach((listener) => { listener(type, value, state); }); } return { init() { Object.assign(state.ui, Storage.loadUi()); }, getContext() { return state.context; }, getUi() { return state.ui; }, setContext(context) { state.context = { ...state.context, ...context, }; notify('context', state.context); }, updateUi(patch) { Object.assign(state.ui, patch); persistUi(); notify('ui', state.ui); }, resetPanelSession() { Object.assign(state.ui, { x: 120, y: 120, minimized: true, docked: true, dockRight: 80, dockBottom: 12, maximized: false, }); notify('ui', state.ui); }, subscribe(listener) { listeners.push(listener); return () => { const index = listeners.indexOf(listener); if (index >= 0) { listeners.splice(index, 1); } }; }, }; })(); /******************************************************** * ROUTER / PAGE CONTEXT ********************************************************/ const Router = (() => { let lastUrl = ''; function getContext() { const url = location.href; return { url, pageId: Utils.extractPageId(url), }; } function emitIfChanged() { const context = getContext(); if (context.url === lastUrl) { return; } lastUrl = context.url; // Reset before publishing the new route so the panel cannot keep // displaying tree data loaded for the previous Space. Store.resetPanelSession(); Store.setContext(context); Logger.info('route.changed', context); } function init() { const rawPushState = history.pushState; const rawReplaceState = history.replaceState; history.pushState = function (...args) { rawPushState.apply(this, args); setTimeout(emitIfChanged, 50); }; history.replaceState = function (...args) { rawReplaceState.apply(this, args); setTimeout(emitIfChanged, 50); }; window.addEventListener('popstate', emitIfChanged); emitIfChanged(); } return { init, getContext, }; })(); /******************************************************** * HTTP CLIENT ********************************************************/ const Http = { requestViaManager(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url, data: options.body, headers: { Accept: 'application/json', ...(options.headers || {}), }, timeout: Config.request.timeout, onload(response) { if (response.status < 200 || response.status >= 300) { const detail = (response.responseText || '') .replace(/\s+/g, ' ') .slice(0, 500); reject(new Error( `HTTP ${response.status}: ${detail}` )); return; } if (response.status === 204) { resolve(null); return; } try { resolve(JSON.parse(response.responseText)); } catch (error) { reject(new Error( 'Azure OpenAI 返回了非 JSON 响应' )); } }, onerror() { reject(new Error('Azure OpenAI 跨域请求失败')); }, ontimeout() { reject(new Error('Azure OpenAI API 请求超时')); }, }); }); }, async request(url, options = {}) { if ( options.crossOrigin && typeof GM_xmlhttpRequest === 'function' ) { return this.requestViaManager(url, options); } const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, Config.request.timeout); try { const response = await fetch(url, { ...options, credentials: options.credentials || 'same-origin', signal: controller.signal, headers: { Accept: 'application/json', ...(options.headers || {}), }, }); if (!response.ok) { const text = await response.text(); const detail = text.replace(/\s+/g, ' ').slice(0, 500); throw new Error( `HTTP ${response.status}: ${detail}` ); } if (response.status === 204) { return null; } const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { throw new Error( 'Confluence 返回了非 JSON 响应,可能需要重新登录' ); } return await response.json(); } catch (error) { if (error.name === 'AbortError') { throw new Error('Confluence API 请求超时'); } throw error; } finally { clearTimeout(timeoutId); } }, get(url, options = {}) { return this.request(url, { method: 'GET', ...options, }); }, post(url, body, options = {}) { return this.request(url, { ...options, method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json', ...(options.headers || {}), }, }); }, }; /******************************************************** * AZURE OPENAI API SERVICES ********************************************************/ const AzureOpenAI = { isV1Endpoint(endpoint) { return /\/openai\/v1$/i.test(endpoint); }, getUrl(endpoint, path, apiVersion) { if (this.isV1Endpoint(endpoint)) { return `${endpoint}${path}`; } const query = new URLSearchParams({ 'api-version': apiVersion, }); return `${endpoint}${path}?${query}`; }, getHeaders(apiKey) { return { 'api-key': apiKey, Accept: 'application/json', }; }, async listModels(candidate) { const config = AIConfig.validate(candidate); const modelsPath = this.isV1Endpoint(config.endpoint) ? '/models' : '/openai/models'; const url = this.getUrl( config.endpoint, modelsPath, config.modelsApiVersion ); const data = await Http.get(url, { crossOrigin: true, credentials: 'omit', headers: this.getHeaders(config.apiKey), }); const rawModels = Array.isArray(data) ? data : data.data || data.models || data.value || []; const models = rawModels .map((item) => { if (typeof item === 'string') { return { id: item, label: item, }; } const id = item.id || item.name || item.model || item.deploymentName; return id ? { id: String(id), label: String( item.displayName || id ), } : null; }) .filter(Boolean); return models; }, async testConnection(candidate) { const models = await this.listModels(candidate); return { models, message: models.length ? `连接成功,找到 ${models.length} 个模型` : '连接成功,但没有返回模型列表', }; }, getResponsesUrl(config) { const path = this.isV1Endpoint(config.endpoint) ? '/responses' : '/openai/responses'; return this.getUrl( config.endpoint, path, config.apiVersion ); }, extractOutputText(response) { if (typeof response.output_text === 'string') { return response.output_text; } const messages = response.output || []; const texts = []; messages.forEach((item) => { (item.content || []).forEach((content) => { if (content.type === 'output_text' && content.text) { texts.push(content.text); } }); }); return texts.join('\n'); }, async suggestLabels({ title, content, existingLabels }) { const config = AIConfig.get(); if (!AIConfig.isConfigured() || !config.deployment) { throw new Error('请先配置并保存可用的 AI Configuration'); } const prompt = [ 'Recommend 3 to 10 concise Confluence labels for this page.', 'Return JSON only in the shape {"labels":["label-a"]}.', 'Use lowercase letters, numbers, and hyphens only.', 'Do not include existing labels or explanations.', `Existing labels: ${(existingLabels || []).join(', ') || '(none)'}`, `Title: ${title || '(untitled)'}`, 'Page content:', content.slice(0, 24000), ].join('\n\n'); const response = await Http.post( this.getResponsesUrl(config), { model: config.deployment, input: prompt, }, { crossOrigin: true, credentials: 'omit', headers: { ...this.getHeaders(config.apiKey), }, } ); const output = this.extractOutputText(response); const match = output.match(/\{[\s\S]*\}/); if (!match) { throw new Error('AI 未返回可识别的 Label 建议'); } let parsed; try { parsed = JSON.parse(match[0]); } catch (error) { throw new Error('AI 返回的 Label 建议不是有效 JSON'); } return Array.isArray(parsed.labels) ? parsed.labels : []; }, }; /******************************************************** * CONFLUENCE API SERVICES ********************************************************/ const ConfluenceApi = { getV1Url(path) { return `${location.origin}${Config.api.v1Path}${path}`; }, getV2Url(path) { return `${location.origin}${Config.api.v2Path}${path}`; }, folders: { get(folderId) { return Http.get( ConfluenceApi.getV2Url( `/folders/${encodeURIComponent(folderId)}` ) ); }, }, spaces: { get(spaceId) { return Http.get( ConfluenceApi.getV2Url( `/spaces/${encodeURIComponent(spaceId)}` ) ); }, async getByKey(spaceKey) { const query = new URLSearchParams({ keys: spaceKey, limit: '1', }); const data = await Http.get( `${ConfluenceApi.getV2Url('/spaces')}?${query}` ); return Array.isArray(data.results) ? data.results[0] : null; }, }, pages: { get(pageId) { return Http.get( ConfluenceApi.getV2Url(`/pages/${pageId}`) ); }, getForAi(pageId) { const query = new URLSearchParams({ expand: 'body.storage,metadata.labels', }); return Http.get( `${ConfluenceApi.getV1Url(`/content/${pageId}`)}?${query}` ); }, addLabels(pageId, labels) { return Http.post( ConfluenceApi.getV1Url(`/content/${pageId}/label`), labels.map((name) => ({ prefix: 'global', name, })) ); }, getChildren(pageId) { return Http.get( ConfluenceApi.getV2Url( `/pages/${pageId}/children` ) ); }, getSpacePages(spaceId, cursor = '') { const query = new URLSearchParams({ limit: '250', }); if (cursor) { query.set('cursor', cursor); } return Http.get( `${ConfluenceApi.getV2Url( `/spaces/${encodeURIComponent(spaceId)}/pages` )}?${query}` ); }, }, pageStates: { get(pageId) { return Http.get( ConfluenceApi.getV1Url( `/content/${pageId}/state` ) ); }, }, }; /******************************************************** * AI LABEL SERVICE ********************************************************/ const AILabelService = { normalize(label) { return String(label || '') .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80); }, getExistingLabels(page) { return (page.metadata?.labels?.results || []) .map((item) => item.name) .filter(Boolean); }, async generate(pageId) { const page = await ConfluenceApi.pages.getForAi(pageId); const existingLabels = this.getExistingLabels(page); const body = page.body?.storage?.value || ''; const suggestions = await AzureOpenAI.suggestLabels({ title: page.title, content: body, existingLabels, }); const existing = new Set( existingLabels.map((label) => this.normalize(label)) ); const unique = []; suggestions.forEach((label) => { const normalized = this.normalize(label); if ( normalized && !existing.has(normalized) && !unique.includes(normalized) ) { unique.push(normalized); } }); return { page, existingLabels, suggestions: unique.slice(0, 10), }; }, }; /******************************************************** * SPACE TREE SERVICE ********************************************************/ const SpaceTreeService = { getNextCursor(data) { const directCursor = data.meta?.cursor || data.cursor || ''; if (directCursor) { return directCursor; } const next = data._links?.next || data.links?.next; if (!next) { return ''; } try { return new URL(next, location.origin) .searchParams.get('cursor') || ''; } catch (error) { return ''; } }, loadFromSidebar(currentPageId) { const containers = [ ...document.querySelectorAll( '[role="tree"], nav, aside, [data-testid*="sidebar" i], [data-testid*="tree" i]' ), ]; let best = null; let bestScore = 0; containers.forEach((container) => { const links = [...container.querySelectorAll( 'a[href*="/pages/"]' )]; const pageLinks = links.filter((link) => { const id = Utils.extractPageId(link.href); return id && link.getClientRects().length; }); const containsCurrent = pageLinks.some( (link) => Utils.extractPageId(link.href) === String(currentPageId) ); const score = pageLinks.length + (containsCurrent ? 1000 : 0); if (score > bestScore) { best = { container, links: pageLinks, }; bestScore = score; } }); if (!best || !best.links.length) { return []; } const items = []; const seen = new Set(); const stack = []; best.links.forEach((link) => { const id = Utils.extractPageId(link.href); if (!id || seen.has(id)) return; seen.add(id); const treeItem = link.closest('[role="treeitem"]'); const ariaLevel = treeItem?.getAttribute('aria-level'); let depth = ariaLevel ? Number(ariaLevel) - 1 : NaN; if (!Number.isFinite(depth)) { depth = 0; let listItem = link.closest('li'); while (listItem && best.container.contains(listItem)) { depth += 1; listItem = listItem.parentElement?.closest('li'); } depth = Math.max(0, depth - 1); } depth = Math.max(0, depth); stack.length = Math.min(stack.length, depth); const title = ( link.getAttribute('data-item-title') || link.getAttribute('aria-label') || link.textContent || '(Untitled)' ).trim(); const parent = depth > 0 ? stack[depth - 1] : null; const item = { id, title, parentId: parent ? parent.id : null, sidebarDepth: depth, }; items.push(item); stack[depth] = item; stack.length = depth + 1; }); return items; }, async load(spaceId) { const pages = []; let cursor = ''; do { const data = await ConfluenceApi.pages.getSpacePages( spaceId, cursor ); const results = Array.isArray(data.results) ? data.results : []; pages.push(...results); cursor = this.getNextCursor(data); } while (cursor); // The pages endpoint does not include folder entities. Pages whose // parentType is folder still expose the folder id, so load those // folder records separately and merge them into the tree. const folders = new Map(); const pendingFolderIds = new Set( pages .filter((page) => page.parentType === 'folder' && page.parentId) .map((page) => String(page.parentId)) ); while (pendingFolderIds.size) { const folderId = pendingFolderIds.values().next().value; pendingFolderIds.delete(folderId); if (folders.has(folderId)) continue; try { const folder = await ConfluenceApi.folders.get(folderId); if (!folder?.id) continue; const normalizedFolder = { ...folder, type: 'folder', id: String(folder.id), parentId: folder.parentId || null, parentType: folder.parentType || 'page', }; folders.set(normalizedFolder.id, normalizedFolder); if ( normalizedFolder.parentType === 'folder' && normalizedFolder.parentId ) { pendingFolderIds.add(String(normalizedFolder.parentId)); } } catch (error) { Logger.warn('space-tree.folder-load-failed', { folderId, error, }); } } return [...pages, ...folders.values()]; }, buildTree(pages, currentPageId, spaceRootTitle) { const normalizedSpaceName = (spaceRootTitle || '').trim(); const spaceRoot = pages.find((page) => ( (page.title || '').trim() === normalizedSpaceName )); const allowedIds = new Set(); if (spaceRoot) { allowedIds.add(String(spaceRoot.id)); let changed = true; while (changed) { changed = false; pages.forEach((page) => { const parentId = page.parentId || page.parent?.id; if ( parentId && allowedIds.has(String(parentId)) && !allowedIds.has(String(page.id)) ) { allowedIds.add(String(page.id)); changed = true; } }); } } const nodes = new Map(); pages.forEach((page, index) => { const id = String(page.id); if (!allowedIds.has(id)) return; const parentId = page.id === spaceRoot?.id ? null : page.parentId || page.parent?.id || null; const rawPosition = page.position ?? page.positionInParent ?? page.order ?? page.ordinal ?? page.sortOrder; const position = Number(rawPosition); nodes.set(id, { id, title: page.title || '(Untitled)', parentId: parentId ? String(parentId) : null, children: [], page, position: Number.isFinite(position) ? position : null, sourceIndex: index, }); }); const roots = []; nodes.forEach((node) => { const parent = node.parentId ? nodes.get(node.parentId) : null; if (parent && parent.id !== node.id) { parent.children.push(node); } else { roots.push(node); } }); const sortNodes = (items) => { items.sort((a, b) => { if (a.position !== null && b.position !== null) { return a.position - b.position || a.sourceIndex - b.sourceIndex; } if (a.position !== null) return -1; if (b.position !== null) return 1; return a.sourceIndex - b.sourceIndex; }); items.forEach((item) => sortNodes(item.children)); }; sortNodes(roots); const expanded = new Set(); let current = nodes.get(String(currentPageId)); while (current) { expanded.add(current.id); current = current.parentId ? nodes.get(current.parentId) : null; } return { roots, nodes, expanded, filteredCount: nodes.size, }; }, }; /******************************************************** * MODULES ********************************************************/ const Modules = [ { id: 'current-page', label: 'Get Current Page', canRun(context) { return Utils.isValidPageId(context.pageId); }, async run(context) { const data = await ConfluenceApi.pages.get( context.pageId ); Logger.info('current-page.loaded', data); return data; }, }, { id: 'page-status', label: 'Get Page Status', canRun(context) { return Utils.isValidPageId(context.pageId); }, async run(context) { const data = await ConfluenceApi.pageStates.get( context.pageId ); Logger.info('page-status.loaded', data); return data; }, }, { id: 'child-pages', label: 'Get Child Pages', canRun(context) { return Utils.isValidPageId(context.pageId); }, async run(context) { const data = await ConfluenceApi.pages.getChildren( context.pageId ); Logger.info('child-pages.loaded', data); return data; }, }, ]; /******************************************************** * MODULE RUNNER ********************************************************/ const ModuleRunner = { async run(module, button) { const context = Store.getContext(); if (!module.canRun(context)) { UI.toast( '当前页面没有有效的 Confluence Page ID', 'error' ); return; } if (button) { button.disabled = true; button.dataset.loading = '1'; } try { Logger.info(`module.start: ${module.id}`); return await module.run(context); } catch (error) { Logger.error(`module.failed: ${module.id}`, error); UI.toast(error.message, 'error'); } finally { if (button) { button.disabled = false; delete button.dataset.loading; } } }, }; /******************************************************** * UI PANEL ********************************************************/ const UI = (() => { let root; let unsubscribe; let overlay; let persistentFab; let contextMenu; let panelView = 'features'; let lastViewport = { width: window.innerWidth, height: window.innerHeight, }; function applyLayout() { if (!root) return; const ui = Store.getUi(); root.classList.toggle('cft-minimized', ui.minimized); root.classList.toggle('cft-expanded', !ui.minimized); root.style.position = 'fixed'; root.style.zIndex = '999999'; if (ui.minimized) { root.style.left = 'auto'; root.style.top = 'auto'; root.style.right = `${ui.dockRight}px`; root.style.bottom = `${ui.dockBottom}px`; root.style.width = '48px'; root.style.height = '48px'; return; } if (ui.maximized) { // Use an explicit border-box size so the resize observer can tell // the true fullscreen geometry from a user resize. root.style.left = '0px'; root.style.top = '0px'; root.style.right = 'auto'; root.style.bottom = 'auto'; root.style.width = `${window.innerWidth}px`; root.style.height = `${window.innerHeight}px`; return; } root.style.width = `${ui.width}px`; root.style.height = `${ui.height}px`; if (ui.docked) { root.style.left = 'auto'; root.style.top = 'auto'; // Keep the popup 48px icon width + 8px gap away from the right edge. root.style.right = `${ui.dockRight + 48 + 8}px`; root.style.bottom = `${ui.dockBottom}px`; } else { root.style.right = 'auto'; root.style.bottom = 'auto'; root.style.left = `${ui.x}px`; root.style.top = `${ui.y}px`; } } function centerPanel() { const ui = Store.getUi(); const width = Math.min( ui.width, Math.max(Config.ui.minWidth, window.innerWidth - 24) ); const height = Math.min( ui.height, Math.max(Config.ui.minHeight, window.innerHeight - 24) ); const x = Math.max(0, Math.round((window.innerWidth - width) / 2)); const y = Math.max(0, Math.round((window.innerHeight - height) / 2)); Store.updateUi({ x, y, width, height, docked: false, }); } function setMinimized(minimized) { Store.updateUi({ minimized }); render(); } function toggleMaximized() { const ui = Store.getUi(); if (ui.maximized) { Store.updateUi({ maximized: false, width: ui._normalWidth || ui.width, height: ui._normalHeight || ui.height, x: ui._normalX ?? ui.x, y: ui._normalY ?? ui.y, }); } else { Store.updateUi({ maximized: true, _normalWidth: ui.width, _normalHeight: ui.height, _normalX: ui.x, _normalY: ui.y, }); } render(); } function showMenuOverlay() { if (!overlay) { overlay = document.createElement('div'); overlay.id = 'cft-menu-overlay'; overlay.addEventListener('click', () => { setMinimized(true); }); document.body.appendChild(overlay); } overlay.style.display = 'block'; } function hideMenuOverlay() { if (overlay) { overlay.style.display = 'none'; } } function getIconMarkup() { const icon = typeof GM_info !== 'undefined' ? GM_info.script?.icon : ''; return icon ? `` : ''; } function createPersistentFab() { if (persistentFab) return; persistentFab = document.createElement('button'); persistentFab.id = 'cft-fab-persistent'; persistentFab.className = 'cft-fab-persistent'; persistentFab.title = 'Click to close the menu'; persistentFab.innerHTML = getIconMarkup(); persistentFab.addEventListener('click', () => { setMinimized(true); }); document.body.appendChild(persistentFab); } function positionPersistentFab() { if (!persistentFab) return; const ui = Store.getUi(); persistentFab.style.right = `${ui.dockRight}px`; persistentFab.style.bottom = `${ui.dockBottom}px`; } function removePersistentFab() { persistentFab?.remove(); persistentFab = null; } function openAiConfig() { const wrapper = document.createElement('div'); const config = AIConfig.get(); const currentStatus = AIConfig.getStatus(); let loadedModels = [...config.models]; let lastTest = null; wrapper.className = 'cft-modal-wrap'; wrapper.innerHTML = ` `; document.body.appendChild(wrapper); wrapper.querySelector('#cft-ai-back').addEventListener( 'click', () => { wrapper.remove(); render(); } ); const endpointInput = wrapper.querySelector('#cft-ai-endpoint'); const apiKeyInput = wrapper.querySelector('#cft-ai-api-key'); const deploymentInput = wrapper.querySelector('#cft-ai-deployment'); const modelsList = wrapper.querySelector('#cft-ai-model-options'); const status = wrapper.querySelector('#cft-ai-status'); endpointInput.value = config.endpoint; apiKeyInput.value = config.apiKey; deploymentInput.value = config.deployment; function getCandidate() { return { endpoint: endpointInput.value, apiKey: apiKeyInput.value, apiVersion: config.apiVersion, modelsApiVersion: config.modelsApiVersion, deployment: deploymentInput.value, models: loadedModels, }; } function renderModels(models) { modelsList.innerHTML = ''; models.forEach((model) => { const option = document.createElement('option'); option.value = model.id; option.label = model.label; modelsList.appendChild(option); }); } renderModels(config.models); wrapper.querySelector('#cft-ai-load-models').addEventListener( 'click', async (event) => { const button = event.currentTarget; button.disabled = true; status.className = 'cft-modal-status'; status.innerText = 'Connecting...'; try { const candidate = AIConfig.validate(getCandidate()); const result = await AzureOpenAI.testConnection(candidate); loadedModels = result.models; lastTest = { ok: true, message: result.message, endpoint: candidate.endpoint, }; renderModels(result.models); status.innerText = result.message; status.className = 'cft-modal-status success'; } catch (error) { lastTest = { ok: false, message: error.message, endpoint: endpointInput.value, }; status.innerText = error.message; status.className = 'cft-modal-status error'; } finally { button.disabled = false; } } ); wrapper.querySelector('#cft-ai-save').addEventListener( 'click', () => { try { const candidate = AIConfig.validate(getCandidate()); const health = lastTest && lastTest.endpoint === candidate.endpoint ? { ok: lastTest.ok, message: lastTest.message, endpoint: candidate.endpoint, checkedAt: Date.now(), } : null; const saved = AIConfig.save({ ...candidate, deployment: deploymentInput.value, models: loadedModels, lastCheck: health, }); wrapper.remove(); render(); toast( saved.deployment ? 'AI configuration saved' : 'AI configuration saved; select a Deployment before use' ); } catch (error) { status.innerText = error.message; status.className = 'cft-modal-status error'; } } ); wrapper.querySelector('#cft-ai-clear').addEventListener( 'click', () => { AIConfig.clear(); wrapper.remove(); render(); toast('AI configuration cleared'); } ); wrapper.querySelector('#cft-ai-cancel').addEventListener( 'click', () => { wrapper.remove(); render(); } ); } function renderSpaceTreePage() { let pageId = Store.getContext().pageId; root.innerHTML = `
Space Page Tree
Expand or collapse pages
Loading Space page tree...
`; const status = root.querySelector('#cft-tree-status'); const treeContainer = root.querySelector('#cft-space-tree'); let treeState = null; let currentSpaceKey = ''; root.querySelector('#cft-tree-back').addEventListener( 'click', () => { panelView = 'features'; render(true); } ); root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); function renderNodes(nodes) { const list = document.createElement('ul'); list.className = 'cft-tree-list'; nodes.forEach((node) => { const item = document.createElement('li'); item.className = 'cft-tree-item'; item.dataset.pageId = node.id; item.dataset.spaceKey = currentSpaceKey; item.dataset.nodeType = node.page?.type || 'page'; const row = document.createElement('div'); row.className = 'cft-tree-row'; if (node.id === String(pageId)) { row.classList.add('cft-tree-current'); row.dataset.treeCurrent = '1'; } if (node.children.length) { const toggle = document.createElement('button'); const isExpanded = treeState.expanded.has(node.id); toggle.className = 'cft-tree-toggle'; toggle.innerText = isExpanded ? '▾' : '▸'; toggle.title = isExpanded ? 'Collapse children' : 'Expand children'; toggle.addEventListener('click', () => { if (treeState.expanded.has(node.id)) { treeState.expanded.delete(node.id); } else { treeState.expanded.add(node.id); } renderTree(true); }); row.appendChild(toggle); } else { const spacer = document.createElement('span'); spacer.className = 'cft-tree-toggle-spacer'; row.appendChild(spacer); } const titleWrap = document.createElement('span'); titleWrap.className = 'cft-tree-title-wrap'; const title = document.createElement('span'); title.className = 'cft-tree-title'; title.innerText = node.title; title.title = `${node.title} (${node.id})`; titleWrap.appendChild(title); if (node.children.length > 0) { const childCount = document.createElement('span'); childCount.className = 'cft-tree-child-count'; childCount.innerText = `(${node.children.length})`; childCount.title = `${node.children.length} child page(s)`; titleWrap.appendChild(childCount); } if (node.id === String(pageId)) { const badge = document.createElement('span'); badge.className = 'cft-tree-current-badge'; badge.innerText = 'Current'; titleWrap.appendChild(badge); } row.appendChild(titleWrap); item.appendChild(row); if ( node.children.length && treeState.expanded.has(node.id) ) { item.appendChild(renderNodes(node.children)); } list.appendChild(item); }); return list; } function renderTree(preserveScroll = false) { const scrollTop = preserveScroll ? treeContainer.scrollTop : 0; treeContainer.innerHTML = ''; if (!treeState.roots.length) { treeContainer.innerText = 'No pages found in this Space.'; return; } treeContainer.appendChild(renderNodes(treeState.roots)); if (preserveScroll) { treeContainer.scrollTop = scrollTop; return; } const current = treeContainer.querySelector( '[data-tree-current="1"]' ); current?.scrollIntoView({ block: 'center', behavior: 'smooth', }); } async function loadTree() { status.className = 'cft-modal-status'; status.innerText = 'Loading Space page tree...'; treeContainer.innerHTML = ''; try { let space = null; if (!Utils.isValidPageId(pageId)) { const spaceMatch = location.pathname.match( /\/spaces\/([^/]+)/i ); const spaceKeyFromUrl = spaceMatch ? decodeURIComponent(spaceMatch[1]) : ''; if (spaceKeyFromUrl) { space = await ConfluenceApi.spaces.getByKey( spaceKeyFromUrl ); pageId = String( space?.homepageId || space?.homepage?.id || '' ); } } if (!Utils.isValidPageId(pageId)) { throw new Error('当前页面没有有效的 Confluence Page ID'); } const currentPage = await ConfluenceApi.pages.get(pageId); const spaceId = currentPage.spaceId || space?.id; if (!spaceId) { throw new Error('当前页面没有可识别的 Space ID'); } if (!space || String(space.id) !== String(spaceId)) { space = await ConfluenceApi.spaces.get(spaceId); } Store.setContext({ pageId }); const spaceName = (space.name || '').trim(); currentSpaceKey = (space.key || '').trim(); if (!spaceName || !currentSpaceKey) { throw new Error('当前 Space 没有可识别的名称或 Space Key'); } const pages = await SpaceTreeService.load(spaceId); treeState = SpaceTreeService.buildTree( pages, pageId, spaceName ); renderTree(); status.className = 'cft-modal-status success'; status.innerText = `Loaded ${treeState.filteredCount} page(s)`; } catch (error) { status.className = 'cft-modal-status error'; status.innerText = error.message; Logger.error('space-tree.load-failed', error); } } root.querySelector('#cft-tree-refresh').addEventListener( 'click', loadTree ); loadTree(); } function renderAiLabelsPage() { const pageId = Store.getContext().pageId; if (!Utils.isValidPageId(pageId)) { toast('当前页面没有有效的 Confluence Page ID', 'error'); panelView = 'features'; render(); return; } root.innerHTML = `
AI Generate Labels
Review before applying changes
Generate suggestions for the current page
The page title and content will be sent to the configured Azure OpenAI endpoint.
`; const status = root.querySelector('#cft-label-status'); const results = root.querySelector('#cft-label-results'); const generateButton = root.querySelector('#cft-label-generate'); let existingLabels = []; root.querySelector('#cft-label-back').addEventListener( 'click', () => { panelView = 'features'; render(true); } ); root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); function renderSuggestions(suggestions) { results.innerHTML = `
Recommended Labels
`; const list = results.querySelector('#cft-label-list'); function appendLabel(label, checked = true) { const normalized = AILabelService.normalize(label); if (!normalized || list.querySelector( `[data-label="${CSS.escape(normalized)}"]` )) { return; } const row = document.createElement('label'); row.className = 'cft-label-item'; row.dataset.label = normalized; row.innerHTML = ` `; row.querySelector('.cft-label-remove').addEventListener( 'click', () => row.remove() ); list.appendChild(row); } suggestions.forEach((label) => appendLabel(label)); results.querySelector('#cft-label-add-manual').addEventListener( 'click', () => { const input = results.querySelector('#cft-label-manual'); appendLabel(input.value); input.value = ''; } ); function showLabelConfirmation(labels, anchor) { return new Promise((resolve) => { const wrapper = document.createElement('div'); const rect = anchor.getBoundingClientRect(); const width = 300; const left = Math.min( Math.max(8, rect.left), window.innerWidth - width - 8 ); wrapper.className = 'cft-popconfirm-wrap'; wrapper.style.left = `${left}px`; wrapper.style.bottom = `${window.innerHeight - rect.top + 8}px`; wrapper.innerHTML = ` `; wrapper.querySelector('.cft-popconfirm-message').innerText = `${labels.length} label(s): ${labels.join(', ')}`; let finished = false; function finish(value) { if (finished) return; finished = true; document.removeEventListener('click', onDocumentClick, true); wrapper.remove(); resolve(value); } function onDocumentClick(event) { if (!wrapper.contains(event.target)) { finish(false); } } wrapper.querySelector('#cft-confirm-cancel').addEventListener( 'click', () => finish(false) ); wrapper.querySelector('#cft-confirm-ok').addEventListener( 'click', () => finish(true) ); document.body.appendChild(wrapper); wrapper.querySelector('#cft-confirm-ok').focus(); setTimeout(() => { document.addEventListener( 'click', onDocumentClick, true ); }, 0); }); } results.querySelector('#cft-label-apply').addEventListener( 'click', async (event) => { const selected = [...list.querySelectorAll('.cft-label-item')] .filter((row) => row.querySelector('input[type="checkbox"]').checked) .map((row) => AILabelService.normalize( row.querySelector('.cft-label-edit').value )) .filter(Boolean); const existing = new Set( existingLabels.map((label) => AILabelService.normalize(label)) ); const labels = [...new Set(selected)].filter( (label) => !existing.has(label) ); if (!labels.length) { status.className = 'cft-modal-status error'; status.innerText = 'Select at least one new valid label'; return; } const applyButton = event.currentTarget; const confirmed = await showLabelConfirmation( labels, applyButton ); if (!confirmed) { return; } applyButton.disabled = true; status.className = 'cft-modal-status'; status.innerText = 'Applying labels...'; try { await ConfluenceApi.pages.addLabels(pageId, labels); existingLabels.push(...labels); applyButton.disabled = true; status.className = 'cft-modal-status success'; status.innerText = `Added ${labels.length} label(s). Refreshing page...`; Logger.info('ai-labels.applied', { pageId, labels }); setTimeout(() => location.reload(), 800); } catch (error) { status.className = 'cft-modal-status error'; status.innerText = error.message; Logger.error('ai-labels.apply-failed', error); applyButton.disabled = false; } } ); } generateButton.addEventListener('click', async () => { if (!AIConfig.isConfigured()) { status.className = 'cft-modal-status error'; status.innerText = 'Configure AI before generating labels'; return; } generateButton.disabled = true; status.className = 'cft-modal-status'; status.innerText = 'Loading page content and generating suggestions...'; results.innerHTML = ''; try { const result = await AILabelService.generate(pageId); existingLabels = result.existingLabels; root.querySelector('#cft-label-page-title').innerText = result.page.title || 'Current Page'; renderSuggestions(result.suggestions); status.className = 'cft-modal-status success'; status.innerText = result.suggestions.length ? `Generated ${result.suggestions.length} suggestion(s)` : 'No new label suggestions were generated'; Logger.info('ai-labels.generated', { pageId, count: result.suggestions.length, }); } catch (error) { status.className = 'cft-modal-status error'; status.innerText = error.message; Logger.error('ai-labels.generate-failed', error); } finally { generateButton.disabled = false; } }); } function renderAiConfigPage() { const config = AIConfig.get(); const currentStatus = AIConfig.getStatus(); let loadedModels = [...config.models]; let lastTest = null; root.innerHTML = `
AI Configuration
Confluence Plus Settings
Azure OpenAI ${currentStatus.label}
Suggestions are optional. You can type the exact Deployment name.
`; const endpointInput = root.querySelector('#cft-ai-endpoint'); const apiKeyInput = root.querySelector('#cft-ai-api-key'); const deploymentInput = root.querySelector('#cft-ai-deployment'); const modelsList = root.querySelector('#cft-ai-model-options'); const status = root.querySelector('#cft-ai-status'); endpointInput.value = config.endpoint; apiKeyInput.value = config.apiKey; deploymentInput.value = config.deployment; function getCandidate() { return { endpoint: endpointInput.value, apiKey: apiKeyInput.value, apiVersion: config.apiVersion, modelsApiVersion: config.modelsApiVersion, deployment: deploymentInput.value, models: loadedModels, }; } function renderModels(models) { modelsList.innerHTML = ''; models.forEach((model) => { const option = document.createElement('option'); option.value = model.id; option.label = model.label; modelsList.appendChild(option); }); } renderModels(config.models); root.querySelector('#cft-ai-back').addEventListener( 'click', () => { panelView = 'config'; render(true); } ); root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); root.querySelector('#cft-ai-load-models').addEventListener( 'click', async (event) => { const button = event.currentTarget; button.disabled = true; status.className = 'cft-modal-status'; status.innerText = 'Connecting...'; try { const candidate = AIConfig.validate(getCandidate()); const result = await AzureOpenAI.testConnection(candidate); loadedModels = result.models; lastTest = { ok: true, message: result.message, endpoint: candidate.endpoint, }; renderModels(result.models); status.innerText = result.message; status.className = 'cft-modal-status success'; } catch (error) { lastTest = { ok: false, message: error.message, endpoint: endpointInput.value, }; status.innerText = error.message; status.className = 'cft-modal-status error'; } finally { button.disabled = false; } } ); root.querySelector('#cft-ai-save').addEventListener( 'click', () => { try { const candidate = AIConfig.validate(getCandidate()); const health = lastTest && lastTest.endpoint === candidate.endpoint ? { ok: lastTest.ok, message: lastTest.message, endpoint: candidate.endpoint, checkedAt: Date.now(), } : null; const saved = AIConfig.save({ ...candidate, models: loadedModels, lastCheck: health, }); panelView = 'config'; render(); toast( saved.deployment ? 'AI configuration saved' : 'AI configuration saved; select a Deployment before use' ); } catch (error) { status.innerText = error.message; status.className = 'cft-modal-status error'; } } ); root.querySelector('#cft-ai-clear').addEventListener( 'click', () => { AIConfig.clear(); panelView = 'config'; render(true); toast('AI configuration cleared'); } ); } function openMenu() { panelView = 'features'; if (Store.getUi().docked) centerPanel(); Store.updateUi({ minimized: false }); render(); } function openConfigPage() { panelView = 'config'; if (Store.getUi().docked) centerPanel(); Store.updateUi({ minimized: false }); render(); } function renderConfigMenu() { const status = AIConfig.getStatus(); const maximized = Store.getUi().maximized; root.innerHTML = `
Confluence Plus
Configuration Menu
`; root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); root.querySelector('#cft-switch-features').addEventListener( 'click', () => { panelView = 'features'; render(true); } ); root.querySelector('#cft-ai-settings').addEventListener( 'click', () => { panelView = 'ai-config'; render(true); } ); } function renderMenu() { const aiStatus = AIConfig.getStatus(); const maximized = Store.getUi().maximized; const aiReady = aiStatus.key === 'available'; const aiLabelTitle = aiReady ? 'Analyze the current article and suggest labels using the configured AI' : `Requires a ready AI configuration (${aiStatus.label}). Open the Configuration Menu to configure it`; root.innerHTML = `
Confluence Plus
Feature Menu
`; root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); root.querySelector('#cft-switch-config').addEventListener( 'click', () => { panelView = 'config'; render(true); } ); root.querySelector('#cft-space-tree-btn').addEventListener( 'click', () => { panelView = 'space-tree'; render(true); } ); root.querySelector('#cft-ai-label-btn').addEventListener( 'click', () => { if (AIConfig.getStatus().key !== 'available') { return; } panelView = 'ai-labels'; render(true); } ); } function protectHeaderControls() { root.querySelectorAll( '.cft-header-actions, .cft-header button, .cft-header-back' ).forEach((element) => { ['mousedown', 'pointerdown', 'touchstart'].forEach((eventName) => { element.addEventListener(eventName, (event) => { event.stopPropagation(); }); }); }); } function render(preserveLayout = false) { if (!root) return; // Menu navigation must not alter the panel geometry, even when a page // has very different content from the previous one. const layoutBeforeRender = preserveLayout ? { ...Store.getUi() } : null; applyLayout(); if (Store.getUi().minimized) { hideMenuOverlay(); removePersistentFab(); root.innerHTML = ` `; const fab = root.querySelector('#cft-fab'); fab.addEventListener('click', (event) => { if (event.button === 0) { openMenu(); } }); fab.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); openConfigPage(); }); return; } showMenuOverlay(); createPersistentFab(); positionPersistentFab(); if (panelView === 'config') { renderConfigMenu(); } else if (panelView === 'ai-config') { renderAiConfigPage(); } else if (panelView === 'ai-labels') { renderAiLabelsPage(); } else if (panelView === 'space-tree') { renderSpaceTreePage(); } else { renderMenu(); } protectHeaderControls(); root.querySelector('#cft-toggle-maximize')?.addEventListener( 'click', toggleMaximized ); if (layoutBeforeRender) { Store.updateUi({ x: layoutBeforeRender.x, y: layoutBeforeRender.y, width: layoutBeforeRender.width, height: layoutBeforeRender.height, docked: layoutBeforeRender.docked, dockRight: layoutBeforeRender.dockRight, dockBottom: layoutBeforeRender.dockBottom, maximized: layoutBeforeRender.maximized, }); applyLayout(); } } function renderLegacy() { if (!root) return; applyLayout(); const aiStatus = AIConfig.getStatus(); if (Store.getUi().minimized) { root.innerHTML = ` `; root.querySelector('#cft-fab').addEventListener( 'click', () => setMinimized(false) ); return; } root.innerHTML = `
Confluence Plus
Browser session
Context
Modules
`; root.querySelector('#cft-close-btn').addEventListener( 'click', () => setMinimized(true) ); root.querySelector('#cft-switch-features').addEventListener( 'click', () => { panelView = 'features'; render(true); } ); root.querySelector('#cft-ai-settings').addEventListener( 'click', () => { panelView = 'ai-config'; render(true); } ); root.querySelector('#cft-toggle-maximize')?.addEventListener( 'click', toggleMaximized ); renderModules(); } function renderModules() { const container = root.querySelector('#cft-modules'); Modules.forEach((module) => { const button = document.createElement('button'); button.className = 'cft-module-btn'; button.innerText = module.label; button.addEventListener('click', (event) => { ModuleRunner.run(module, event.currentTarget); }); container.appendChild(button); }); } function updatePageId() { const input = root?.querySelector('#cft-current-page-id'); if (input) { input.value = Store.getContext().pageId; } } function toast(message, type = 'info') { const element = document.createElement('div'); element.className = `cft-toast ${type}`; element.innerText = message; document.body.appendChild(element); setTimeout(() => element.remove(), 2500); } function hideContextMenu() { contextMenu?.remove(); contextMenu = null; } function showContextMenu(event) { if (Store.getUi().minimized) return; event.preventDefault(); event.stopPropagation(); hideContextMenu(); const script = typeof GM_info !== 'undefined' ? GM_info.script || {} : {}; const name = script.name || 'Confluence Plus'; const version = script.version || 'unknown'; const treeRow = event.target.closest('.cft-tree-row'); const treeItem = treeRow?.closest('.cft-tree-item'); const pageId = treeItem?.dataset.pageId || ''; const spaceKey = treeItem?.dataset.spaceKey || ''; const nodeType = treeItem?.dataset.nodeType || 'page'; const canOpenPage = Boolean( pageId && spaceKey && nodeType !== 'folder' ); const menuWidth = 220; const menuHeight = canOpenPage ? 92 : 54; const left = Math.min( Math.max(8, event.clientX), window.innerWidth - menuWidth - 8 ); const top = Math.min( Math.max(8, event.clientY), window.innerHeight - menuHeight - 8 ); contextMenu = document.createElement('div'); contextMenu.className = 'cft-context-menu'; contextMenu.setAttribute('role', 'menu'); contextMenu.style.left = `${left}px`; contextMenu.style.top = `${top}px`; if (canOpenPage) { const openPage = document.createElement('button'); openPage.type = 'button'; openPage.className = 'cft-context-menu-action'; openPage.setAttribute('role', 'menuitem'); openPage.textContent = 'Open page'; openPage.addEventListener('click', (clickEvent) => { clickEvent.preventDefault(); clickEvent.stopPropagation(); hideContextMenu(); window.location.href = `${location.origin}/wiki/spaces/${encodeURIComponent(spaceKey)}/pages/${pageId}`; }); contextMenu.appendChild(openPage); } const item = document.createElement('div'); item.className = 'cft-context-menu-item cft-context-menu-info'; item.setAttribute('role', 'menuitem'); item.setAttribute('aria-disabled', 'true'); item.textContent = `${name} v${version}`; contextMenu.appendChild(item); document.body.appendChild(contextMenu); setTimeout(() => { document.addEventListener('click', hideContextMenu, { once: true, capture: true, }); }, 0); } function enableDrag() { let dragging = false; let offsetX = 0; let offsetY = 0; root.addEventListener('dblclick', (event) => { if (Store.getUi().minimized || event.button !== 0) return; const header = event.target.closest('.cft-header'); const interactive = event.target.closest( 'button, input, select, textarea, a, .cft-header-actions' ); if (!header || interactive) return; event.preventDefault(); event.stopPropagation(); toggleMaximized(); }); root.addEventListener('mousedown', (event) => { if ( Store.getUi().minimized || Store.getUi().maximized || event.button !== 0 ) return; const header = event.target.closest('.cft-header'); const interactive = event.target.closest( 'button, input, select, textarea, a, .cft-header-actions' ); if (!header || interactive) { return; } const rect = root.getBoundingClientRect(); dragging = true; offsetX = event.clientX - rect.left; offsetY = event.clientY - rect.top; Store.updateUi({ docked: false }); document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (event) => { if (!dragging) return; const rect = root.getBoundingClientRect(); const maxX = Math.max(0, window.innerWidth - rect.width); const maxY = Math.max(0, window.innerHeight - rect.height); const x = Math.min( maxX, Math.max(0, event.clientX - offsetX) ); const y = Math.min( maxY, Math.max(0, event.clientY - offsetY) ); root.style.left = `${x}px`; root.style.top = `${y}px`; root.style.right = 'auto'; root.style.bottom = 'auto'; Store.updateUi({ x, y }); }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; document.body.style.userSelect = ''; }); } function observeResize() { if (!window.ResizeObserver) return; const observer = new ResizeObserver( Utils.debounce(() => { if (!root || Store.getUi().minimized) return; const ui = Store.getUi(); const rect = root.getBoundingClientRect(); const width = Math.round(rect.width); const height = Math.round(rect.height); const fullscreenWidth = Math.round(window.innerWidth); const fullscreenHeight = Math.round(window.innerHeight); const isFullscreenGeometry = Math.abs(width - fullscreenWidth) <= 1 && Math.abs(height - fullscreenHeight) <= 1 && Math.abs(rect.left) <= 1 && Math.abs(rect.top) <= 1; if (!ui.maximized && isFullscreenGeometry) { // A normal window manually resized exactly to the viewport is // logically fullscreen, so the button must switch to Restore. Store.updateUi({ maximized: true, _normalWidth: ui.width, _normalHeight: ui.height, _normalX: ui.x, _normalY: ui.y, }); render(); return; } if (ui.maximized) { const userResized = !isFullscreenGeometry; if (!userResized) return; // A resize from the lower-right corner means the user wants a // normal window again. Keep the size and location they chose. Store.updateUi({ maximized: false, width, height, x: Math.max(0, Math.round(rect.left)), y: Math.max(0, Math.round(rect.top)), docked: false, }); render(); return; } Store.updateUi({ width, height }); }) ); observer.observe(root); } function handleViewportResize() { const viewportChanged = window.innerWidth !== lastViewport.width || window.innerHeight !== lastViewport.height; lastViewport = { width: window.innerWidth, height: window.innerHeight, }; if (!viewportChanged || Store.getUi().minimized) return; // Keep the expanded panel centered whenever the browser viewport // changes. A maximized panel first returns to its normal state. if (Store.getUi().maximized) { Store.updateUi({ maximized: false }); } centerPanel(); render(); } function init() { root = document.createElement('div'); root.id = 'cft-root'; root.addEventListener('contextmenu', showContextMenu); document.body.appendChild(root); window.addEventListener('resize', handleViewportResize); render(); enableDrag(); observeResize(); unsubscribe = Store.subscribe((type) => { if (type === 'context') { updatePageId(); } if (type === 'ui' && Store.getUi().minimized) { render(); } }); } return { init, render, toast, destroy() { unsubscribe?.(); hideContextMenu(); window.removeEventListener('resize', handleViewportResize); root?.remove(); }, }; })(); /******************************************************** * STYLES ********************************************************/ GM_addStyle(` #cft-menu-overlay { position: fixed; inset: 0; z-index: 999998; background: rgba(9, 30, 66, .28); backdrop-filter: blur(1px); } #cft-root { position: fixed; z-index: 999999; font-size: 14px; color: #333; } .cft-context-menu { position: fixed; z-index: 1000003; width: 220px; box-sizing: border-box; border: 1px solid #dfe1e6; border-radius: 6px; background: white; box-shadow: 0 8px 24px rgba(9, 30, 66, .24); padding: 4px 0; color: #172b4d; font-size: 12px; } .cft-context-menu-item, .cft-context-menu-action { display: block; width: 100%; box-sizing: border-box; border: none; background: transparent; padding: 9px 12px; text-align: left; white-space: nowrap; user-select: none; } .cft-context-menu-action { color: #172b4d; cursor: pointer; font: inherit; } .cft-context-menu-action:hover { background: #deebff; color: #0747a6; } .cft-context-menu-info { color: #8993a4; cursor: default; } #cft-root.cft-expanded { background: white; border: 1px solid #ddd; border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, .15); overflow: hidden; box-sizing: border-box; resize: both; min-width: ${Config.ui.minWidth}px; min-height: ${Config.ui.minHeight}px; max-width: 100vw; max-height: 100vh; } #cft-root.cft-minimized { background: transparent; border: none; box-shadow: none; overflow: visible; resize: none; } #cft-fab { width: 48px; height: 48px; border: none; border-radius: 14px; background: #172b4d; box-shadow: 0 8px 20px rgba(9, 30, 66, .25); cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; } #cft-fab:hover { transform: translateY(-1px); } #cft-fab-persistent { position: fixed; z-index: 999997; width: 48px; height: 48px; border: none; border-radius: 14px; background: #172b4d; box-shadow: 0 8px 20px rgba(9, 30, 66, .25); cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; } .cft-fab-tooltip { position: absolute; right: 58px; bottom: 0; z-index: 1000002; display: none; width: 190px; padding: 10px 12px; border: 1px solid rgba(255, 255, 255, .12); border-radius: 10px; background: #172b4d; box-shadow: 0 10px 28px rgba(9, 30, 66, .28); color: white; font-size: 12px; line-height: 1.5; pointer-events: none; } .cft-fab-tooltip div { display: flex; align-items: center; gap: 8px; } .cft-fab-tooltip div + div { margin-top: 5px; } .cft-fab-tooltip strong { min-width: 34px; border-radius: 4px; background: rgba(255, 255, 255, .16); padding: 2px 4px; text-align: center; color: #deebff; } #cft-root.cft-minimized:hover .cft-fab-tooltip { display: block; } .cft-fab-image { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; } .cft-fab-logo { width: 28px; height: 28px; border-radius: 8px; background: linear-gradient( 135deg, #6554c0, #00b8d9, #36b37e, #ffab00 ); color: white; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; } .cft-header { height: 48px; padding: 0 16px; background: #0052cc; color: white; display: flex; align-items: center; justify-content: space-between; cursor: move; user-select: none; } .cft-header-title { flex: 0 0 auto; min-width: 0; } .cft-session-label { margin-top: 2px; font-size: 11px; opacity: .8; } .cft-header-actions { display: flex; align-items: center; gap: 6px; } .cft-header button { width: 28px; height: 28px; border: none; border-radius: 6px; background: rgba(255, 255, 255, .2); color: white; font-size: 18px; line-height: 1; cursor: pointer; } .cft-header-icon-btn { font-size: 15px !important; } .cft-body { height: calc(100% - 48px); overflow: auto; padding: 12px; box-sizing: border-box; } .cft-section { margin-bottom: 20px; } .cft-title { margin-bottom: 12px; font-weight: bold; } .cft-label { display: block; margin-bottom: 4px; color: #666; font-size: 12px; } .cft-page-id { width: 100%; box-sizing: border-box; border: 1px solid #ddd; border-radius: 6px; padding: 8px; } .cft-module-btn { width: 100%; margin-bottom: 10px; border: none; border-radius: 8px; background: #0052cc; color: white; padding: 10px; cursor: pointer; } .cft-module-btn:hover:not(:disabled) { opacity: .9; } .cft-module-btn:disabled { cursor: wait; opacity: .6; } .cft-module-btn[data-loading]::after { content: ' ...'; } .cft-tree-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; } .cft-tree-refresh { width: 28px; height: 28px; border: 1px solid #dfe1e6; border-radius: 6px; background: white; color: #172b4d; cursor: pointer; font-size: 17px; } .cft-space-tree { max-height: calc(100% - 48px); overflow: auto; padding: 4px 0; } .cft-tree-list { margin: 0; padding-left: 0; list-style: none; } .cft-tree-item .cft-tree-list { margin-left: 18px; } .cft-tree-row { position: relative; display: flex; align-items: center; gap: 6px; min-height: 32px; border-radius: 6px; padding: 3px 6px; } .cft-tree-row:hover { background: #f4f5f7; } .cft-tree-current { background: #deebff; color: #0747a6; font-weight: 600; } .cft-tree-toggle, .cft-tree-toggle-spacer { width: 22px; height: 24px; flex: 0 0 22px; } .cft-tree-toggle { border: none; border-radius: 4px; background: transparent; color: #42526e; cursor: pointer; font-size: 15px; line-height: 1; } .cft-tree-toggle:hover { background: #dfe1e6; } .cft-tree-title-wrap { min-width: 0; flex: 1; display: flex; align-items: baseline; overflow: hidden; } .cft-tree-title { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cft-tree-child-count { flex: 0 0 auto; margin-left: 4px; color: #0052cc; font-size: 11px; font-weight: 600; white-space: nowrap; } .cft-tree-current-badge { flex: 0 0 auto; margin-left: 4px; border-radius: 999px; background: #0052cc; color: white; padding: 2px 6px; font-size: 10px; white-space: nowrap; } .cft-label-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } .cft-label-item { display: flex; align-items: center; gap: 8px; } .cft-label-item input[type="checkbox"] { width: 16px; height: 16px; flex: 0 0 auto; } .cft-label-edit { min-width: 0; flex: 1; border: 1px solid #ccc; border-radius: 6px; padding: 7px; } .cft-label-remove { width: 28px; height: 28px; border: none; border-radius: 6px; background: #ffebe6; color: #bf2600; cursor: pointer; font-size: 16px; } .cft-label-add-row { display: flex; gap: 8px; margin-bottom: 12px; } .cft-popconfirm-wrap { position: fixed; z-index: 1000002; width: 300px; } .cft-popconfirm { position: relative; border: 1px solid #dfe1e6; border-radius: 8px; background: white; box-shadow: 0 8px 24px rgba(9, 30, 66, .22); padding: 14px; } .cft-popconfirm::after { content: ''; position: absolute; left: 24px; bottom: -7px; width: 12px; height: 12px; border-right: 1px solid #dfe1e6; border-bottom: 1px solid #dfe1e6; background: white; transform: rotate(45deg); } .cft-popconfirm-title { color: #172b4d; font-size: 14px; font-weight: 700; } .cft-popconfirm-message { margin-top: 7px; color: #42526e; font-size: 12px; line-height: 1.45; word-break: break-word; } .cft-popconfirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } .cft-popconfirm-actions button { border: none; border-radius: 6px; padding: 6px 10px; cursor: pointer; font-size: 12px; } .cft-confirm-cancel { background: #f1f2f4; color: #172b4d; } .cft-confirm-ok { background: #0052cc; color: white; } .cft-label-add-row .cft-secondary-btn { width: auto; } .cft-feature-btn { margin-bottom: 10px; } .cft-secondary-btn { width: 100%; border: 1px solid #0052cc; border-radius: 8px; background: white; color: #0052cc; padding: 9px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; } .cft-status-badge { display: inline-block; border-radius: 999px; padding: 3px 7px; font-size: 11px; font-weight: 600; white-space: nowrap; } .cft-status-badge.not-configured { background: #f1f2f4; color: #6b778c; } .cft-status-badge.unverified { background: #fff0b3; color: #7f5f01; } .cft-status-badge.available { background: #abf5d1; color: #006644; } .cft-status-badge.unavailable { background: #ffbdad; color: #bf2600; } .cft-menu-hint { color: #6b778c; font-size: 11px; line-height: 1.5; } .cft-modal-wrap { position: fixed; inset: 0; z-index: 10000000; background: rgba(0, 0, 0, .4); display: flex; align-items: center; justify-content: center; padding: 16px; } .cft-modal { width: min(440px, 100%); max-height: 90vh; overflow: auto; box-sizing: border-box; border-radius: 12px; background: white; padding: 20px; box-shadow: 0 12px 32px rgba(0, 0, 0, .24); } .cft-modal-title { margin-bottom: 18px; font-size: 18px; font-weight: bold; } .cft-modal-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .cft-back-btn { width: 30px; height: 30px; border: none; border-radius: 6px; background: #f1f2f4; color: #172b4d; cursor: pointer; font-size: 18px; line-height: 1; } .cft-title-spacer { width: 30px; } .cft-modal-section-title { margin: 18px 0 8px; color: #172b4d; font-size: 13px; font-weight: 700; border-bottom: 1px solid #ebecf0; padding-bottom: 6px; } .cft-modal-section-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .cft-modal-label { display: block; margin: 12px 0 4px; color: #555; font-size: 12px; } .cft-modal-input { width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; padding: 9px; } .cft-modal-hint { margin-top: 6px; color: #6b778c; font-size: 11px; line-height: 1.4; } .cft-modal-action, .cft-modal-actions button { border: none; border-radius: 7px; background: #0052cc; color: white; padding: 9px 12px; cursor: pointer; } .cft-modal-action { width: 100%; margin-top: 16px; } .cft-modal-action:disabled, .cft-modal-actions button:disabled { cursor: wait; opacity: .6; } .cft-modal-actions { display: flex; gap: 8px; margin-top: 16px; } .cft-modal-actions button { flex: 1; } .cft-modal-actions button:nth-child(2) { background: #6b778c; } .cft-modal-actions button:nth-child(3) { background: #f1f2f4; color: #172b4d; } .cft-modal-status { min-height: 18px; margin-top: 10px; font-size: 12px; white-space: pre-wrap; } .cft-modal-status.success { color: #16803c; } .cft-modal-status.error { color: #d93025; } .cft-toast { position: fixed; right: 20px; bottom: 20px; z-index: 9999999; border-radius: 8px; background: #333; color: white; padding: 10px 14px; } .cft-toast.error { background: #d93025; } `); /******************************************************** * BOOTSTRAP ********************************************************/ function bootstrap() { Store.init(); Router.init(); UI.init(); Logger.info('initialized', { version: GM_info.script.version, origin: location.origin, }); } bootstrap(); })();