AIGPT Everywhere

Mini A.I. floating menu that can define words, answer questions, translate, and much more in a single click and with your custom prompts. Includes useful click to search on Google and copy selected text buttons, along with Rocker+Mouse Gestures and Units+Currency+Time zone Converters, all features can be easily modified or disabled

当前为 2025-06-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AIGPT Everywhere
  3. // @namespace OperaBrowserGestures
  4. // @description Mini A.I. floating menu that can define words, answer questions, translate, and much more in a single click and with your custom prompts. Includes useful click to search on Google and copy selected text buttons, along with Rocker+Mouse Gestures and Units+Currency+Time zone Converters, all features can be easily modified or disabled
  5. // @version 79
  6. // @author hacker09
  7. // @include *
  8. // @exclude https://accounts.google.com/v3/signin/*
  9. // @icon https://i.imgur.com/8iw8GOm.png
  10. // @grant GM_unregisterMenuCommand
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_getResourceText
  13. // @grant GM.xmlHttpRequest
  14. // @grant GM_setClipboard
  15. // @grant GM_deleteValue
  16. // @grant GM_openInTab
  17. // @grant window.close
  18. // @run-at document-end
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // @connect google.com
  22. // @connect generativelanguage.googleapis.com
  23. // @resource AIMenuContent https://cyber-sec0.github.io/AIMenu.html
  24. // @require https://update.greasyfork.org/scripts/506699/marked.js
  25. // @require https://unpkg.com/@highlightjs/cdn-assets/highlight.min.js
  26. // @require https://update.greasyfork.org/scripts/519002/Units%20Converter.js
  27. // @require https://update.greasyfork.org/scripts/538628/Gemini%20AI%20Stream%20Parser.js
  28. // ==/UserScript==
  29.  
  30. /* jshint esversion: 11 */
  31. const toHTML = html => window.trustedTypes?.createPolicy('BypassTT', { createHTML: HTML => HTML })?.createHTML(html) || html; //Bypass Trusted Types API+create safe HTML for chromium browsers
  32.  
  33. if(GM_getValue("SearchHighlight") === undefined) ["SearchHighlight", "MouseGestures", "TimeConverter", "UnitsConverter", "CurrenciesConverter"].forEach(option => GM_setValue(option, true)); //Set up everything on the first run
  34.  
  35. if ((location.href === 'https://aistudio.google.com/app/apikey' && document.querySelector(".apikey-link") !== null) && GM_getValue("APIKey") === undefined || GM_getValue("APIKey") === null || GM_getValue("APIKey") === '') { //Set up the API Key
  36. window.onload = setTimeout(() => {
  37. document.querySelectorAll(".apikey-link")[1].click(); //Click on the API Key
  38. setTimeout(() => {
  39. GM_setValue("APIKey", document.querySelector(".apikey-text").innerText); //Store the API Key
  40. alert((GM_getValue("APIKey") !== undefined && GM_getValue("APIKey") !== null && GM_getValue("APIKey") !== '') ? 'API Key automatically added!' : 'Failed to add API Key automatically!');
  41. }, 500);
  42. }, 1000);
  43. }
  44.  
  45. var registeredToggleableCommandIDs = []; //Store the IDs of feature options
  46.  
  47. function AddMenu() {
  48. registeredToggleableCommandIDs = []; //Reset to capture new IDs from this registration pass
  49. ["MouseGestures", "RockerMouseGestures", "SearchHighlight", "TimeConverter", "CurrenciesConverter", "UnitsConverter"].forEach(option => {
  50. registeredToggleableCommandIDs.push( GM_registerMenuCommand(`${GM_getValue(option) ? "🟢 On" : "🔴 Off" } 🠞 ${option.replace(/([A-Z])/g, " $1").trim()}`, () => { GM_setValue(option, !GM_getValue(option)); location.reload(); }) );
  51. });
  52. }
  53.  
  54. if (window.top === window.self) AddMenu();
  55. //Mouse Gestures_________________________________________________________________________________________________________________________________________________________________________________
  56. if (GM_getValue("MouseGestures") === true) //If the MouseGestures is enabled
  57. {
  58. var link;
  59.  
  60. document.querySelectorAll('a').forEach(el => {
  61. el.addEventListener('mouseover', function() {
  62. link = this.href; //Store the hovered link
  63. });
  64.  
  65. el.addEventListener('mouseout', () => {
  66. const previousLink = link; //Save the hovered link
  67. setTimeout(() => {
  68. if (previousLink === link) { //Check if the same link is still hovered
  69. link = 'about:newtab'; //Open a new tab
  70. }
  71. }, 200);
  72. });
  73. });
  74.  
  75. const funcs = { //Store the MouseGestures functions
  76. 'DL': () => { //Detect the Down+Left movement
  77. GM_openInTab(location.href, { incognito: true, });
  78. window.top.close();
  79. },
  80.  
  81. 'L': () => { //Detect the Left movement
  82. window.history.back();
  83. },
  84.  
  85. 'R': () => { //Detect the Right movement
  86. window.history.forward();
  87. },
  88.  
  89. 'D': (e) => { //Detect the Down movement
  90. if (e.shiftKey) {
  91. open(link, '_blank', 'height=' + screen.height + ',width=' + screen.width);
  92. }
  93. else {
  94. GM_openInTab(link, { active: true, insert: true, setParent: true });
  95. }
  96. },
  97.  
  98. 'UD': () => { //Detect the Up+Down movement
  99. location.reload();
  100. },
  101.  
  102. 'DR': (e) => { //Detect the Down+Right movement
  103. top.close();
  104. e.preventDefault();
  105. e.stopPropagation();
  106. },
  107.  
  108. 'DU': () => { //Detect the Down+Up movement
  109. GM_openInTab(link, { active: false, insert: true, setParent: true });
  110. }
  111. };
  112.  
  113. //Math codes to track the mouse movement gestures
  114. var x, y, path;
  115. const TOLERANCE = 3;
  116. const SENSITIVITY = 3;
  117. const s = 1 << ((7 - SENSITIVITY) << 1);
  118. const t1 = Math.tan(0.15708 * TOLERANCE),t2 = 1 / t1;
  119.  
  120. const tracer = (e) => {
  121. var cx = e.clientX, cy = e.clientY, deltaX = cx - x, deltaY = cy - y, distance = deltaX * deltaX + deltaY * deltaY;
  122. if (distance > s) {
  123. var slope = Math.abs(deltaY / deltaX), direction = '';
  124. if (slope > t1) {
  125. direction = deltaY > 0 ? 'D' : 'U';
  126. } else if (slope <= t2) {
  127. direction = deltaX > 0 ? 'R' : 'L';
  128. }
  129. if (path.charAt(path.length - 1) !== direction) {
  130. path += direction;
  131. }
  132. x = cx;
  133. y = cy;
  134. }
  135. };
  136.  
  137. window.addEventListener('mousedown', (e) => {
  138. if (e.which === 3) {
  139. x = e.clientX;
  140. y = e.clientY;
  141. path = "";
  142. window.addEventListener('mousemove', tracer, false); //Detect the mouse position
  143. }
  144. }, false);
  145.  
  146. window.addEventListener('contextmenu', (e) => { //When the right click BTN is released
  147. window.removeEventListener('mousemove', tracer, false); //Track the mouse movements
  148. if (path !== "") {
  149. e.preventDefault();
  150. if (funcs.hasOwnProperty(path)) {
  151. funcs[path](e);
  152. }
  153. }
  154. }, false);
  155. }
  156. //Rocker Mouse Gestures__________________________________________________________________________________________________________________________________________________________________________
  157. if (GM_getValue("RockerMouseGestures") === true) //If the RockerMouseGestures is enabled
  158. {
  159. const mouseState = { 0: false, 2: false }; //0: Left, 2: Right
  160.  
  161. window.addEventListener("mouseup", (e) => {
  162. mouseState[e.button] = false; //Update the state for the released button
  163.  
  164. if (mouseState[0] && !mouseState[2]) { //Left clicked, Right released
  165. history.back();
  166. } else if (mouseState[2] && !mouseState[0]) { //Right clicked, Left released
  167. history.forward();
  168. }
  169. }, false);
  170.  
  171. window.addEventListener("mousedown", (e) => {
  172. mouseState[e.button] = true; //Update the state for the pressed button
  173. }, false);
  174. }
  175. //Search HighLight + Time + Currencies + Units Converters + Search HighLight + AI menus__________________________________________________________________________________________________________
  176. if (GM_getValue("SearchHighlight") === true) //If the SearchHighlight is enabled
  177. {
  178. var SelectedText;
  179. const Links = new RegExp(/\.org|\.ly|\.net|\.co|\.tv|\.me|\.biz|\.club|\.site|\.br|\.gov|\.io|\.ai|\.jp|\.edu|\.au|\.in|\.it|\.ca|\.mx|\.fr|\.tw|\.il|\.uk|\.zoom\.us|\.youtu\.be|\.com|\.us|\.de|\.cn|\.ru|\.es|\.ch|\.nl|\.se|\.no|\.dk|\.fi|\.pl|\.tr|\.xyz|\.za/i);
  180.  
  181. document.body.addEventListener('mouseup', async (e) => { //When the user releases the mouse click after selecting something
  182. HtmlMenu.style.display = 'block'; //Display the container div
  183. SelectedText = getSelection().toString().trim();
  184. shadowRoot.querySelector("#ShowCurrencyORUnits").innerText = ''; //Remove the previous Units/Currency text
  185. shadowRoot.querySelector("#SearchBTN span")?.remove(); //Remove the GreyBar
  186.  
  187. function ShowConversion(UnitORCurrency, Type, Result) {
  188. shadowRoot.querySelector("#SearchBTN").innerHTML = toHTML('<span class="GreyBar">&#x2502; </span>' + shadowRoot.querySelector("#SearchBTN").innerHTML);
  189.  
  190. if (UnitORCurrency === 'Currencies') {
  191. const hasSymbol = SelectedText.match(Currencies)[2].match(CurrencySymbols) !== null;
  192. const currencyFormat = Intl.NumberFormat(navigator.language, { style: 'currency', currency: GM_getValue("YourLocalCurrency") }).format(Result);
  193. shadowRoot.querySelector("#ShowCurrencyORUnits").innerHTML = toHTML(hasSymbol ? (Type + ' 🠂 ' + currencyFormat) : currencyFormat);
  194. }
  195. else
  196. {
  197. shadowRoot.querySelector("#ShowCurrencyORUnits").innerHTML = toHTML(UnitORCurrency === 'Units' ? `${Result} ${Type}` : Result); //Show the converted time results
  198. }
  199.  
  200. setTimeout(() => { //Wait for Units to show up to get the right offsetWidth
  201. const offsetWidth = shadowRoot.querySelector("#ShowCurrencyORUnits").offsetWidth; //Store the current menu size
  202. shadowRoot.querySelector("#ShowCurrencyORUnits").onmouseover = () => { //When the mouse hovers the unit/currency
  203. shadowRoot.querySelector("#ShowCurrencyORUnits").innerHTML = toHTML(`Copy`);
  204. shadowRoot.querySelector("#ShowCurrencyORUnits").style.display = 'inline-flex';
  205. shadowRoot.querySelector("#ShowCurrencyORUnits").style.width = `${offsetWidth}px`; //Maintain the aspect ratio
  206. };
  207. }, 0);
  208.  
  209. const htmlcode = shadowRoot.querySelector("#ShowCurrencyORUnits").innerHTML; //Save the converted unit/currency value
  210. shadowRoot.querySelector("#ShowCurrencyORUnits").onmouseout = () => { //When the mouse leaves the unit/currency
  211. shadowRoot.querySelector("#ShowCurrencyORUnits").style.width = ''; //Return the original aspect ratio
  212. shadowRoot.querySelector("#ShowCurrencyORUnits").style.display = ''; //Return the original aspect ratio
  213. shadowRoot.querySelector("#ShowCurrencyORUnits").innerHTML = toHTML(htmlcode); //Return the previous html
  214. };
  215.  
  216. shadowRoot.querySelector("#ShowCurrencyORUnits").onclick = () => { //When the unit/currency is clicked
  217. UnitORCurrency.match(/Units|Time/) ? GM_setClipboard(`${Result} ${Type}`) : GM_setClipboard(Intl.NumberFormat(navigator.language, { style: 'currency', currency: GM_getValue("YourLocalCurrency") }).format(Result));
  218. };
  219. }
  220.  
  221. function Get(url) { //Get the final converted time/currency value
  222. return new Promise(resolve => GM.xmlHttpRequest({
  223. method: "GET",
  224. url: url,
  225. onload: response => resolve(new DOMParser().parseFromString(response.responseText, 'text/html'))
  226. }));
  227. }
  228. //Time Converter_____________________________________________________________________________________________________________________________________________________________________________
  229. const time = new RegExp(/^[ \t\xA0]*(?=.*?(\d{1,2}:\d{2}(?::\d{2})?\s?(?:[aApP]\.?[mM]\.?)?)|\d{1,2}(?::\d{2}(?::\d{2})?)?\s?(?:[aApP]\.?[mM]\.?)?)(?=.*?(PST|PDT|MST|MDT|CST|CDT|EST|EDT|AST|ADT|NST|NDT|GMT|BST|MET|CET|CEST|EET|EEST|WET|WEST|JST|KST|IST|MSK|UTC|PT))(?:\1[ \t\xA0]*\2|\2[ \t\xA0]*\1)[ \t\xA0]*$/i);
  230.  
  231. if (GM_getValue("TimeConverter") === true && SelectedText.match(time) !== null) //If the TimeConverter is enabled and if the selected text is a time
  232. {
  233. const timeResponse = await Get(`https://www.google.com/search?q=${SelectedText.match(time)[0].replace("WET", "Western European Time")} to local time`);
  234. const ConvertedTime = timeResponse.querySelector(".aCAqKc")?.innerText;
  235. const Secs = SelectedText.match(time)[0].match(/(?:\d{1,2}:\d{2}(:\d{2})\s?[ap]\.?m)/i)?.[1] || '';
  236. ConvertedTime && ShowConversion('Time', '', ConvertedTime.replace(/(\d{1,2}):(\d{2})\s?([pP][mM])/, (_, h, m) => `${(h % 12 + 12) % 24}:${m}`).match(/[\d:]+/g)[0] + Secs); //Convert to 24-hour format
  237. }
  238. //Currencies Converter_______________________________________________________________________________________________________________________________________________________________________
  239. const CurrencySymbols = new RegExp(/AU\$|HK\$|US\$|\$US|R\$|\$|¥|€|Rp|Kč|kr(?!w)|zł|£|฿|₩|лв|₪|円|₱|₽|руб|lei|Fr|krw|RON|TRY|₿|Br|₾|₴|₸|₺/i);
  240. const Currencies = new RegExp(/^[ \t\xA0]*\$?(?=.*?(\d+(?:.*\d+)?))(?=(?:\1[ \t\xA0]*)?(Dólares|dolares|dólares|dollars?|AU\$?D?|BGN|BRL|BCH|BTC|BYN|CAD|CHF|Fr|CNY|CZK|DKK|EUR|EGP|ETH|GBP|GEL|HKD|HUF|IDR|ILS|INR|JPY|LTC|KRW|MXN|NOK|NZD|PHP|PLN|RON|RUB|SEK|SGD|THB|TRY|USD|UAH|ZAR|KZT|YTL|\$|R\$|HK\$|US\$|\$US|¥|€|Rp|Kč|kr|krw|zł|£|฿|₩|лв|₪|円|₱|₽|руб|lei|Kč|₿|Br|₾|₴|₸|₺))(?:\1[ \t\xA0]*\2|\2[ \t\xA0]*\1)[ \t\xA0]*$/i); //https://regex101.com/r/6vTbtv/20 Davidebyzero
  241.  
  242. if (GM_getValue("CurrenciesConverter") === true && SelectedText.match(Currencies) !== null) { //If Currencies Converter is enabled and if the selected text is a currency
  243. if (GM_getValue("YourLocalCurrency") === undefined) {
  244. const UserInput = prompt('Write your local currency.\nThe script will always use your local currency to make exchange-rate conversions.\n\n*Currency input examples:\nBRL\nCAD\nUSD\netc...\n\n*Press OK');
  245. GM_setValue("YourLocalCurrency", UserInput);
  246. }
  247. const currencyMap = { 'AU$': 'AUD', '$': 'USD', 'us$': 'USD', '$us': 'USD', 'r$': 'BRL', 'hk$': 'HKD', '¥': 'JPY', '€': 'EUR', 'rp': 'IDR', 'kč': 'CZK', 'kr': 'NOK', 'zł': 'PLN', '£': 'GBP', '฿': 'THB', '₩': 'KRW', 'лв': 'BGN', '₪': 'ILS', '円': 'JPY', '₱': 'PHP', '₽': 'RUB', 'руб': 'RUB', 'lei': 'RON', 'ron': 'Romanian Leu', 'krw': 'KRW', 'fr': 'CHF', '₿': 'BTC', 'Br': 'BYN', '₾': 'GEL', '₴': 'UAH', '₸': 'KZT', '₺': 'YTL', 'try': 'Turkish Lira' };
  248. if((currencyMap[SelectedText.match(CurrencySymbols)?.[0].toLowerCase()]||SelectedText.match(Currencies)[2]).toUpperCase() === GM_getValue("YourLocalCurrency").toUpperCase()) return; //Disable same unit conversion
  249. const CurrencySymbol = currencyMap[SelectedText.match(CurrencySymbols)?.[0].toUpperCase()] || SelectedText.match(Currencies)[2]; //Store the currency symbol
  250. const currencyResponse = await Get(`https://www.google.com/search?q=${SelectedText.replace(/[.,]/g, '').match(Currencies)[1]} ${CurrencySymbol} in ${GM_getValue("YourLocalCurrency")}`);
  251. const FinalCurrency = parseFloat(currencyResponse.querySelector(".SwHCTb, .pclqee").innerText.split(' ')[0].replaceAll(',', '')); //Store the FinalCurrency+erase all commas
  252. ShowConversion('Currencies', CurrencySymbol, FinalCurrency);
  253. }
  254. //Units Converter____________________________________________________________________________________________________________________________________________________________________________
  255. const Units = new RegExp(/^[ \t\xA0]*(-?\d+(?:[., ]\d+)?)(?:[ \t\xA0]*(in|inch|inches|"|”|″|cm|cms|centimeters?|m|mt|mts|meters?|ft|kg|lbs?|pounds?|kilograms?|ounces?|g|ozs?|fl oz|fl oz \(us\)|fluid ounces?|kphs?|km\/h|kilometers per hours?|mhp|mphs?|meters per hours?|(?:°\s?|º\s?|)(?:degrees?\s+)?(?:celsius|fahrenheit|[CF])|km\/hs?|ml|milliliters?|l|liters?|litres?|gal|gallons?|yards?|yd|Millimeter|millimetre|kilometers?|mi|mm|miles?|ft|fl|feets?|grams?|kilowatts?|kws?|brake horsepower|mechanical horsepower|hps?|bhps?|miles per gallons?|mpgs?|liters per 100 kilometers?|lt?\/100km|liquid quarts?|lqs?|qt|foot-? ?pounds?|ft-?lbs?|lb fts?|newton-? ?meters?|n·?m))?(?:[ \t\xA0]*x[ \t\xA0]*(-?\d+(?:[., ]\d+)?)(?:[ \t\xA0]*(in|inch|inches|"|”|″|cm|cms|centimeters?|m|mt|mts|meters?|ft))?)?[ \t\xA0]*(?:\(\w+\)[ \t\xA0]*)?(?:[ \t\xA0]*\^(\d+\.?\d*))*$/i);
  256.  
  257. if (GM_getValue("UnitsConverter") === true && SelectedText.match(/\^(\d+\.?\d*)/) || (SelectedText.match(Units)?.[1] && SelectedText.match(Units)?.[2] || SelectedText.match(Units)?.[3])) { //If the Units Converter option is enabled+if the selected text is a math power or an unit
  258.  
  259. const selectedUnitType = (SelectedText.match(Units)[2]||SelectedText.match(Units)[4])?.toLowerCase();
  260. const SecondSelectedUnitValue = SelectedText.match(Units)[3]?.replaceAll(',', '.')||0;
  261. const SelectedUnitValue = SelectedText.match(Units)[1].replaceAll(',', '.');
  262. var NewUnit = window.UConv[selectedUnitType]?.unit || selectedUnitType;
  263. const convertValue = (value, unitType) => {
  264. const { factor, convert } = window.UConv[unitType] || {};
  265. return convert ? convert(value) : value * factor;
  266. };
  267.  
  268. var ConvertedUnit = `${convertValue(parseFloat(SelectedUnitValue), selectedUnitType).toFixed(2)}${SecondSelectedUnitValue != 0 ? ` x ${convertValue(parseFloat(SecondSelectedUnitValue), selectedUnitType).toFixed(2)}` : ''}`;
  269. ConvertedUnit = SelectedText.match(/\^(\d+\.?\d*)/) ? (NewUnit = 'power', Math.pow(parseFloat(SelectedUnitValue), parseFloat(SelectedText.match(/\^(\d+\.?\d*)/)[1]))) : ConvertedUnit;
  270. ShowConversion('Units', NewUnit, ConvertedUnit);
  271. }
  272. //Mini Menu__________________________________________________________________________________________________________________________________________________________________________________
  273. if (shadowRoot.querySelector("#SearchBTN").innerText === 'Open') //If the Search BTN text is 'Open'
  274. {
  275. shadowRoot.querySelector("#highlight_menu > ul").style.paddingInlineStart = '19px'; //Increase the menu size
  276. shadowRoot.querySelector("#SearchBTN").innerText = 'Search'; //Display the BTN text as Search again
  277. shadowRoot.querySelectorAll(".AI-BG-box button").forEach(button => { button.style.marginLeft = ''; }); //Remove the margin left
  278. shadowRoot.querySelector("#OpenAfter").remove(); //Remove the custom Open white hover overlay
  279. }
  280.  
  281. if (SelectedText.match(Links) !== null) //If the selected text is a link
  282. {
  283. shadowRoot.querySelector("#highlight_menu > ul").style.paddingInlineStart = '27px'; //Increase the menu size
  284. shadowRoot.querySelector("#SearchBTN").innerText = 'Open'; //Change the BTN text to Open
  285. shadowRoot.querySelectorAll(".AI-BG-box button").forEach(button => { button.style.marginLeft = '-2%'; }); //Add a margin left
  286. shadowRoot.innerHTML += toHTML(`<style id="OpenAfter"> #SearchBTN::after { width: 177% !important; transform: translate(-34%, -71%) !important; } </style> `); //Add a custom Open white hover overlay
  287. }
  288.  
  289. shadowRoot.querySelector("#SearchBTN").onmousedown = (words) => {
  290. GM_openInTab(SelectedText.match(Links) ? SelectedText.replace(/^(?!https?:\/\/)(.+)$/, 'https://$1') : `https://www.google.com/search?q=${SelectedText.replaceAll('&', '%26').replace(/\s+/g, ' ')}`, { active: true, setParent: true, loadInBackground: true }); //Open link or Google+search for the selected text
  291. shadowRoot.querySelector("#highlight_menu").classList.remove('show');
  292. };
  293.  
  294. const menu = shadowRoot.querySelector("#highlight_menu");
  295. if (document.getSelection().toString().trim() !== '' && shadowRoot.querySelector('#CloseOverlay.show') === null) { //If text has been selected and the AI overlay isn't showing
  296. const p = document.getSelection().getRangeAt(0).getBoundingClientRect(); //Store the selected position
  297.  
  298. menu.classList.add('show'); //Show the menu
  299. menu.offsetHeight; //Trigger reflow by forcing a style calculation
  300. menu.style.top = p.top - menu.offsetHeight - 11 + 'px';
  301. menu.style.left = p.left + (p.width / 2) - (menu.offsetWidth / 2) + 'px';
  302. menu.classList.add('highlight_menu_animate');
  303.  
  304. return; //Keep the menu open
  305. }
  306. menu.classList.remove('show'); //Hide the menu after a text is unselected
  307. }); //Finishes the mouseup event listener
  308. //AI Menu______________________________________________________________________________________________________________________________________________________________________________________
  309. var isImagePrompt, OldActive, FinalPrompt, transcript, request, OldRequest, silenceTimer, menuIds = [], retryCount = 0, desiredVoice = null, isRecognizing = false, SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition;
  310. const HtmlMenu=document.body.appendChild(Object.assign(document.createElement('div'),{style:'width:0px;height:0px;display:none;'},{id:'AIContainer'})); //Create+hide a container div
  311. const UniqueLangs = navigator.languages.filter((l, i, arr) => !arr.slice(0, i).some(e => e.split('-')[0].toLowerCase() === l.split('-')[0].toLowerCase()) ); //Filter unique languages
  312. const Lang = UniqueLangs.join('+into '); //Use 1 or more languages
  313. const shadowRoot = HtmlMenu.attachShadow({ mode: 'closed' });
  314. const renderer = new window.marked.Renderer();
  315. const recognition = new SpeechRecognition();
  316. recognition.interimResults = true; //Show partial results
  317. recognition.continuous = true; //Keep listening until stopped
  318.  
  319. shadowRoot.innerHTML = toHTML(GM_getResourceText("AIMenuContent")); //Set the AI menu HTML+CSS
  320. if (document.body.textContent || document.body.innerText) document.body.appendChild(HtmlMenu); //Append menu if body contains text
  321.  
  322. renderer.code = function(code, lang, escaped) { //Override the default code rendering
  323. const tempDiv = Object.assign(document.createElement('div'), { innerHTML: window.marked.Renderer.prototype.code.call(this, code, lang, escaped) }); //Create a div and add the original HTML
  324. hljs.highlightElement(tempDiv.querySelector('code')); //Highlight the code
  325. return `<div class="code-block-wrapper">${shadowRoot.querySelector('#code-block-header-template').innerHTML.replaceAll('{{language}}', tempDiv.innerHTML.match(/class="language-([^"]+)"/)?.[1]?.slice(0,-5) || 'text')}${tempDiv.innerHTML}</div>`; //Clean up language name, combine with highlighted HTML
  326. };
  327. window.marked.setOptions({ renderer: renderer }); //Set our custom renderer
  328.  
  329. function handleState(state) { //Show #AIMenu + #dictate but hide #TopPause for the load/abort states. Do the opposite for the 'start' state.
  330. const isStart = state === 'start';
  331. [["#TopPause", isStart], ["#AIMenu", !isStart], ["#dictate", !isStart]] .forEach(([el, show]) => shadowRoot.querySelector(el).classList.toggle('show', show));
  332. }
  333.  
  334. function SwitchMode() {
  335. void shadowRoot.querySelector(".animated-prompt-box").offsetWidth; //Force reflow
  336. shadowRoot.querySelector(".animated-prompt-box").classList.add("magnify-animation");
  337.  
  338. if (shadowRoot.querySelector("#prompt").placeholder.match('about')) { //If the input bar contains the word "about"
  339. shadowRoot.querySelector("#AddContext").remove(); //Return original prompt input styles
  340. shadowRoot.querySelector("#context").classList.remove('show');
  341. shadowRoot.querySelector("#prompt").placeholder = 'Ask Gemini anything...'; //Return default placeholder
  342. }
  343. else
  344. {
  345. shadowRoot.querySelector("#context").classList.add('show'); //Show the context mode
  346. shadowRoot.querySelector("#prompt").placeholder = `Ask about ${location.host.replace('www.','')}`; //Change placeholder
  347. shadowRoot.querySelector("#highlight_menu").insertAdjacentHTML('beforebegin', `<style id="AddContext"> #gemini { display: none; } #prompt { left: 5.5vw; width: 20.5vw; } .animated-prompt-box { --color-OrangeORLilac: #FF8051; } </style> `); //Show the context bar
  348. }
  349. setTimeout(() => { shadowRoot.querySelector(".animated-prompt-box").classList.remove("magnify-animation"); }, 300);
  350. }
  351.  
  352. function AskAI(Prompt, button) {
  353. OldRequest = [Prompt, button, isImagePrompt];
  354. const IsLatin = !/\p{Script=Latin}/u.test(Prompt) ? `, add 2 headings, "Pronunciation:" and "Language:"` : '';
  355. const responsePrompt = Prompt.includes('?') ? 'Give me a very short, then a long, detailed answer' : 'Help me further understand/learn a term or topic from the text/word';
  356. const ShortTXTPrompt = Prompt.split(' ').length < 5 ? `\nAfter showing (in order) (add a heading as "${Prompt}") ${IsLatin} , a few possible "Synonyms:", "Definition:" and "Example:".` : '';
  357. const context = !!shadowRoot.querySelector("#context.show") ? `"${Prompt}"\n\nMainly base yourself on the text below\n\n${document.body.innerText}` : Prompt; //Add the page context if context is enabled
  358. const taskTXT = button.match('translate') ? `Translate this text: "${Prompt.trim().slice(0, 215)}${Prompt.length > 215 ? '…' : ''}"` : button.match('Prompt') ? `${Prompt.trim().slice(0, 240)}${Prompt.length > 240 ? '…' : ''}` : `Help me further explore a term or topic from the text: "${Prompt.trim().slice(0, 180)}${Prompt.length > 180 ? '…' : ''}"`; //AI Box top text
  359. FinalPrompt = button.match('translate') ? `Translate into ${Lang} the following text:\n\n"${Prompt}"\n${ShortTXTPrompt}${UniqueLangs.length > 1 ? `\n\nYou must answer using only 1 language first, then use only the other language, don't mix both languages!\nAlso, be sure to say which language is the translated text from, if the text isn't into ${Lang}!\n\n"${Prompt}" should be translated for the other languages.\nUse ---  ${UniqueLangs.length-1}x to divide your answer into language sections.` : ''}` : button.match('Prompt') ? context : `${responsePrompt}: "${Prompt}"`;
  360. FinalPrompt = button.match(/Rewrite|Tweak|Spellcheck/) ? `${button} the following text: "${Prompt}"` : FinalPrompt;
  361. FinalPrompt = button.match('Writer') ? Prompt : FinalPrompt;
  362. const data = { contents: [{ parts: [{ text: FinalPrompt }] }], generationConfig: { response_modalities: isImagePrompt ? ['TEXT','IMAGE'] : ['TEXT'] } };
  363.  
  364. shadowRoot.querySelectorAll('.action-buttons, .insert-to-page-btn').forEach(el => {
  365. el.style.display = button.match(/Rewrite|Tweak|Spellcheck|Writer/) ? 'flex' : 'none';
  366. });
  367.  
  368. shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML(isImagePrompt ? '<p>Generating image...</p>' : '');
  369. shadowRoot.querySelectorAll("#CloseOverlay, #AIBox, .animated-prompt-box, .prompt-arrow").forEach(el => el.classList.add('show')); //Show Overlay, input+answer box+order and arrows
  370. shadowRoot.querySelector("#msg-txt").innerText = button.match(/Rewrite|Tweak|Spellcheck/) ? `${button} the following text: "${Prompt}"` : button.match('Writer') ? Prompt : taskTXT;
  371. shadowRoot.querySelector("#msg-txt").innerText = shadowRoot.querySelector("#msg-txt").innerText.length > 240 ? shadowRoot.querySelector("#msg-txt").innerText.slice(0, 240) + '…' : shadowRoot.querySelector("#msg-txt").innerText;
  372.  
  373. if (!isImagePrompt) {
  374. data.systemInstruction = { parts: [{ text: `List of things you aren't allowed to say/do anything like:\n1 "Based on the provided text"\n2 "The text is already in"\n3 "No translation is needed"\n4 Ask for more context\n5 "You haven't provided context"\n6 Use bullet points for Synonyms` }] };
  375. data.safetySettings = ["HARASSMENT", "HATE_SPEECH", "SEXUALLY_EXPLICIT", "DANGEROUS_CONTENT"].map(cat => ({ category: `HARM_CATEGORY_${cat}`, threshold: "BLOCK_NONE" }));
  376. }
  377.  
  378. request = GM.xmlHttpRequest({
  379. method: "POST",
  380. url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-${isImagePrompt ? 'exp-image-generation' : '001'}:${isImagePrompt ? `g` : `streamG`}enerateContent?key=${GM_getValue("APIKey")}`,
  381. responseType: isImagePrompt ? 'json' : 'stream',
  382. headers: { "Content-Type": "application/json" },
  383. data: JSON.stringify(data),
  384. onerror: (err) => {
  385. shadowRoot.querySelector("#msg-txt").innerText = 'Error';
  386. shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML(`<br>Please copy and paste the error below:<br><a class="feedback" href="https://greasyfork.org/scripts/419825/feedback">Click here to report this bug</a><br><br> Prompt: ${Prompt}<br> Button: ${button}<br> Error: <pre>${JSON.stringify(err, null, 2)}</pre><br><br><br>`);
  387. },
  388. onload: (res) => {
  389. handleState('load');
  390. shadowRoot.querySelector("#finalanswer-txt").scrollTop = shadowRoot.querySelector("#finalanswer-txt").scrollHeight; //Perform a smooth final scroll
  391.  
  392. if (isImagePrompt) {
  393. if (res.response.error?.message) shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML(`<p>${res.response.error?.message}</p>`); //Show the AI error message
  394. shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML(`<p>${res.response.candidates?.[0]?.content?.parts?.[0]?.text.replace(/I will (.)/i,(_,c)=>c.toUpperCase()) || 'Generated Image:'}</p><img src="data:image/png;base64,${res.response.candidates?.[0]?.content?.parts.find(p=>p.inlineData).inlineData.data}">`);
  395. shadowRoot.querySelector("#IMGOverlay").querySelector(".overlay-image").src = shadowRoot.querySelector("#finalanswer-txt img").src;
  396.  
  397. shadowRoot.querySelector("#finalanswer-txt img").onclick = function() {
  398. shadowRoot.querySelector("#IMGOverlay").classList.add("show");
  399. };
  400. isImagePrompt = false;
  401. }
  402. },
  403. onabort: (response) => {
  404. handleState('abort');
  405. shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML('<p>Response has been interrupted.<\p>');
  406. },
  407. onloadstart: async function(response, reader = response.response.getReader()) {
  408. handleState('start');
  409. shadowRoot.querySelector("#prompt").focus(); //Focus
  410. await window.parseGeminiStream(reader, (item, markdown) => { //Call the stream parser
  411. if (item.error?.message && retryCount < 2) (AskAI(Prompt, button), retryCount++); //Retry 2x on error
  412. shadowRoot.querySelector("#finalanswer-txt").innerHTML = toHTML(window.marked.parse(markdown || `<p>${item.error?.message}</p>`)); //Render the AI response or error as HTML
  413. shadowRoot.querySelector("#finalanswer-txt").scrollTop = shadowRoot.querySelector("#finalanswer-txt").scrollHeight; //Scroll to the bottom as new content comes in
  414. });
  415. }
  416. });
  417. }
  418.  
  419. if (window.top === window.self) ['📝 Generate Text','🖊️ Tweak it','A✓ Spellcheck'].map((label,i) => GM_registerMenuCommand(label,() => { AskAI(OldActive.value.trim(), ['Rewrite','Tweak','Spellcheck'][i]); AddMenu(); }));
  420. //AI related event listeners___________________________________________________________________________________________________________________________________________________________________
  421. ['keydown','keyup','keypress'].forEach(type => document.addEventListener(type, e => e.target.id == 'AIContainer' && !{Tab:1,Enter:1,Escape:1}[e.key] && e.stopPropagation(), 1)) //Block key events on the prompt box except for Tab/Enter/Escape
  422. speechSynthesis.onvoiceschanged = () => desiredVoice = speechSynthesis.getVoices().find(v => v.name === "Microsoft Zira - English (United States)"); //Find+store the desired voice
  423. shadowRoot.querySelectorAll(".prompt-arrow").forEach(el => el.onclick = () => SwitchMode());
  424. speechSynthesis.onvoiceschanged(); //Handle cases where the event doesn't fire
  425.  
  426. shadowRoot.querySelector("#TopPause").onclick = () => {
  427. request.abort();
  428. };
  429.  
  430. shadowRoot.querySelector('#CopyBTN').onmousedown = () => {
  431. GM_setClipboard(SelectedText);
  432. };
  433.  
  434. shadowRoot.querySelector("#IMGOverlay").onclick = function() {
  435. this.classList.remove("show");
  436. };
  437.  
  438. window.addEventListener('scroll', () => {
  439. shadowRoot.querySelector("#highlight_menu").classList.remove('show');
  440. });
  441.  
  442. shadowRoot.querySelector(".download-button").onclick = () => {
  443. Object.assign(document.createElement('a'), { href: shadowRoot.querySelector(".overlay-image").src, download: 'AI_Img.png' }).click();
  444. };
  445.  
  446. document.addEventListener('contextmenu', e => {
  447. OldActive = document.activeElement;
  448. registeredToggleableCommandIDs.forEach(id => GM_unregisterMenuCommand(id)); //Remove some menu features
  449. });
  450.  
  451. shadowRoot.querySelector(".insert-to-page-btn").onclick = function() {
  452. OldActive.value = shadowRoot.querySelector("#finalanswer-txt").innerText;
  453. shadowRoot.querySelectorAll("#CloseOverlay, #AIBox, .animated-prompt-box, .prompt-arrow").forEach(el => el.classList.remove('show')); //Hide Overlay, input+answer box+order and arrows
  454. };
  455.  
  456. shadowRoot.querySelectorAll('.action-btn').forEach((el, i) => {
  457. el.onclick = () => {
  458. AskAI(`${['Make the following text shorter', 'Make the following text longer', 'Add hashtags to the following text', 'Add emojis to the following text' ][i]}: "${shadowRoot.querySelector("#finalanswer-txt").innerText}"`, 'Writer');
  459. };
  460. });
  461.  
  462. shadowRoot.querySelector("#NewAnswer").onclick = () => {
  463. recognition.stop(); //Stop recognizing audio
  464. speechSynthesis.cancel(); //Stop speaking
  465. isImagePrompt = OldRequest[2]
  466. AskAI(OldRequest[0], OldRequest[1]);
  467. };
  468.  
  469. shadowRoot.querySelector("#copyAnswer").onclick = () => {
  470. (shadowRoot.querySelector("#copyAnswer").style.display = 'none', shadowRoot.querySelector("#AnswerCopied").style.display = 'inline-flex'); //Hide copy+show checkmark BTNs
  471. const finalEl = shadowRoot.querySelector("#finalanswer-txt");
  472. navigator.clipboard.write([ new ClipboardItem({ "text/plain": new Blob([finalEl.innerText], {type:"text/plain"}), "text/html": new Blob([finalEl.innerHTML], {type:"text/html"}) }) ]);
  473. setTimeout(() => { //Return play BTN svg
  474. (shadowRoot.querySelector("#copyAnswer").style.display = 'inline-flex', shadowRoot.querySelector("#AnswerCopied").style.display = 'none'); //Show copy+hide checkmark BTNs
  475. }, 1000);
  476. };
  477.  
  478. shadowRoot.querySelector("#dictate").onclick = () => {
  479. if (isRecognizing) {
  480. recognition.stop();
  481. } else {
  482. isRecognizing = true;
  483. recognition.start();
  484. shadowRoot.querySelectorAll('.state1, .state2, .state3').forEach((state, index) => { //ForEach SVG animation state
  485. state.style.display = 'unset'; //Show all states
  486. state.classList.add('animate'+index); //Start the voice recording animation
  487. });
  488. }
  489. };
  490.  
  491. shadowRoot.querySelector("#CloseOverlay").onclick = () => {
  492. [...shadowRoot.querySelector("#finalanswer-txt").childNodes].slice(0, -1).forEach(node => node.remove()); //Reset the text content
  493. shadowRoot.querySelectorAll("#CloseOverlay, #AIBox, .animated-prompt-box, .prompt-arrow").forEach(el => el.classList.remove('show')); //Hide Overlay, input+answer box+order and arrows
  494. recognition.stop(); //Stop recognizing audio
  495. speechSynthesis.cancel(); //Stop speaking
  496. request.abort(); //Abort any ongoing request
  497. if (shadowRoot.querySelector("#gemini").style.display === 'none') {
  498. shadowRoot.querySelector("#AddContext").remove(); //Return original prompt input styles
  499. shadowRoot.querySelector("#context").classList.remove('show');
  500. shadowRoot.querySelector("#prompt").placeholder = 'Ask Gemini anything...'; //Return default placeholder
  501. }
  502. };
  503.  
  504. shadowRoot.querySelector("#finalanswer-txt").addEventListener('click', (e) => {
  505. const button = e.target.closest('button[data-action]'); //Get the clicked action button
  506.  
  507. if (button.dataset.action === 'copy') {
  508. GM_setClipboard(button.closest('.code-block-wrapper').querySelector('pre > code').innerText); //Copy code to clipboard
  509. button.innerHTML = toHTML(shadowRoot.querySelector('#icon-checkmark-template').innerHTML);
  510. setTimeout(() => ( button.innerHTML = toHTML(shadowRoot.querySelector('#icon-copy-template').innerHTML) ), 1000); //Revert back to copy icon
  511. }
  512.  
  513. if (button.dataset.action === 'download') {
  514. Object.assign(document.createElement('a'), { //Create a temporary anchor element
  515. href: URL.createObjectURL(new Blob([button.closest('.code-block-wrapper').querySelector('pre > code').innerText], {type:'text/plain'})), //Create a blob URL with the code content
  516. download: `AI_Code.${({javascript:'js',html:'html',css:'css',python:'py',shell:'sh',bash:'sh',json:'json',sql:'sql',xml:'xml',typescript:'ts',java:'java',csharp:'cs',cpp:'cpp',c:'c'}[button.dataset.lang?.toLowerCase()]||'txt')}` //Get lang and set filename
  517. }).click(); //Trigger download
  518. }
  519. });
  520.  
  521. shadowRoot.querySelectorAll("#speak, #SpeakingPause").forEach((el) => {
  522. el.onclick = () => { //When the speak or the bottom pause BTNs are clicked
  523. if (speechSynthesis.speaking) {
  524. speechSynthesis.cancel();
  525. (shadowRoot.querySelector("#speak").style.display = 'inline-flex', shadowRoot.querySelector("#SpeakingPause").classList.remove('show')) //Show the play+hide the pause BTNs
  526. }
  527. else
  528. {
  529. (shadowRoot.querySelector("#speak").style.display = 'none', shadowRoot.querySelector("#SpeakingPause").classList.add('show')) //Hide the play+show the pause BTNs
  530.  
  531. const audio = new SpeechSynthesisUtterance(shadowRoot.querySelector("#finalanswer-txt").innerText.replace(/\b[a-z]{2}-[A-Z]{2}\b|[^\p{L}\p{N}\s%.,!?]/gui, '')); //Play the AI response text, removing non-alphanumeric characters+lang locales for better pronunciation
  532. audio.voice = desiredVoice; //Use the desiredVoice
  533. speechSynthesis.speak(audio); //Speak the text
  534.  
  535. audio.onend = (event) => {
  536. (shadowRoot.querySelector("#speak").style.display = 'inline-flex', shadowRoot.querySelector("#SpeakingPause").classList.remove('show')) //Show the play+hide the pause BTNs
  537. };
  538. }
  539. };
  540. });
  541.  
  542. shadowRoot.querySelectorAll("#AIBTN").forEach((button) => {
  543. button.onmousedown = function(event, i) { //When the Explore or the Translate BTNs are clicked
  544. if (GM_getValue("APIKey") === undefined || GM_getValue("APIKey") === null || GM_getValue("APIKey") === '') { //Set up the API Key if it isn't already set
  545. GM_setValue("APIKey", prompt('Enter your API key\n*Press OK\n\nYou can get a free API key at https://aistudio.google.com/app/apikey'));
  546. }
  547. if (GM_getValue("APIKey") !== null && GM_getValue("APIKey") !== '') {
  548. AskAI(SelectedText, this.className);
  549. }
  550. };
  551. });
  552.  
  553. [document, shadowRoot.querySelector("#prompt")].forEach(el => {
  554. el.addEventListener('keydown', (e) => {
  555. if (e.key === "Escape") {
  556. shadowRoot.querySelector("#CloseOverlay").click();
  557. }
  558.  
  559. if (el === document && (e.shiftKey && e.code === 'Digit7' || e.key === '/') && (e.ctrlKey || e.metaKey)) { //Detect Ctrl/Cmd + / or Ctrl/Cmd + Shift + 7
  560. shadowRoot.querySelector("#prompt").value = document.activeElement.value?.trim() || "";
  561. shadowRoot.querySelectorAll("#CloseOverlay, .animated-prompt-box, .prompt-arrow, #dictate").forEach(el => el.classList.add('show')); //Show Overlay, input box+border, arrows and speak BTNs
  562. HtmlMenu.style.display = 'block'; //Display the container div
  563. shadowRoot.querySelector("#prompt").focus(); //Focus
  564. }
  565. if (e.target.id === 'AIContainer') {
  566. if (e.key === "Enter") {
  567. isImagePrompt = /^(generate|create|draw|make) (me|an?|the|this|that)? .* (at|in|on|of|for|about|showing|depicting|illustrating)/i.test(shadowRoot.querySelector("#prompt").value);
  568. AskAI(shadowRoot.querySelector("#prompt").value, shadowRoot.querySelector("#prompt").className);
  569. shadowRoot.querySelector("#prompt").value = ''; //Erase the prompt text
  570. }
  571. if (e.key === "Tab") {
  572. e.preventDefault(); //Block focus on browser tabs + the read BTN
  573. SwitchMode();
  574. }
  575. shadowRoot.querySelector("#prompt").focus(); //Focus
  576. }
  577. });
  578. });
  579.  
  580. recognition.onend = () => {
  581. clearTimeout(silenceTimer); //Clear any pending timeout
  582. isRecognizing = false;
  583.  
  584. shadowRoot.querySelectorAll('.state1, .state2, .state3').forEach((state, index) => { //ForEach SVG animation state
  585. index.toString().match(/1|2/) && (state.style.display = 'none'); //Show only the 1 state
  586. state.classList.remove('animate'+index); //Stop the voice recording animation
  587. });
  588. transcript ? AskAI(transcript, shadowRoot.querySelector("#prompt").className) : ["#finalanswer-txt","#msg-txt"].forEach(el => shadowRoot.querySelector(el).innerHTML = toHTML(`<p>No audio detected. Please try again or check your mic settings.<\p>`));
  589. transcript = ''; //Reset transcript
  590. }; //Finish the recognition end event listener
  591.  
  592. recognition.onresult = (words) => {
  593. clearTimeout(silenceTimer); //Reset the silence timer on new input
  594. silenceTimer = setTimeout(() => isRecognizing && recognition.stop(), 5000); //Stop after 5 seconds of silence
  595. transcript = [...words.results].map(result => result[0].transcript).join(' '); //Combine speech recognition segments into a single transcript
  596. shadowRoot.querySelector("#msg-txt").innerText = transcript.slice(0, 240) + (transcript.length > 240 ? '…' : '');
  597. };
  598. }