// ==UserScript==
// @name BraveGPT 🤖
// @description Adds ChatGPT answers to Brave Search sidebar (powered by GPT-4o!)
// @description:af Voeg ChatGPT-antwoorde by Brave Search-kantbalk by (aangedryf deur GPT-4o!)
// @description:am የChatGPT መልስናወርቃለች እርስዎን በBrave Search የተወሰኑ ገጽታዎችን (ተግባር በGPT-4o!) ይጨምሩ
// @description:ar يضيف إجابات ChatGPT إلى شريط البحث الجانبي في Brave (مدعوم بواسطة GPT-4o!)
// @description:az ChatGPT cavablarını Brave Axtarış yan panelinə əlavə edir (GPT-4o ilə gücləndirilmiş!)
// @description:be Дадае адказы ChatGPT да бакавой баковай панэлі Brave Search (падтрымліваецца GPT-4o!)
// @description:bem Aziya ChatGPT ndalama ku Brave Search sidebar (muma GPT-4o!)
// @description:bg Добавя ChatGPT отговори към страничната лента на Brave Search (задвижван от GPT-4o!)
// @description:bn Brave সার্চ সাইডবারে ChatGPT উত্তর যোগ করে (পাওয়ারডে GPT-4o দ্বারা!)
// @description:bo ChatGPT ལེ་བས་ཚད་བདག་སྐྱེད་དེ་བཟུམ་སྒྲིག་ནང་ Brave Search གནས་པ་བརྗོད་པ། (GPT-4oབྱ་བ་བརྒྱུད་པ་!)
// @description:bs Dodaje odgovore ChatGPT-a na bočnu traku Brave pretrage (pokreće GPT-4o!)
// @description:ca Afegeix respostes de ChatGPT a la barra lateral de Brave Search (amb tecnologia GPT-4o!)
// @description:ceb Nagdugang sa mga tubag sa ChatGPT sa sidebar sa Brave Search (gamit ang GPT-4o!)
// @description:ckb وەرگرتنی ڕاستەوخۆیی ChatGPT بۆ پەنجەرەی لاتی لە Brave (بە پشتگیرییی GPT-4o!)
// @description:cs Přidává odpovědi od ChatGPT do bočního panelu Brave Search (poháněno GPT-4o!)
// @description:cy Ychwanega Atebion ChatGPT i'r bar ochr Brave Search (a gryfhawyd gan GPT-4o!)
// @description:da Tilføjer ChatGPT-svar til Brave Search-sidelinjen (drevet af GPT-4o!)
// @description:de Fügt ChatGPT-Antworten zur Seitenleiste der Brave-Suche hinzu (betrieben mit GPT-4o!)
// @description:dv ChatGPT އައިކްސޭޓުގެ ޖަވާބުގެ Brave Search ސައިޓުގައި ފޯރުވާރައުގެ ޑައުން (އެކައުންއައި ވަކި GPT-4o!)
// @description:dz ChatGPT དང་ Brave འབྱུང་ཆུང་ལེ་བས་འཐུས་པ་ལགས་སྤྱོད་སྒྲིག་པ་བརྟགས་བཞུགས། (GPT-4o་གི་སྒྲིག་དང་!)
// @description:el Προσθέτει απαντήσεις ChatGPT στην πλαϊνή γραμμή αναζήτησης του Brave (με την υποστήριξη του GPT-4o!)
// @description:eo Aldonas ChatGPT-respondojn al la flanka breto de Brave Serĉo (funkciigita de GPT-4o!)
// @description:es Agrega respuestas de ChatGPT a la barra lateral de Brave Search (¡impulsado por GPT-4o!)
// @description:et Lisab ChatGPT vastused Brave Search küljepaneelile (toetatud GPT-4o poolt!)
// @description:eu Gehitu ChatGPT erantzunak Brave Search aldeko alderakoan (GPT-4oren aurrerapenean oinarrituta!)
// @description:fa ChatGPT پاسخها را به نوار کناری جستجوی Brave اضافه میکند (قدرت گرفته شده توسط GPT-4o!)
// @description:fi Lisää ChatGPT-vastaukset Brave-haun sivupalkkiin (käyttäen GPT-4o!)
// @description:fo Leggur ChatGPT-svar til Brave leitarstika síðupall (drivin av GPT-4o!)
// @description:fr Ajoute les réponses de ChatGPT à la barre latérale de Brave Search (propulsé par GPT-4o !)
// @description:fr-CA Ajoute les réponses de ChatGPT à la barre latérale de Brave Search (propulsé par GPT-4o !)
// @description:gd Cur Freagairtean ChatGPT ris an t-siostam-cùlaiche airson Innse Brave (le GPT-4o air!)
// @description:gl Engade respostas de ChatGPT á barra lateral de busca de Brave (potenciado por GPT-4o!)
// @description:gu બ્રેવ શોધનપટનમાં ChatGPT જવાબો ઉમેરે છે (પાવર્ડપુંજ GPT-4o દ્વારા!)
// @description:haw Hoʻoni i nā manaʻoʻiʻo ChatGPT i loko o ka papa kālai Brave Search (i hoʻohui ʻia e GPT-4o!)
// @description:he מוסיף תשובות של ChatGPT לסרגל הצד של חיפוש Brave (מופעל על ידי GPT-4o!)
// @description:hi ब्रेव सर्च साइडबार में ChatGPT उत्तर जोड़ता है (GPT-4o द्वारा संचालित!)
// @description:hr Dodaje odgovore ChatGPT-a na bočnu traku Brave pretraživanja (pokreće GPT-4o!)
// @description:ht Ajoute ChatGPT repons nan sibbard Brave Search la (pouvwa pa GPT-4o!)
// @description:hu Hozzáadja a ChatGPT válaszokat a Brave Search oldalsávjához (GPT-4o által támogatva!)
// @description:hy Ավելացնում է ChatGPT պատասխանները Brave Search կողմից (ուղղահայացված է GPT-4o-ի միջոցով!)
// @description:id Menambahkan jawaban ChatGPT ke sidebar Brave Search (didukung oleh GPT-4o!)
// @description:is Bætir við ChatGPT svari í hliðarstiku Brave leitarinnar (rekinn með GPT-4o!)
// @description:it Aggiunge le risposte di ChatGPT alla barra laterale di Brave Search (alimentato da GPT-4o!)
// @description:ja Brave 検索サイドバーに ChatGPT の回答を追加します(GPT-4o で動作中!)
// @description:jv Nambahake jawaban ChatGPT menyang sidebar Pencarian Brave (didheteni GPT-4o!)
// @description:ka დაამატებს ChatGPT პასუხებს Brave Search გვერდის მარჯვნივ (GPT-4o-ით გამოყენებული!)
// @description:kab Izgan-d yemdanen n teblaḍ n Brave Search (ɣef GPT-4o!)
// @description:kk Brave іздеу жағдайындағы ChatGPT жауаптарын қосады (GPT-4o-мен жұмыс істейді!)
// @description:km បន្ថែមចម្លើយ ChatGPT ទៅរបារចំហៀងស្វែងរកក្លាហាន (ដំណើរការដោយ GPT-4o!)
// @description:kn ಬ್ರೇವ್ ಶೋಧನೆ ಪಟ್ಟಿಗೆ ChatGPT ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (GPT-4o ಅನ್ನು ಬಳಸಿ!)
// @description:ko ChatGPT 답변을 Brave 검색 사이드바에 추가합니다 (GPT-4o 로 구동됨!)
// @description:ku Li ser panoya li serê lêgerîna Brave dihêle ChatGPT bersivan (bi alîkariya GPT-4o!)
// @description:ky ChatGPT каардарларын Браве Издөө экибинын көнчыгышына кошотот (GPT-4o менен күтөлгөн!)
// @description:la Adiungit responsiones ChatGPT ad barra lateralem investigatiois Brave (GPT-4o motore!)
// @description:lb Füügt ChatGPT Äntwerten zur Brave-Sichbarsäit bäi (gedriwwen vun GPT-4o!)
// @description:lo ຕອບສອນ ChatGPT ໄປຫາແຖບຂັ້ນຕອນຂອງການຄົ້ນຫາ Brave (ໂດຍໃຊ້ GPT-4o!)
// @description:lt Prideda ChatGPT atsakymus į šoninę juostą paieškai Brave (varomi GPT-4o!)
// @description:lv Pievieno ChatGPT atbildes Brave meklēšanas sānjoslai (darbināts ar GPT-4o!)
// @description:mg Mampiditra valiny avy amin'ny ChatGPT ao amin'ny saroka Search Brave (maneho ny GPT-4o!)
// @description:mi Tāpiri atu i ngā whakautu a ChatGPT ki te tara taha o te Rapu Wawata Brave (e whakahaerehia ana e GPT-4o!)
// @description:mk Додава одговори од ChatGPT на страничната лента на Brave Search (поддржано од GPT-4o!)
// @description:ml Brave സേർച്ചിന്റെ സൈഡ്ബാർലേക്ക് ChatGPT ഉത്തരങ്ങൾ ചേർക്കുന്നു (GPT-4o-യിൽ പ്രവർത്തിക്കുന്നു!)
// @description:mn ChatGPT хариултуудыг Brave Хайлтын зааврын зурагт нэмнэ (GPT-4o ашиглаж!)
// @description:ms Menambah jawapan ChatGPT ke sidebar Carian Brave (dikuasakan oleh GPT-4o!)
// @description:mt Iżżid Risposti ta 'ChatGPT lill-Sidebar Brave Search (pwered by GPT-4o!)
// @description:my ChatGPT အဖြေမှာ Brave ရွေးချယ်ရေးတွင် ChatGPT အဖြေများကိုထည့်သွင်းထားသည် (GPT-4o ဖြင့်အလုပ်လုပ်ပါ!)
// @description:ne ChatGPT उत्तरहरूलाई ब्रेभ खोजको साइडबारमा थप्छ (GPT-4o द्वारा संचालित!)
// @description:nl Voegt ChatGPT-antwoorden toe aan de zijbalk van Brave Search (aangedreven door GPT-4o!)
// @description:no Legger til ChatGPT-svar i sidenotatfeltet for Brave Search (drevet av GPT-4o!)
// @description:ny Anayambitsa zambiri za ChatGPT kubanja lovala la Brave Search (liyenera ndi GPT-4o!)
// @description:pa ਬਰੇਵ ਖੋਜ ਦੇ ਸਾਈਡਬਾਰ ਵਿੱਚ ChatGPT ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ (GPT-4o ਦੁਆਰਾ ਸ਼ਕਤੀਸ਼ਾਲਕ ਕੀਤਾ ਗਿਆ!)
// @description:pap Añadi respuèstanan di ChatGPT na bar lateral di Buskeda Brave (poderá pa GPT-4o!)
// @description:pl Dodaje odpowiedzi ChatGPT do paska bocznego Brave Search (napędzane przez GPT-4o!)
// @description:ps په سپینې د Brave لټونکې توګه ChatGPT جوابونه ورکړي (د GPT-4o لخوا سره!)
// @description:pt Adiciona respostas do ChatGPT à barra lateral de pesquisa do Brave (alimentado por GPT-4o!)
// @description:pt-BR Adiciona respostas do ChatGPT à barra lateral de pesquisa do Brave (alimentado por GPT-4o!)
// @description:rn Abugira inkomoko za ChatGPT mu gisubizo cya Brave Search (yahindutse na GPT-4o!)
// @description:ro Adaugă răspunsuri ChatGPT în bara laterală de căutare Brave (propulsat de GPT-4o!)
// @description:ru Добавляет ответы ChatGPT в боковую панель поиска Brave (работает на GPT-4o!)
// @description:rw Atera amakuru ya ChatGPT mu bariro bya Brave Search (yarahindutse n'ikoranabuhanga cya GPT-4o!)
// @description:sg Yãngã añ fããmi ChatGPT pũngu loo yaaka yi Brave tãngbala (saango wã yi GPT-4o!)
// @description:si ඔබේ Brave සෙවුම් පිළිතුරුද ChatGPT එක් කිරීමෙන් එක්වන්න (GPT-4o විද්යාවලියේ සිදු වේ!)
// @description:sk Pridáva odpovede ChatGPT do bočnej lišty Brave Search (poháňané GPT-4o!)
// @description:sl Dodaja odgovore ChatGPT na stransko vrstico iskanja Brave (poganja GPT-4o!)
// @description:sm Fa'afaigofie atu ai le tali a ChatGPT i le sidebar o le Search Brave (e avea i le GPT-4o!)
// @description:sn Anoratidza zita reChatGPT pa sidebar yeBrave Search (ine vedzere GPT-4o!)
// @description:so Wax ka dar ChatGPT jawaabaha sidebar Brave Search (u shaqeeya GPT-4o!)
// @description:sr Додаје одговоре ChatGPT-а на страни панел претраге Брејва (покреће GPT-4o!)
// @description:sv Lägger till ChatGPT-svar i sidofältet för Brave Sökning (drivet av GPT-4o!)
// @description:sw Inaongeza majibu ya ChatGPT kwenye upau wa pembeni wa Brave Search (inaendeshwa na GPT-4o!)
// @description:ta பேராசை ஆய்வுக் கேட்பதற்கு ChatGPT பதில்களை Brave பக்க பக்கில் சேர்க்கிறது (GPT-4o ஆல் இயங்குகிறது!)
// @description:te Brave శోధనపై ChatGPT సమాధానాలను జోడిస్తుంది (GPT-4o ద్వారా పెంచబడింది!)
// @description:tg Шарҳи ChatGPT-ро ба сатҳи барои ҷустуҷӯи Брейв илова мекунад (тавассути GPT-4o мубодиъ будааст!)
// @description:th เพิ่มคำตอบของ ChatGPT ไปยังแถบข้างของการค้นหา Brave (ทำงานโดย GPT-4o!)
// @description:ti ክልል ጸጋን ChatGPT መልእኽት ንምስርሓብ Brave ውልቀት ይጨምሩ (በቀረበ GPT-4o!)
// @description:tk ChatGPT-nyň jogaplaryny Brave Gözleg çarpyşysyna goşýar (GPT-4o-dan güýçlenýär!)
// @description:tn Enza mavoti ChatGPT ku sidebar yeBrave Search (ine yeduva neGPT-4o!)
// @description:to Tānaki e fiemaʻu ai ha ngaahi ngaue fakamatala 'a e ChatGPT ki he tu'unga ni ha Brave Search (na'e neongo'i moe GPT-4o!)
// @description:tpi Adim na pes bilong ChatGPT long tab bilong Brave Søk (wok long GPT-4o!)
// @description:tr ChatGPT yanıtlarını Brave Arama yan çubuğuna ekler (GPT-4o tarafından desteklenir!)
// @description:uk Додає відповіді ChatGPT до бічної панелі пошуку Brave (працює на GPT-4o!)
// @description:ur ChatGPT جوابات کو بریو سرچ کے سائڈبار میں شامل کرتا ہے (GPT-4o کے ذریعے!)
// @description:uz ChatGPT javoblarni Brave Qidiruvning yon paneliga qo'shadi (GPT-4o tomonidan ta'minlanadi!)
// @description:vi Thêm câu trả lời của ChatGPT vào thanh bên của Brave Search (được cung cấp bởi GPT-4o!)
// @description:xh Enza amaxwebhu kaChatGPT e sideba yeBrave Search (enokukhuthaziswa nguGPT-4o!)
// @description:yi צוגעבן אַנטוואָרטן פֿון ChatGPT אין די זײַטל-פֿעלד פֿון Brave זוך (געפּאַווערטעד דורך GPT-4o!)
// @description:zh 将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4o 提供支持!)
// @description:zh-CN 将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4o 提供支持!)
// @description:zh-HK 將 ChatGPT 答案添加到 Brave Search 側邊欄 (由 GPT-4o 提供支持!)
// @description:zh-SG 将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4o 提供支持!)
// @description:zh-TW 將 ChatGPT 答案添加到 Brave Search 側邊欄 (由 GPT-4o 提供支持!)
// @description:zu Engeza amaswazi aseChatGPT emugqa wokuqala weBrave Search (ibhulohwe nguGPT-4o!)
// @author KudoAI
// @namespace https://kudoai.com
// @version 2024.5.31.10
// @license MIT
// @icon https://media.bravegpt.com/images/icons/bravegpt/icon48.png?0a9e287
// @icon64 https://media.bravegpt.com/images/icons/bravegpt/icon64.png?0a9e287
// @compatible chrome
// @compatible firefox
// @compatible edge
// @compatible opera
// @compatible brave
// @compatible vivaldi
// @compatible waterfox
// @compatible librewolf
// @compatible ghost
// @compatible qq
// @compatible whale
// @compatible kiwi
// @match *://search.brave.com/search*
// @include https://auth0.openai.com
// @connect binjie.fun
// @connect chatgpt.com
// @connect gptforlove.com
// @connect greasyfork.org
// @connect jsdelivr.net
// @connect onrender.com
// @connect openai.com
// @connect sogou.com
// @require https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@2.9.3/dist/chatgpt.min.js#sha256-EDN+mCc+0Y4YVzJEoNikd4/rAIaJDLAdb+erWvupXTM=
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require https://cdn.jsdelivr.net/npm/generate-ip@2.4.2/dist/generate-ip.min.js#sha256-PRvQIDVWK/a+aAqEFVQv7RePbRe/tX6tWQVM80rAe2M=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_cookie
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM.xmlHttpRequest
// @homepageURL https://www.bravegpt.com
// @supportURL https://support.bravegpt.com
// ==/UserScript==
// Documentation: https://docs.autoclearchatgpt.com
// NOTE: This script relies on the powerful chatgpt.js library @ https://chatgpt.js.org © 2023–2024 KudoAI & contributors under the MIT license.
setTimeout(async () => {
// Define SCRIPT functions
function loadSetting(...keys) { keys.forEach(key => config[key] = GM_getValue(config.keyPrefix + '_' + key, false)) }
function saveSetting(key, value) { GM_setValue(config.keyPrefix + '_' + key, value) ; config[key] = value }
function safeWindowOpen(url) { window.open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
function getUserscriptManager() { try { return GM_info.scriptHandler } catch (err) { return 'other' }}
// Define MENU functions
function registerMenu() {
// Add command to toggle proxy API mode
const pamLabel = state.symbol[+!config.proxyAPIenabled] + ' '
+ ( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' '
+ state.separator + state.word[+!config.proxyAPIenabled]
menuIDs.push(GM_registerMenuCommand(pamLabel, () => {
saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
notify(( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' ' + state.word[+!config.proxyAPIenabled])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
location.reload() // re-send query using new endpoint
}))
// Add command to toggle auto-get mode
const agmLabel = state.symbol[+config.autoGetDisabled] + ' '
+ ( msgs.menuLabel_autoGetAnswers || 'Auto-Get Answers' ) + ' '
+ state.separator + state.word[+config.autoGetDisabled]
menuIDs.push(GM_registerMenuCommand(agmLabel, () => {
saveSetting('autoGetDisabled', !config.autoGetDisabled)
notify(( msgs.menuLabel_autoGetAnswers || 'Auto-Get Answers' ) + ' ' + state.word[+config.autoGetDisabled])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
}))
// Add command to toggle showing related queries
const rqLabel = state.symbol[+config.rqDisabled] + ' '
+ ( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' '
+ state.separator + state.word[+config.rqDisabled]
menuIDs.push(GM_registerMenuCommand(rqLabel, () => {
saveSetting('rqDisabled', !config.rqDisabled)
try { // to update visibility based on latest setting
const relatedQueriesDiv = document.querySelector('.related-queries')
relatedQueriesDiv.style.display = config.rqDisabled ? 'none' : 'flex'
} catch (err) {}
updateTweaksStyle() // toggle <pre> max-height
notify(( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' '
+ state.word[+config.rqDisabled])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
}))
// Add command to toggle prefix mode
const pmLabel = state.symbol[+!config.prefixEnabled] + ' '
+ ( msgs.menuLabel_require || 'Require' ) + ' "/" '
+ ( msgs.menuLabel_beforeQuery || 'before query' ) + ' '
+ state.separator + state.word[+!config.prefixEnabled]
menuIDs.push(GM_registerMenuCommand(pmLabel, () => {
saveSetting('prefixEnabled', !config.prefixEnabled)
if (config.prefixEnabled && config.suffixEnabled) { // disable Suffix Mode if activating Prefix Mode
saveSetting('suffixEnabled', !config.suffixEnabled) }
notify(( msgs.mode_prefix || 'Prefix Mode' ) + ' ' + state.word[+!config.prefixEnabled])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
}))
// Add command to toggle suffix mode
const smLabel = state.symbol[+!config.suffixEnabled] + ' '
+ ( msgs.menuLabel_require || 'Require' ) + ' "?" '
+ ( msgs.menuLabel_afterQuery || 'after query' ) + ' '
+ state.separator + state.word[+!config.suffixEnabled]
menuIDs.push(GM_registerMenuCommand(smLabel, () => {
saveSetting('suffixEnabled', !config.suffixEnabled)
if (config.prefixEnabled && config.suffixEnabled) { // disable Prefix Mode if activating Suffix Mode
saveSetting('prefixEnabled', !config.prefixEnabled) }
notify(( msgs.mode_suffix || 'Suffix Mode' ) + ' ' + state.word[+!config.suffixEnabled])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
}))
if (!isMobile) {
// Add command to toggle wider sidebar
const wsbLabel = state.symbol[+!config.widerSidebar] + ' '
+ ( msgs.menuLabel_widerSidebar || 'Wider Sidebar' )
+ state.separator + state.word[+!config.widerSidebar]
menuIDs.push(GM_registerMenuCommand(wsbLabel, () => toggleSidebar('wider')))
}
// Add command to set reply language
const rlLabel = '🌐 ' + ( msgs.menuLabel_replyLanguage || 'Reply Language' )
+ state.separator + config.replyLanguage
menuIDs.push(GM_registerMenuCommand(rlLabel, () => {
while (true) {
let replyLanguage = prompt(
( msgs.prompt_updateReplyLang || 'Update reply language' ) + ':', config.replyLanguage)
if (replyLanguage == null) break // user cancelled so do nothing
else if (!/\d/.test(replyLanguage)) {
replyLanguage = ( // auto-case for menu/alert aesthetics
[2, 3].includes(replyLanguage.length) || replyLanguage.includes('-') ? replyLanguage.toUpperCase()
: replyLanguage.charAt(0).toUpperCase() + replyLanguage.slice(1).toLowerCase() )
saveSetting('replyLanguage', replyLanguage || config.userLanguage)
alert(( msgs.alert_langUpdated || 'Language updated' ) + '!', // title
`${ config.appName } ${ msgs.alert_willReplyIn || 'will reply in' } `
+ ( replyLanguage || msgs.alert_yourSysLang || 'your system language' ) + '.')
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
break
}}}))
// Add command to launch About modal
const aboutLabel = `💡 ${ msgs.menuLabel_about || 'About' } ${ config.appName }`
menuIDs.push(GM_registerMenuCommand(aboutLabel, launchAboutModal))
}
function launchAboutModal() {
// Show modal
const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
const aboutAlertID = alert(
config.appName, // title
'🏷️ ' + ( msgs.about_version || 'Version' ) + ': ' + GM_info.script.version + '\n'
+ '⚡ ' + ( msgs.about_poweredBy || 'Powered by' ) + ': '
+ '<a href="https://chatgpt.js.org" target="_blank" rel="noopener">chatgpt.js</a>'
+ ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
+ '📜 ' + ( msgs.about_sourceCode || 'Source code' ) + ':\n '
+ `<a href="${ config.gitHubURL }" target="_blank" rel="nopener">`
+ config.gitHubURL + '</a>',
[ // buttons
function checkForUpdates() { updateCheck() },
function getSupport() { safeWindowOpen(config.supportURL) },
function leaveAReview() {
const reviewAlertID = chatgpt.alert(( msgs.alert_choosePlatform || 'Choose a platform' ) + ':', '',
[ function greasyFork() { safeWindowOpen(
config.greasyForkURL + '/feedback#post-discussion') },
function productHunt() { safeWindowOpen(
'https://www.producthunt.com/products/bravegpt/reviews/new') },
function futurepedia() { safeWindowOpen(
'https://www.futurepedia.io/tool/bravegpt#bravegpt-review') },
function alternativeTo() { safeWindowOpen(
'https://alternativeto.net/software/bravegpt/about/') }],
'', 571) // Review modal width
const reviewBtns = document.getElementById(reviewAlertID).querySelectorAll('button')
reviewBtns[0].style.display = 'none' // hide dismiss button
reviewBtns[1].textContent = ( // remove spaces from AlternativeTo label
reviewBtns[1].textContent.replace(/\s/g, '')) },
function moreChatGPTapps() { safeWindowOpen('https://github.com/adamlui/chatgpt-apps') }
], '', 577) // About modal width
// Re-format buttons to include emoji + localized label + hide Dismiss button
for (const button of document.getElementById(aboutAlertID).querySelectorAll('button')) {
if (/updates/i.test(button.textContent)) button.textContent = (
'🚀 ' + ( msgs.buttonLabel_updateCheck || 'Check for Updates' ))
else if (/support/i.test(button.textContent)) button.textContent = (
'🧠 ' + ( msgs.buttonLabel_getSupport || 'Get Support' ))
else if (/review/i.test(button.textContent)) button.textContent = (
'⭐ ' + ( msgs.buttonLabel_leaveReview || 'Leave a Review' ))
else if (/apps/i.test(button.textContent)) button.textContent = (
'🤖 ' + ( msgs.buttonLabel_moreApps || 'More ChatGPT Apps' ))
else button.style.display = 'none' // hide Dismiss button
}
}
function updateCheck() {
// Fetch latest meta
const currentVer = GM_info.script.version
GM.xmlHttpRequest({
method: 'GET', url: config.updateURL + '?t=' + Date.now(),
headers: { 'Cache-Control': 'no-cache' },
onload: response => { const updateAlertWidth = 489
// Compare versions
const latestVer = /@version +(.*)/.exec(response.responseText)[1]
for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
latestSubVer = parseInt(latestVer.split('.')[i], 10) || 0
if (currentSubVer > latestSubVer) break // out of comparison since not outdated
else if (latestSubVer > currentSubVer) { // if outdated
// Alert to update
const updateAlertID = alert(( msgs.alert_updateAvail || 'Update available' ) + '! 🚀', // title
`${ msgs.alert_newerVer || 'An update to' } ${ config.appName } `
+ `(v${ latestVer }) ${ msgs.alert_isAvail || 'is available' }! `
+ '<a target="_blank" rel="noopener" style="font-size: 0.93rem" '
+ 'href="' + config.gitHubURL + '/commits/main/greasemonkey/'
+ config.updateURL.replace(/.*\/(.*)meta\.js/, '$1user.js') + '" '
+ `>${ msgs.link_viewChanges || 'View changes' }</a>`,
function update() { // button
GM_openInTab(config.updateURL.replace('meta.js', 'user.js') + '?t=' + Date.now(),
{ active: true, insert: true } // focus, make adjacent
).onclose = () => location.reload() },
'', updateAlertWidth
)
// Localize button labels if needed
if (!config.userLanguage.startsWith('en')) {
const updateAlert = document.querySelector(`[id="${ updateAlertID }"]`),
updateBtns = updateAlert.querySelectorAll('button')
updateBtns[1].textContent = msgs.buttonLabel_update || 'Update'
updateBtns[0].textContent = msgs.buttonLabel_dismiss || 'Dismiss'
}
return
}}
// Alert to no update found, nav back
alert(( msgs.alert_upToDate || 'Up-to-date' ) + '!', // title
`${ config.appName } (v${ currentVer }) ${ msgs.alert_isUpToDate || 'is up-to-date' }!`, // msg
'', '', updateAlertWidth)
launchAboutModal()
}})}
// Define FEEDBACK functions
function notify(msg, position = '', notifDuration = '', shadow = '') {
chatgpt.notify(`${ config.appSymbol } ${ msg }`, position, notifDuration,
shadow || scheme == 'dark' ? '' : 'shadow' )
}
function alert(title = '', msg = '', btns = '', checkbox = '', width = '') {
return chatgpt.alert(`${ config.appSymbol } ${ title }`, msg, btns, checkbox, width)}
function appAlert(msg) {
msg = appAlerts[msg] || msg
if (msg.includes('login')) deleteOpenAIcookies()
while (appDiv.firstChild) { appDiv.removeChild(appDiv.firstChild) }
const alertP = document.createElement('p') ; alertP.textContent = msg
alertP.className = 'no-user-select' ; alertP.style.marginBottom = '-15px'
if (/waiting|loading/i.test(msg)) alertP.classList.add('loading')
if (msg.includes('@')) { // needs login link, add it
alertP.append(createAnchor('https://chatgpt.com', 'chatgpt.com'),
' (', msgs.alert_ifIssuePersists || 'If issue persists, try activating Proxy Mode', ')')
}
appDiv.append(alertP)
}
function appInfo(msg) { console.info(`${ config.appSymbol } ${ config.appName } >> ${ msg }`) }
function appError(msg) { console.error(`${ config.appSymbol } ${ config.appName } >> ERROR: ${ msg }`) }
// Define UI functions
function isDarkMode() {
return document.documentElement.classList.contains('dark') ? true
: document.documentElement.classList.contains('light') ? false
: window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
}
function toggleSidebar(mode) {
saveSetting(mode + 'Sidebar', !config[mode + 'Sidebar'])
updateTweaksStyle()
if (mode == 'wider' && document.querySelector('.corner-btn')) updateWSBsvg()
notify(( msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' )
+ ' ' + state.word[+!config[mode + 'Sidebar']])
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu() // refresh menu
}
function updateAppLogoSrc() {
appLogoImg.onerror = () => appLogoImg.style.display = 'none'
appLogoImg.src = 'data:image/png;base64,'
+ ( scheme == 'light' ? 'iVBORw0KGgoAAAANSUhEUgAAAPoAAAA1CAYAAABoUvZcAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMGGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDQtMTJUMDQ6MDE6MTctMDc6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDQtMTJUMDQ6MDE6MTctMDc6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZTgxNTM4MTQtOWQ0NC1hZDQ1LWEyYzYtMDY3YjIxMjlkMDc1IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDUwZDFiZTItMzY3NC1kNTRiLWJkNDQtZGE2ZGE0MjE2ZjFkIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjdiY2U5OTUtOTdmYS0wODQ4LTg4MjktNmRmMWEwY2MwMDg1IiB0aWZmOk9yaWVudGF0aW9uPSIxIiB0aWZmOlhSZXNvbHV0aW9uPSI3MjAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjcyMDAwMC8xMDAwMCIgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIgZXhpZjpDb2xvclNwYWNlPSIxIiBleGlmOlBpeGVsWERpbWVuc2lvbj0iNzMwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTU1Ij4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSInZ3B0JyIgcGhvdG9zaG9wOkxheWVyVGV4dD0iZ3B0Ii8+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6VGV4dExheWVycz4gPHBob3Rvc2hvcDpEb2N1bWVudEFuY2VzdG9ycz4gPHJkZjpCYWc+IDxyZGY6bGk+YWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjgyNTg1NWVhLThmZjYtNjk0OC04ODZmLWIxMmZmZDBjMGJlMTwvcmRmOmxpPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjI3YmNlOTk1LTk3ZmEtMDg0OC04ODI5LTZkZjFhMGNjMDA4NSIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL3BuZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YzQwMzgzYy03Mzg4LWNlNDgtYTE4MC1kZTVjNGU0YTA0ZmEiIHN0RXZ0OndoZW49IjIwMjMtMDMtMjFUMTk6NDI6MjEtMDc6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmNlOTczNzJkLTdlZTgtZTg0NC1iODdjLTc1ODc3MzcxMzdkZCIgc3RFdnQ6d2hlbj0iMjAyNC0wNC0xMlQwNDowMToxNy0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZTgxNTM4MTQtOWQ0NC1h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'
: 'iVBORw0KGgoAAAANSUhEUgAAAPoAAAA1CAYAAABoUvZcAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMDGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDQtMTJUMDQ6MDItMDc6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDQtMTJUMDQ6MDItMDc6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjdmZjk2MmQtZTkyMi00YTQzLTgyMzYtMDYyY2Q5YWQyYjhiIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YmNjZTNmODEtMjI4Yy0xNjRmLWExMjAtMjRlMjg1ODk2ZGUxIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjdiY2U5OTUtOTdmYS0wODQ4LTg4MjktNmRmMWEwY2MwMDg1IiB0aWZmOk9yaWVudGF0aW9uPSIxIiB0aWZmOlhSZXNvbHV0aW9uPSI3MjAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjcyMDAwMC8xMDAwMCIgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIgZXhpZjpDb2xvclNwYWNlPSIxIiBleGlmOlBpeGVsWERpbWVuc2lvbj0iNzMwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTU1Ij4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSInZ3B0JyIgcGhvdG9zaG9wOkxheWVyVGV4dD0iZ3B0Ii8+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6VGV4dExheWVycz4gPHBob3Rvc2hvcDpEb2N1bWVudEFuY2VzdG9ycz4gPHJkZjpCYWc+IDxyZGY6bGk+YWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjgyNTg1NWVhLThmZjYtNjk0OC04ODZmLWIxMmZmZDBjMGJlMTwvcmRmOmxpPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjI3YmNlOTk1LTk3ZmEtMDg0OC04ODI5LTZkZjFhMGNjMDA4NSIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL3BuZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YzQwMzgzYy03Mzg4LWNlNDgtYTE4MC1kZTVjNGU0YTA0ZmEiIHN0RXZ0OndoZW49IjIwMjMtMDMtMjFUMTk6NDI6MjEtMDc6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmU1OWQ5NzU4LTk1ZjEtZGE0Ni05YmFiLTM2YjM1YWE0YzhiNiIgc3RFdnQ6d2hlbj0iMjAyNC0wNC0xMlQwNDowMi0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjdmZjk2MmQtZTkyMi00YTQzLTgyMzYt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' )
}
function updateAppStyle() {
appStyle.innerText = (
'.no-user-select { -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none }'
+ '.bravegpt {'
+ `word-wrap: break-word ; white-space: pre-wrap ; margin-bottom: ${ isMobile ? -29 : 20}px ;`
+ 'border: 1px solid var(--color-divider-subtle) ; border-radius: 18px ;'
+ 'padding: 24px 23px 45px 23px ; background:'
+ ( scheme == 'dark' ? ( isMobile ? 'var(--search-gray-800)' : '#282828' ) : 'white' ) + '}'
+ '.bravegpt:hover { box-shadow: 0 9px 28px rgba(0, 0, 0, 0.09) }'
+ '.bravegpt p { margin: 0 }'
+ '.bravegpt .chatgpt-icon { position: relative ; bottom: -4px ; margin-right: 11px }'
+ '.app-name { font-size: 20px ; font-family: var(--brand-font) ; text-decoration: none;'
+ `color: ${ scheme == 'dark' ? 'white' : 'black' } !important }`
+ '.corner-btn { float: right ; cursor: pointer ; position: relative ; top: 4px ;'
+ ( scheme == 'dark' ? 'fill: white ; stroke: white;' : 'fill: #adadad ; stroke: #adadad' ) + '}'
+ `.corner-btn:hover { ${ scheme == 'dark' ? 'fill: #aaa ; stroke: #aaa' : 'fill: black ; stroke: black' }}`
+ '.bravegpt .loading {'
+ 'margin-bottom: -55px ;' // offset vs. `.bravegpt` bottom-padding footer accomodation
+ 'color: #b6b8ba ; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite }'
+ '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
+ '.bravegpt section.loading { padding-left: 5px ; font-size: 90% }'
+ '.standby-btn { width: 100% ; padding: 13px 0 ; cursor: pointer ; margin: 14px 0 20px ;'
+ `border-radius: 4px ; border: 1px solid ${ scheme == 'dark' ? '#fff' : '#000' } ;`
+ 'transition: transform 0.1s ease !important ; transform: scale(1) }'
+ '.standby-btn:hover { border-radius: 4px ; transform: scale(1.025) ;'
+ `${ scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' }}`
+ '.bravegpt pre {'
+ 'font-family: Consolas, Menlo, Monaco, monospace ; white-space: pre-wrap ; line-height: 21px ;'
+ 'padding: 1.2em 1.2em 0 1.2em ; margin-top: .7em ; border-radius: 13px ; overflow: auto ;'
+ ( scheme == 'dark' ? 'background: #3a3a3a ; color: #f2f2f2 } ' : ' background: #eaeaea ; color: #282828 }' )
+ `.bravegpt footer { margin: ${ isFirefox ? 32 : 27 }px 0 -26px 0 ; border-top: none !important }`
+ '.bravegpt .feedback {'
+ 'float: right ; font-family: var(--brand-font) ; font-size: .55rem; color: #aaa ;'
+ 'letter-spacing: .02em ; position: relative ; right: -18px ; bottom: 15px }'
+ '.bravegpt .feedback .icon {'
+ ' fill: currentColor ; color: currentColor ; --size: 12px ; position: relative ; top: 0.19em ; right: 2px }'
+ `.bravegpt footer a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
+ '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
+ '.balloon-tip { content: "" ; position: relative ; border: 7px solid transparent ;'
+ 'border-bottom-style: solid ; border-bottom-width: 16px ; border-top: 0 ; border-bottom-color:'
+ ( scheme == 'dark' ? '#3a3a3a' : '#eaeaea' ) + '}'
+ '.chatgpt-js { font-family: var(--brand-font) ; font-size: .65rem ; position: relative ; right: .9rem }'
+ '.chatgpt-js > a { color: inherit ; top: .054rem }'
+ '.chatgpt-js > svg { top: 3px ; position: relative ; margin-right: 1px }'
+ '.continue-chat > textarea {'
+ `border: solid 1px ${ scheme == 'dark' ? '#aaa' : '#638ed4' } ; border-radius: 12px 15px 12px 0 ;`
+ 'border-radius: 15px 16px 15px 0 ; margin: -6px 0 -7px 0 ; padding: 14px 22px 5px 10px ;'
+ 'height: 2.15rem ; width: 100% ; max-height: 200px ; resize: none ; background:'
+ ( scheme == 'dark' ? '#515151' : '#eeeeee70' ) + '}'
+ '.related-queries { display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: -18px ;'
+ 'position: relative ; top: -3px ;' // scooch up to hug feedback gap
+ `${ isFirefox ? '' : 'margin-top: -31px' }}`
+ '.related-query { margin: 4px 4px 2px 0 ; padding: 8px 13px 7px 14px ;'
+ `color: ${ scheme == 'dark' ? '#f2f2f2' : '#767676' } ;`
+ `background: ${ scheme == 'dark' ? '#424242' : '#dadada12' } ;`
+ `border: 1px solid ${ scheme == 'dark' ? '#777' : '#e1e1e1' } ; font-size: 0.77em ; cursor: pointer ;`
+ 'border-radius: 0 13px 12px 13px ; width: fit-content ; flex: 0 0 auto ;'
+ `box-shadow: 1px 3px ${ scheme == 'dark' ? '11px -8px lightgray' : '8px -6px rgba(169, 169, 169, 0.75)' };`
+ 'transition: transform 0.1s ease !important ; transform: scale(1) }'
+ '.related-query:hover, .related-query:focus { transform: scale(1.025) !important ;'
+ `background: ${ scheme == 'dark' ? '#a2a2a270': '#e5edff ; color: #000000a8 ; border-color: #a3c9ff' }}`
+ '.related-query svg { float: left ; margin: 0.09em 6px 0 0 ;' // related query icon
+ `color: ${ scheme == 'dark' ? '#aaa' : '#c1c1c1' }}`
+ '.fade-in { opacity: 0 ; transform: translateY(7px) ; transition: opacity 0.5s ease, transform 0.5s ease }'
+ '.fade-in.active { opacity: 1 ; transform: translateY(0) }'
+ '#send-btn { border: none ; float: right ; position: relative ; background: none ; margin: 29px 4px 0 0 ;'
+ `color: ${ scheme == 'dark' ? '#aaa' : 'lightgrey' } ; cursor: pointer }`
+ `#send-btn:hover { color: ${ scheme == 'dark' ? 'white' : '#638ed4' } }`
+ '.kudoai { margin-left: 7px ; font-size: .65rem ; color: #aaa }'
+ '.kudoai a { color: #aaa ; text-decoration: none }'
+ `.kudoai a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
+ ( // markdown styles
'.bravegpt pre h1 { font-size: 1.25em } .bravegpt pre h2 { font-size: 1.1em }' // size headings
+ '.bravegpt pre ul { margin: -10px 0 -6px ; }' // reduce v-spacing
+ '.bravegpt pre ol { margin: -33px 0 -6px ; }' // reduce v-spacing
+ '.bravegpt pre li { margin: -10px 0 ; list-style: inside }' ) // reduce v-spacing, show left symbols
+ '.katex-html { display: none }' // hide unrendered math
+ '.chatgpt-modal > div { padding: 24px 20px 24px 20px !important }' // increase alert padding
+ '.chatgpt-modal p { margin-left: 4px ; font-size: 1.115rem }' // position/size alert msg
+ '.chatgpt-modal button {' // alert buttons
+ 'font-size: 0.72rem ; text-transform: uppercase ; min-width: 123px ; '
+ ( !isMobile ? 'padding: 5px !important ;' : '' )
+ 'border-radius: 0 !important ; border: 1px solid ' + ( scheme == 'dark' ? 'white' : 'black' ) + ' !important }'
+ `.modal-buttons { margin: 20px 0px -3px ${ isMobile ? 0 : -7 }px !important }` // position alert buttons
+ '.modal-close-btn { top: -7px }' // raise alert close button
+ ( scheme == 'dark' ? // darkmode alert styles
( '.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {'
+ 'background-color: black !important ; color: white }'
+ '.primary-modal-btn { background: white !important ; color: black !important }'
+ '.modal-close-btn { stroke: white ; fill: white }'
+ '.chatgpt-modal a { color: #00cfff !important }' ) : '' )
+ ( // stylize scrollbars in Chromium/Safari
'.bravegpt *::-webkit-scrollbar { width: 7px }'
+ '.bravegpt *::-webkit-scrollbar-thumb { background: #cdcdcd }'
+ '.bravegpt *::-webkit-scrollbar-thumb:hover { background: #a6a6a6 }'
+ '.bravegpt *::-webkit-scrollbar-track { background: none }' )
+ '.bravegpt * { scrollbar-width: thin }' // make scrollbars thin in Firefox
)
}
function updateTweaksStyle() { // based on settings (for tweaks init + appShow() + toggleSidebar())
tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )}
function updateWSBsvg() {
// Init span/SVG/paths
const wsbSpan = appDiv.querySelector('#wsb-btn'),
wsbSVG = wsbSpan.querySelector('svg')
const wsbONpaths = [
createSVGpath({ fill: '', 'fill-rule': 'evenodd',
d: 'm26,13 0,10 -16,0 0,-10 z m-14,2 12,0 0,6 -12,0 0,-6 z' }) ]
const wsbOFFpaths = [
createSVGpath({ fill: '', 'fill-rule': 'evenodd',
d: 'm28,11 0,14 -20,0 0,-14 z m-18,2 16,0 0,10 -16,0 0,-10 z' }) ]
// Set SVG attributes
for (const [attr, value] of [['width', 18], ['height', 18], ['viewBox', '8 8 20 20']])
wsbSVG.setAttribute(attr, value)
// Update SVG elements
while (wsbSVG.firstChild) { wsbSVG.removeChild(wsbSVG.firstChild) }
const wsbSVGpaths = config.widerSidebar ? wsbONpaths : wsbOFFpaths
wsbSVGpaths.forEach(path => wsbSVG.append(path))
if (!wsbSpan.contains(wsbSVG)) wsbSpan.append(wsbSVG)
}
function updateFooterContent() {
fetchJSON('https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json',
(err, advertisersData) => { if (err) return
// Init vars
let chosenAdvertiser, adSelected
const re_appName = new RegExp(config.appName.toLowerCase(), 'i')
const currentDate = (() => { // in YYYYMMDD format
const today = new Date(), year = today.getFullYear(),
month = String(today.getMonth() + 1).padStart(2, '0'),
day = String(today.getDate()).padStart(2, '0')
return year + month + day
})()
// Select random, active advertiser
for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData))))
if (details.campaigns.text) { chosenAdvertiser = advertiser ; break }
// Fetch a random, active creative
if (chosenAdvertiser) {
const campaignsURL = 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/'
+ chosenAdvertiser + '/text/campaigns.json'
fetchJSON(campaignsURL, (err, campaignsData) => { if (err) return
// Select random, active campaign
for (const [campaignName, campaign] of shuffle(applyBoosts(Object.entries(campaignsData)))) {
const campaignIsActive = campaign.active && (!campaign.endDate || currentDate <= campaign.endDate)
if (!campaignIsActive) continue // to next campaign since campaign inactive
// Select random active group
for (const [groupName, adGroup] of shuffle(applyBoosts(Object.entries(campaign.adGroups)))) {
// Skip disqualified groups
if (/^self$/i.test(groupName) && !re_appName.test(campaignName) // self-group for other apps
|| re_appName.test(campaignName) && !/^self$/i.test(groupName) // non-self group for this app
|| adGroup.active == false // group explicitly disabled
|| adGroup.targetBrowsers && // target browser(s) exist...
!adGroup.targetBrowsers.some( // ...but doesn't match user's
browser => new RegExp(browser, 'i').test(navigator.userAgent))
|| adGroup.targetLocations && ( // target locale(s) exist...
!config.userLocale || !adGroup.targetLocations.some( // ...but user locale is missing or excluded
loc => loc.includes(config.userLocale) || config.userLocale.includes(loc)))
) continue // to next group
// Filter out inactive ads, pick random active one
const activeAds = adGroup.ads.filter(ad => ad.active != false)
if (activeAds.length == 0) continue // to next group since no ads active
const chosenAd = activeAds[Math.floor(Math.random() * activeAds.length)] // random active one
// Build destination URL
let destinationURL = chosenAd.destinationURL || adGroup.destinationURL
|| campaign.destinationURL || ''
if (destinationURL.includes('http')) { // insert UTM tags
const [baseURL, queryString] = destinationURL.split('?'),
queryParams = new URLSearchParams(queryString || '')
queryParams.set('utm_source', config.appName.toLowerCase())
queryParams.set('utm_content', 'app_footer_link')
destinationURL = baseURL + '?' + queryParams.toString()
}
// Update footer content
const newFooterContent = destinationURL ? createAnchor(destinationURL)
: document.createElement('span')
footerContent.replaceWith(newFooterContent) ; footerContent = newFooterContent
footerContent.classList.add('feedback', 'svelte-8js1iq') // Brave classes
footerContent.textContent = chosenAd.text.length < 49 ? chosenAd.text
: chosenAd.text.slice(0, 49) + '...'
footerContent.setAttribute('title', chosenAd.tooltip ||
footerContent.textContent.includes('...') ? chosenAd.text : '')
adSelected = true ; break
}
if (adSelected) break // out of campaign loop after ad selection
}})}})
function fetchJSON(url, callback) { // for dynamic footer
GM.xmlHttpRequest({ method: 'GET', url: url, onload: response => {
if (response.status >= 200 && response.status < 300) {
try { const data = JSON.parse(response.responseText) ; callback(null, data) }
catch (err) { callback(err, null) }
} else callback(new Error('Failed to load data: ' + response.statusText), null)
}})}
function shuffle(list) {
let currentIdx = list.length, tempValue, randomIdx
while (currentIdx != 0) { // elements remain to be shuffled
randomIdx = Math.floor(Math.random() * currentIdx) ; currentIdx -= 1
tempValue = list[currentIdx] ; list[currentIdx] = list[randomIdx] ; list[randomIdx] = tempValue
}
return list
}
function applyBoosts(list) {
let boostedList = [...list],
boostedListLength = boostedList.length - 1 // for applying multiple boosts
list.forEach(([name, data]) => { // check for boosts
if (data.boost) { // boost flagged entry's selection probability
const boostPercent = parseInt(data.boost, 10) / 100,
entriesNeeded = Math.ceil(boostedListLength / (1 - boostPercent)) // total entries needed
* boostPercent - 1 // reduced to boosted entries needed
for (let i = 0 ; i < entriesNeeded ; i++) boostedList.push([name, data]) // saturate list
boostedListLength += entriesNeeded // update for subsequent calculations
}})
return boostedList
}
}
// Define FACTORY functions
function createSVGpath(attrs) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
for (const attr in attrs) path.setAttributeNS(null, attr, attrs[attr])
return path
}
function createAnchor(linkHref, displayContent) {
const anchor = document.createElement('a'),
anchorAttrs = [['href', linkHref], ['target', '_blank'], ['rel', 'noopener']]
anchorAttrs.forEach(([attr, value]) => anchor.setAttribute(attr, value))
if (displayContent) {
if (typeof displayContent == 'string') anchor.textContent = displayContent
else if (displayContent instanceof HTMLElement) anchor.append(displayContent)
}
return anchor
}
// Define TOOLTIP functions
function toggleTooltip(event) { // visibility
tooltipDiv.eventYpos = event.currentTarget.getBoundingClientRect().top // for updateTooltip() y-pos calc
updateTooltip(event.currentTarget.id.replace(/-btn$/, ''))
tooltipDiv.style.opacity = event.type == 'mouseover' ? 0.8 : 0
}
function updateTooltip(buttonType) { // text & position
const cornerBtnTypes = ['about', 'speak', 'wsb'],
[ctrAddend, spreadFactor] = document.querySelector('.standby-btn') ? [15, 18] : [5, 28],
iniRoffset = spreadFactor * (buttonType == 'send' ? 1.65 : cornerBtnTypes.indexOf(buttonType) + 1) + ctrAddend
// Update text
tooltipDiv.innerText = (
buttonType == 'about' ? msgs.menuLabel_about || 'About'
: buttonType == 'speak' ? msgs.tooltip_playAnswer || 'Play answer'
: buttonType == 'wsb' ? (( config.widerSidebar ? `${ msgs.prefix_exit || 'Exit' } ` : '' )
+ msgs.menuLabel_widerSidebar || 'Wider Sidebar' )
: buttonType == 'send' ? msgs.tooltip_sendReply || 'Send reply' : '' )
// Update position
tooltipDiv.style.top = `${ buttonType != 'send' ? -6
: tooltipDiv.eventYpos - appDiv.getBoundingClientRect().top - 34 }px`
tooltipDiv.style.right = `${ iniRoffset - tooltipDiv.getBoundingClientRect().width / 2 }px`
}
// Define SESSION functions
function isBlockedbyCloudflare(resp) {
try {
const html = new DOMParser().parseFromString(resp, 'text/html'),
title = html.querySelector('title')
return title.innerText == 'Just a moment...'
} catch (err) { return false }
}
function deleteOpenAIcookies() {
if (getUserscriptManager() != 'Tampermonkey') return
GM_cookie.list({ url: openAIendpoints.auth }, (cookies, error) => {
if (!error) { for (const cookie of cookies) {
GM_cookie.delete({ url: openAIendpoints.auth, name: cookie.name })
}}})}
function getOpenAItoken() {
return new Promise(resolve => {
const accessToken = GM_getValue(config.keyPrefix + '_openAItoken')
appInfo('OpenAI access token: ' + accessToken)
if (!accessToken) {
GM.xmlHttpRequest({ url: openAIendpoints.session, onload: response => {
if (isBlockedbyCloudflare(response.responseText)) {
appAlert('checkCloudflare') ; return }
try {
const newAccessToken = JSON.parse(response.responseText).accessToken
GM_setValue(config.keyPrefix + '_openAItoken', newAccessToken)
resolve(newAccessToken)
} catch { appAlert('login') ; return }
}})
} else resolve(accessToken)
})}
function generateGPTplusKey() {
let nn = Math.floor(new Date().getTime() / 1e3)
const fD = e => {
let t = CryptoJS.enc.Utf8.parse(e),
o = CryptoJS.AES.encrypt(t, 'fjfsd我w4真3dd服iuhf了wf', {
mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
})
return o.toString()
}
return fD(nn)
}
// Define ANSWER functions
let endpoint, endpointMethod, accessKey, model
async function pickAPI() {
if (config.proxyAPIenabled) { // randomize proxy API
const untriedEndpoints = proxyEndpoints.filter(
entry => !getShowReply.triedEndpoints?.includes(entry[0]))
const entry = untriedEndpoints[Math.floor(chatgpt.randomFloat() * untriedEndpoints.length)]
if (!entry) // no more proxy endpoints left untried
appAlert('suggestOpenAI')
else { endpoint = entry[0] ; endpointMethod = entry[1].method }
} else { // use OpenAI API
endpoint = openAIendpoints.chat
accessKey = await Promise.race([getOpenAItoken(), new Promise(reject =>
setTimeout(() => reject(new Error('Timeout occurred')), 3000))])
if (!accessKey) { appAlert('login') ; return }
endpointMethod = 'POST' ; model = 'gpt-3.5-turbo'
}
appInfo('Endpoint used: ' + endpoint)
}
function createHeaders(api) {
let headers = { 'Content-Type': 'application/json', 'X-Forwarded-For': ipv4.generate({ verbose: false })}
if (api.includes('openai.com')) headers.Authorization = 'Bearer ' + accessKey
headers.Referer = headers.Origin = ( // preserve expected traffic src
api.includes('openai.com') ? 'https://chatgpt.com'
: api.includes('binjie.fun') ? 'https://chat18.aichatos.xyz'
: api.includes('gptforlove.com') ? 'https://ai27.gptforlove.com'
: api.includes('onrender.com') ? 'https://e8.frechat.xyz' : ''
)
return headers
}
const ids = { gptPlus: { parentID: '' }, yqCloud: { userID: '#/chat/' + Date.now() }}
function createPayload(api, msgs) {
let payload = {}
if (api.includes('openai.com'))
payload = { messages: msgs, model: model, max_tokens: 4000 }
else if (api.includes('binjie.fun')) {
payload = {
prompt: msgs[msgs.length - 1].content,
withoutContext: false, userId: ids.yqCloud.userID, network: true
}
} else if (api.includes('gptforlove.com')) {
payload = {
prompt: msgs[msgs.length - 1].content,
secret: generateGPTplusKey(), top_p: 1, temperature: 0.8,
systemMessage: 'You are ChatGPT, the version is GPT-4o, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.'
}
if (ids.gptPlus.parentID) payload.options = { parentMessageId: ids.gptPlus.parentID }
} else if (api.includes('onrender.com'))
payload = { messages: msgs, model: 'gemma-7b-it' }
return JSON.stringify(payload)
}
function getRelatedQueries(query) {
return new Promise((resolve, reject) => {
const rqPrompt = 'Show a numbered list of queries related to this one:\n\n' + query
+ '\n\nMake sure to suggest a variety that can even greatly deviate from the original topic.'
+ ' For example, if the original query asked about someone\'s wife,'
+ ' a good related query could involve a different relative and using their name.'
+ ' Another example, if the query asked about a game/movie/show,'
+ ' good related queries could involve pertinent characters.'
+ ' Another example, if the original query asked how to learn JavaScript,'
+ ' good related queries could ask why/when/where instead, even replacing JS w/ other languages.'
+ ' But the key is variety. Do not be repetitive.'
+ ' You must entice user to want to ask one of your related queries.'
GM.xmlHttpRequest({
method: endpointMethod, url: endpoint, responseType: 'text', headers: createHeaders(endpoint),
data: createPayload(endpoint, [{ role: 'user', content: rqPrompt }]),
onload: event => {
let str_relatedQueries = ''
if (endpoint.includes('openai.com')) {
try { str_relatedQueries = JSON.parse(event.response).choices[0].message.content }
catch (err) { appError(err) ; reject(err) }
} else if (endpoint.includes('binjie.fun')) {
try {
const text = event.responseText, chunkSize = 1024
let currentIdx = 0
while (currentIdx < text.length) {
const chunk = text.substring(currentIdx, currentIdx + chunkSize)
currentIdx += chunkSize ; str_relatedQueries += chunk
}
} catch (err) { appError(err) ; reject(err) }
} else if (endpoint.includes('gptforlove.com')) {
try {
let chunks = event.responseText.trim().split('\n')
str_relatedQueries = JSON.parse(chunks[chunks.length - 1]).text
} catch (err) { appError(err) ; reject(err) }
} else if (endpoint.includes('onrender.com')) {
try { str_relatedQueries = event.responseText ; console.log(event)}
catch (err) { appError(err) ; reject(err) }
}
const arr_relatedQueries = (str_relatedQueries.match(/\d+\.\s*(.*?)(?=\n|$)/g) || [])
.slice(0, 5) // limit to 1st 5
.map(match => match.replace(/^\d+\.\s*/, '')) // strip numbering
resolve(arr_relatedQueries)
},
onerror: err => { appError(err) ; reject(err) }
})
})}
function handleRQevent(event) { // for attachment/removal in `getShowReply()` + `appShow().handleSubmit()`
if ([' ', 'Enter'].includes(event.key) || event.type == 'click') {
event.preventDefault() // prevent scroll on space taps
// Remove divs/listeners
const relatedQueriesDiv = document.querySelector('.related-queries')
Array.from(relatedQueriesDiv.children).forEach(relatedQueryDiv => {
relatedQueryDiv.removeEventListener('click', handleRQevent)
relatedQueryDiv.removeEventListener('keydown', handleRQevent)
})
relatedQueriesDiv.remove()
// Send related query
const chatbar = appDiv.querySelector('textarea')
if (chatbar) {
chatbar.value = event.target.textContent
chatbar.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', bubbles: true, cancelable: true }))
appShow.submitSrc = 'relatedQuery' // to not auto-focus chatbar in appShow()
}
}}
async function getShowReply(convo, callback) {
// Initialize attempt properties
if (!getShowReply.triedEndpoints) getShowReply.triedEndpoints = []
if (!getShowReply.attemptCnt) getShowReply.attemptCnt = 0
// Get/show answer from ChatGPT
await pickAPI()
GM.xmlHttpRequest({
method: endpointMethod, url: endpoint, headers: createHeaders(endpoint),
responseType: 'text', data: createPayload(endpoint, convo), onload: onLoad(),
onerror: err => {
appError(err)
if (!config.proxyAPIenabled) appAlert(!accessKey ? 'login' : 'suggestProxy')
else { // if proxy mode
if (getShowReply.attemptCnt < proxyEndpoints.length) retryDiffHost()
else appAlert('suggestOpenAI')
}}
})
// Get/show related queries
if (!config.rqDisabled) {
const lastQuery = convo[convo.length - 1]
getRelatedQueries(lastQuery.content).then(relatedQueries => {
if (relatedQueries && appDiv.querySelector('textarea')) {
// Create/classify/append parent div
const relatedQueriesDiv = document.createElement('div')
relatedQueriesDiv.className = 'related-queries'
appDiv.append(relatedQueriesDiv)
// Fill each child div, add attributes + icon + listener
relatedQueries.forEach((relatedQuery, idx) => {
const relatedQueryDiv = document.createElement('div'),
relatedQuerySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
relatedQuerySVGpath = document.createElementNS('http://www.w3.org/2000/svg','path')
// Add attributes
relatedQueryDiv.title = msgs.tooltip_sendRelatedQuery || 'Send related query'
relatedQueryDiv.classList.add('related-query', 'fade-in', 'no-user-select')
relatedQueryDiv.setAttribute('tabindex', 0)
relatedQueryDiv.textContent = relatedQuery
// Create icon
for (const [attr, value] of [
['viewBox', '0 0 24 24'], ['width', 18], ['height', 18], ['fill', 'currentColor']
]) relatedQuerySVG.setAttribute(attr, value)
relatedQuerySVGpath.setAttribute('d',
'M16 10H6.83L9 7.83l1.41-1.41L9 5l-6 6 6 6 1.41-1.41L9 14.17 6.83 12H16c1.65 0 3 1.35 3 3v4h2v-4c0-2.76-2.24-5-5-5z')
relatedQuerySVG.style.transform = 'rotate(180deg)' // flip arrow upside down
// Assemble/insert elements
relatedQuerySVG.append(relatedQuerySVGpath) ; relatedQueryDiv.prepend(relatedQuerySVG)
relatedQueriesDiv.append(relatedQueryDiv)
// Add fade + listeners
setTimeout(() => {
relatedQueryDiv.classList.add('active')
relatedQueryDiv.addEventListener('click', handleRQevent)
relatedQueryDiv.addEventListener('keydown', handleRQevent)
}, idx * 100)
})
updateTweaksStyle() // to shorten <pre> max-height
}})}
updateFooterContent()
function retryDiffHost() {
appError(`Error calling ${ endpoint }. Trying another endpoint...`)
getShowReply.triedEndpoints.push(endpoint) // store current proxy to not retry
getShowReply.attemptCnt++
getShowReply(convo, callback)
}
function onLoad() { // process text
const proxyRetryOrAlert = () => {
if (getShowReply.attemptCnt < proxyEndpoints.length) retryDiffHost()
else appAlert('suggestOpenAI')
}
return async event => {
if (event.status != 200) {
appError('Event status: ' + event.status)
appError('Event response: ' + event.responseText)
if (config.proxyAPIenabled && getShowReply.attemptCnt < proxyEndpoints.length)
retryDiffHost()
else if (event.status == 401 && !config.proxyAPIenabled) {
GM_deleteValue(config.keyPrefix + '_openAItoken') ; appAlert('login') }
else if (event.status == 403)
appAlert(config.proxyAPIenabled ? 'suggestOpenAI' : 'checkCloudflare')
else if (event.status == 429) appAlert('tooManyRequests')
else appAlert(config.proxyAPIenabled ? 'suggestOpenAI' : 'suggestProxy')
} else if (endpoint.includes('openai.com')) {
if (event.response) {
try {
appShow(JSON.parse(event.response).choices[0].message.content, footerContent)
} catch (err) {
appInfo('Response: ' + event.response)
appError(appAlerts.parseFailed + ': ' + err)
appAlert('suggestProxy')
}
} else { appInfo('Response: ' + event.responseText) ; appAlert('suggestProxy') }
} else if (endpoint.includes('binjie.fun')) {
if (event.responseText) {
try {
const text = event.responseText, chunkSize = 1024
let answer = '', currentIdx = 0
while (currentIdx < text.length) {
const chunk = text.substring(currentIdx, currentIdx + chunkSize)
currentIdx += chunkSize ; answer += chunk
}
appShow(answer, footerContent) ; getShowReply.triedEndpoints = [] ; getShowReply.attemptCnt = 0
} catch (err) { // use different endpoint or suggest OpenAI
appInfo('Response: ' + event.responseText)
appError(appAlerts.parseFailed + ': ' + err)
proxyRetryOrAlert()
}
} else { appInfo('Response: ' + event.responseText) ; proxyRetryOrAlert() }
} else if (endpoint.includes('gptforlove.com')) {
if (event.responseText && !event.responseText.includes('Fail')) {
try {
let chunks = event.responseText.trim().split('\n'),
lastObj = JSON.parse(chunks[chunks.length - 1])
if (lastObj.id) ids.gptPlus.parentID = lastObj.id
appShow(lastObj.text, footerContent) ; getShowReply.triedEndpoints = [] ; getShowReply.attemptCnt = 0
} catch (err) { // use different endpoint or suggest OpenAI
appInfo('Response: ' + event.responseText)
appError(appAlerts.parseFailed + ': ' + err)
proxyRetryOrAlert()
}
} else { appInfo('Response: ' + event.responseText) ; proxyRetryOrAlert() }
} else if (endpoint.includes('onrender.com')) {
if (event.responseText) {
try {
console.log(event)
appShow(event.responseText, footerContent) ; getShowReply.triedEndpoints = [] ; getShowReply.attemptCnt = 0
} catch (err) { // use different endpoint or suggest OpenAI
appInfo('Response: ' + event.responseText)
appError(appAlerts.parseFailed + ': ' + err)
proxyRetryOrAlert()
}
} else { appInfo('Response: ' + event.responseText) ; proxyRetryOrAlert() }
}
}}}
function appShow(answer, footerContent) {
while (appDiv.firstChild) // clear all children
appDiv.removeChild(appDiv.firstChild)
// Create/append app title anchor
const appTitleAnchor = createAnchor(config.appURL, (() => {
if (appLogoImg.loaded) { // size/return app logo img
appLogoImg.width = 143 ; return appLogoImg
} else { // create/fill/pos/return app name span
const appNameSpan = document.createElement('span')
appNameSpan.innerText = '🤖 ' + config.appName
appNameSpan.style.marginLeft = '3px'
return appNameSpan
}
})())
appTitleAnchor.classList.add('app-name', 'no-user-select')
appDiv.append(appTitleAnchor)
// Create/append 'by KudoAI'
const kudoAIspan = document.createElement('span')
kudoAIspan.classList.add('kudoai', 'no-user-select') ; kudoAIspan.textContent = 'by '
kudoAIspan.style.cssText = appLogoImg.loaded ? 'position: relative ; bottom: 8px ; font-size: 12px' : ''
const kudoAIlink = createAnchor('https://www.kudoai.com', 'KudoAI')
kudoAIspan.append(kudoAIlink) ; appDiv.append(kudoAIspan)
// Create/append about button
const aboutSpan = document.createElement('span'),
aboutSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
aboutSVGpath = document.createElementNS('http://www.w3.org/2000/svg','path')
aboutSpan.id = 'about-btn' // for toggleTooltip()
aboutSpan.className = 'corner-btn'
const aboutSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 56.693 56.693']]
aboutSVGattrs.forEach(([attr,value]) => aboutSVG.setAttribute(attr, value))
aboutSVGpath.setAttribute('d',
'M28.765,4.774c-13.562,0-24.594,11.031-24.594,24.594c0,13.561,11.031,24.594,24.594,24.594 c13.561,0,24.594-11.033,24.594-24.594C53.358,15.805,42.325,4.774,28.765,4.774z M31.765,42.913c0,0.699-0.302,1.334-0.896,1.885 c-0.587,0.545-1.373,0.82-2.337,0.82c-0.993,0-1.812-0.273-2.431-0.814c-0.634-0.551-0.954-1.188-0.954-1.891v-1.209 c0-0.703,0.322-1.34,0.954-1.891c0.619-0.539,1.438-0.812,2.431-0.812c0.964,0,1.75,0.277,2.337,0.82 c0.594,0.551,0.896,1.186,0.896,1.883V42.913z M38.427,24.799c-0.389,0.762-0.886,1.432-1.478,1.994 c-0.581,0.549-1.215,1.044-1.887,1.473c-0.643,0.408-1.248,0.852-1.798,1.315c-0.539,0.455-0.99,0.963-1.343,1.512 c-0.336,0.523-0.507,1.178-0.507,1.943v0.76c0,0.504-0.247,1.031-0.735,1.572c-0.494,0.545-1.155,0.838-1.961,0.871l-0.167,0.004 c-0.818,0-1.484-0.234-1.98-0.699c-0.532-0.496-0.801-1.055-0.801-1.658c0-1.41,0.196-2.611,0.584-3.572 c0.385-0.953,0.86-1.78,1.416-2.459c0.554-0.678,1.178-1.27,1.854-1.762c0.646-0.467,1.242-0.93,1.773-1.371 c0.513-0.428,0.954-0.885,1.312-1.354c0.328-0.435,0.489-0.962,0.489-1.608c0-1.066-0.289-1.83-0.887-2.334 c-0.604-0.512-1.442-0.771-2.487-0.771c-0.696,0-1.294,0.043-1.776,0.129c-0.471,0.083-0.905,0.223-1.294,0.417 c-0.384,0.19-0.745,0.456-1.075,0.786c-0.346,0.346-0.71,0.783-1.084,1.301c-0.336,0.473-0.835,0.83-1.48,1.062 c-0.662,0.239-1.397,0.175-2.164-0.192c-0.689-0.344-1.11-0.793-1.254-1.338c-0.135-0.5-0.135-1.025-0.002-1.557 c0.098-0.453,0.369-1.012,0.83-1.695c0.451-0.67,1.094-1.321,1.912-1.938c0.814-0.614,1.847-1.151,3.064-1.593 c1.227-0.443,2.695-0.668,4.367-0.668c1.648,0,3.078,0.249,4.248,0.742c1.176,0.496,2.137,1.157,2.854,1.967 c0.715,0.809,1.242,1.738,1.568,2.762c0.322,1.014,0.486,2.072,0.486,3.146C39.024,23.075,38.823,24.024,38.427,24.799z')
aboutSVGpath.setAttribute('stroke', 'none')
aboutSVG.append(aboutSVGpath) ; aboutSpan.append(aboutSVG) ; appDiv.append(aboutSpan)
// Create/append speak button
if (answer != 'standby') {
var speakSpan = document.createElement('span'),
speakSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
speakSpan.id = 'speak-btn' // for toggleTooltip()
speakSpan.className = 'corner-btn' ; speakSpan.style.margin = '-0.04em 5px 0 0'
const speakSVGattrs = [['width', 22], ['height', 22], ['viewBox', '0 0 32 32']]
speakSVGattrs.forEach(([attr, value]) => speakSVG.setAttributeNS(null, attr, value))
const speakSVGpaths = [
createSVGpath({ stroke: '', 'stroke-width': '2px', fill: 'none',
d: 'M24.5,26c2.881,-2.652 4.5,-6.249 4.5,-10c0,-3.751 -1.619,-7.348 -4.5,-10' }),
createSVGpath({ stroke: '', 'stroke-width': '2px', fill: 'none',
d: 'M22,20.847c1.281,-1.306 2,-3.077 2,-4.924c0,-1.846 -0.719,-3.617 -2,-4.923' }),
createSVGpath({ stroke: 'none', fill: '',
d: 'M9.957,10.88c-0.605,0.625 -1.415,0.98 -2.262,0.991c-4.695,0.022 -4.695,0.322 -4.695,4.129c0,3.806 0,4.105 4.695,4.129c0.846,0.011 1.656,0.366 2.261,0.991c1.045,1.078 2.766,2.856 4.245,4.384c0.474,0.49 1.18,0.631 1.791,0.36c0.611,-0.272 1.008,-0.904 1.008,-1.604c0,-4.585 0,-11.936 0,-16.52c0,-0.7 -0.397,-1.332 -1.008,-1.604c-0.611,-0.271 -1.317,-0.13 -1.791,0.36c-1.479,1.528 -3.2,3.306 -4.244,4.384Z' })
]
speakSVGpaths.forEach(path => speakSVG.append(path))
speakSpan.append(speakSVG) ; appDiv.append(speakSpan)
}
if (!isMobile) {
// Create/append Wider Sidebar button
var wsbSpan = document.createElement('span'),
wsbSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
wsbSpan.id = 'wsb-btn' // for updateWSBsvg() + toggleTooltip()
wsbSpan.className = 'corner-btn' ; wsbSpan.style.margin = '0.07rem 9px 0 0'
wsbSpan.append(wsbSVG) ; appDiv.append(wsbSpan) ; updateWSBsvg()
}
// Add tooltips
appDiv.append(tooltipDiv)
// Add corner button listeners
aboutSVG.addEventListener('click', launchAboutModal)
speakSVG?.addEventListener('click', () => {
const dialectMap = [
{ code: 'en', regex: /^(eng(lish)?|en(-\w\w)?)$/i, rate: 2 },
{ code: 'ar', regex: /^(ara?(bic)?|اللغة العربية)$/i, rate: 1.5 },
{ code: 'cs', regex: /^(cze(ch)?|[cč]e[sš].*|cs)$/i, rate: 1.4 },
{ code: 'da', regex: /^dan?(ish|sk)?$/i, rate: 1.3 },
{ code: 'de', regex: /^(german|deu?(tsch)?)$/i, rate: 1.5 },
{ code: 'es', regex: /^(spa(nish)?|espa.*|es(-\w\w)?)$/i, rate: 1.5 },
{ code: 'fi', regex: /^(fin?(nish)?|suom.*)$/i, rate: 1.4 },
{ code: 'fr', regex: /^fr/i, rate: 1.2 },
{ code: 'hu', regex: /^(hun?(garian)?|magyar)$/i, rate: 1.5 },
{ code: 'it', regex: /^ita?(lian[ao]?)?$/i, rate: 1.4 },
{ code: 'ja', regex: /^(ja?pa?n(ese)?|日本語|ja)$/i, rate: 1.5 },
{ code: 'nl', regex: /^(dut(ch)?|flemish|nederlandse?|vlaamse?|nld?)$/i, rate: 1.3 },
{ code: 'pl', regex: /^po?l(ish|ski)?$/i, rate: 1.4 },
{ code: 'pt', regex: /^(por(tugu[eê]se?)?|pt(-\w\w)?)$/i, rate: 1.5 },
{ code: 'ru', regex: /^(rus?(sian)?|русский)$/i, rate: 1.3 },
{ code: 'sv', regex: /^(swe?(dish)?|sv(enska)?)$/i, rate: 1.4 },
{ code: 'tr', regex: /^t[uü]?r(k.*)?$/i, rate: 1.6 },
{ code: 'vi', regex: /^vi[eệ]?t?(namese)?$/i, rate: 1.5 },
{ code: 'zh-CHS', regex: /^(chi(nese)?|zh|中[国國])/i, rate: 2 }
]
const replyDialect = dialectMap.find(entry => entry.regex.test(config.replyLanguage)) || dialectMap[0],
payload = { text: answer, curTime: Date.now(), spokenDialect: replyDialect.code, rate: replyDialect.rate.toString() },
key = CryptoJS.enc.Utf8.parse('76350b1840ff9832eb6244ac6d444366'),
iv = CryptoJS.enc.Utf8.parse(atob('AAAAAAAAAAAAAAAAAAAAAA==') || '76350b1840ff9832eb6244ac6d444366')
const securePayload = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
iv: iv, mode: CryptoJS.mode.CBC, pad: CryptoJS.pad.Pkcs7 }).toString()
GM.xmlHttpRequest({ // audio from Sogou TTS
url: 'https://fanyi.sogou.com/openapi/external/getWebTTS?S-AppId=102356845&S-Param='
+ encodeURIComponent(securePayload),
method: 'GET', responseType: 'arraybuffer',
onload: async response => {
if (response.status != 200) chatgpt.speak(answer, { voice: 2, pitch: 1, speed: 1.5 })
else {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
audioContext.decodeAudioData(response.response, buffer => {
const audioSrc = audioContext.createBufferSource()
audioSrc.buffer = buffer
audioSrc.connect(audioContext.destination) // connect source to speakers
audioSrc.start(0) // play audio
})}}
})
})
wsbSVG?.addEventListener('click', () => toggleSidebar('wider'))
const buttonSpans = [aboutSpan, speakSpan, wsbSpan]
buttonSpans.forEach(span => { if (span) { // add hover listeners for tooltips
span.addEventListener('mouseover', toggleTooltip)
span.addEventListener('mouseout', toggleTooltip)
}})
// Show standby state if prefix/suffix mode on
if (answer == 'standby') {
const standbyBtn = document.createElement('button')
standbyBtn.className = 'standby-btn'
standbyBtn.textContent = msgs.buttonLabel_sendQueryToGPT || 'Send search query to GPT'
appDiv.append(standbyBtn)
standbyBtn.addEventListener('click', () => {
appAlert('waitingResponse')
const query = `${ new URL(location.href).searchParams.get('q') } (reply in ${ config.replyLanguage })`
convo.push({ role: 'user', content: query })
getShowReply(convo)
})
// Otherwise create/append ChatGPT response
} else {
const balloonTipSpan = document.createElement('span')
var answerPre = document.createElement('pre')
balloonTipSpan.className = 'balloon-tip'
balloonTipSpan.style.cssText = ( // pos it
`top: ${( isFirefox ? 0.33 : 0.16 ) - ( appLogoImg.loaded ? 0.13 : 0 )}em ;`
+ `right: ${ isFirefox ? ( 10.03 - ( appLogoImg.loaded ? 0 : 0.577 ))
: ( 5.01 - ( appLogoImg.loaded ? 0 : 0.262 ))}rem`
)
answerPre.textContent = answer
appDiv.append(balloonTipSpan) ; appDiv.append(answerPre)
}
// Create/append reply section/elements
const replySection = document.createElement('section'),
replyForm = document.createElement('form'),
continueChatDiv = document.createElement('div'),
chatTextarea = document.createElement('textarea')
continueChatDiv.className = 'continue-chat'
chatTextarea.id = 'app-chatbar' ; chatTextarea.rows = '1'
chatTextarea.placeholder = ( answer == 'standby' ? msgs.placeholder_askSomethingElse || 'Ask something else'
: msgs.tooltip_sendReply || 'Send reply' ) + '...'
continueChatDiv.append(chatTextarea)
replyForm.append(continueChatDiv) ; replySection.append(replyForm)
appDiv.append(replySection)
// Create/append send button
const sendButton = document.createElement('button'),
sendSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sendSVGpath = createSVGpath({ stroke: '', 'stroke-width': '2', linecap: 'round',
'stroke-linejoin': 'round', d: 'M7 11L12 6L17 11M12 18V7' })
sendButton.id = 'send-btn'
sendButton.style.right = '10px' ; sendButton.style.bottom = `${ isFirefox ? 56 : 60 }px`
for (const [attr, value] of [
['viewBox', '4 2 16 16'], ['fill', 'none'], ['height', 16], ['width', 16],
['stroke', 'currentColor'], ['stroke-width', '2'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round']
]) sendSVG.setAttribute(attr, value)
sendSVG.append(sendSVGpath) ; sendButton.append(sendSVG) ; continueChatDiv.append(sendButton)
// Create/classify/fill/append footer
const appFooter = document.createElement('footer')
appFooter.append(footerContent) ; appDiv.append(appFooter)
// Render markdown/math
if (answer != 'standby') {
answerPre.innerHTML = marked.parse(answer)
renderMathInElement(answerPre, { // eslint-disable-line no-undef
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
{ left: '\\begin{equation}', right: '\\end{equation}', display: true },
{ left: '\\begin{align}', right: '\\end{align}', display: true },
{ left: '\\begin{alignat}', right: '\\end{alignat}', display: true },
{ left: '\\begin{gather}', right: '\\end{gather}', display: true },
{ left: '\\begin{CD}', right: '\\end{CD}', display: true },
{ left: '\\[', right: '\\]', display: true }
],
throwOnError: false
})}
// Add reply section listeners
replyForm.addEventListener('keydown', handleEnter)
replyForm.addEventListener('submit', handleSubmit)
chatTextarea.addEventListener('input', autosizeChatbar)
sendButton.addEventListener('mouseover', toggleTooltip)
sendButton.addEventListener('mouseout', toggleTooltip)
// Focus chatbar if user typed in prev appShow()
if (appShow.submitSrc && appShow.submitSrc != 'relatedQuery') chatTextarea.focus()
appShow.submitSrc = 'none'
function handleEnter(event) {
if (event.key == 'Enter') {
if (event.ctrlKey) { // add newline
const chatTextarea = document.querySelector('#app-chatbar'),
caretPos = chatTextarea.selectionStart,
textBefore = chatTextarea.value.substring(0, caretPos),
textAfter = chatTextarea.value.substring(caretPos)
chatTextarea.value = textBefore + '\n' + textAfter // add newline
chatTextarea.selectionStart = chatTextarea.selectionEnd = caretPos + 1 // preserve caret pos
autosizeChatbar()
} else if (!event.shiftKey) handleSubmit(event)
}}
function handleSubmit(event) {
event.preventDefault()
if (convo.length > 2) convo.splice(0, 2) // keep token usage maintainable
const prevReplyTrimmed = appDiv.querySelector('pre')?.textContent.substring(0, 250 - chatTextarea.value.length) || '',
yourReply = `${ chatTextarea.value } (reply in ${ config.replyLanguage })`
convo.push({ role: 'assistant', content: prevReplyTrimmed })
convo.push({ role: 'user', content: yourReply })
getShowReply(convo)
// Remove re-added reply section listeners
replyForm.removeEventListener('keydown', handleEnter)
replyForm.removeEventListener('submit', handleSubmit)
chatTextarea.removeEventListener('input', autosizeChatbar)
// Remove related queries
try {
const relatedQueriesDiv = document.querySelector('.related-queries')
Array.from(relatedQueriesDiv.children).forEach(relatedQueryDiv => {
relatedQueryDiv.removeEventListener('click', handleRQevent)
relatedQueryDiv.removeEventListener('keydown', handleRQevent)
})
relatedQueriesDiv.remove()
} catch (err) {}
// Remove 'Send reply' tooltip from send btn clicks
tooltipDiv.style.opacity = 0
// Clear footer
while (appFooter.firstChild) // clear all children
appFooter.removeChild(appFooter.firstChild)
// Show loading status
replySection.classList.add('loading', 'no-user-select')
replySection.innerText = appAlerts.waitingResponse
}
let prevLength = chatTextarea.value.length
function autosizeChatbar() {
const newLength = chatTextarea.value.length
if (newLength < prevLength) { // if deleting txt
chatTextarea.style.height = 'auto' // ...auto-fit height
if (parseInt(getComputedStyle(chatTextarea).height) < 55) { // if down to one line
chatTextarea.style.height = '2.15rem' } // ...reset to original height
}
chatTextarea.style.height = chatTextarea.scrollHeight > 55 ? chatTextarea.scrollHeight + 'px' : '2.15rem'
prevLength = newLength
}
}
// Run MAIN routine
// Init CONFIG/CONVO/MENU
const config = {
appName: 'BraveGPT', appSymbol: '🤖', keyPrefix: 'braveGPT',
appURL: 'https://www.bravegpt.com', gitHubURL: 'https://github.com/KudoAI/bravegpt',
greasyForkURL: 'https://greasyfork.org/scripts/462440-bravegpt' }
config.updateURL = config.greasyForkURL.replace('https://', 'https://update.')
.replace(/(\d+)-?([a-zA-Z-]*)$/, (_, id, name) => `${ id }/${ !name ? 'script' : name }.meta.js`)
config.supportURL = config.gitHubURL + '/issues/new'
config.feedbackURL = config.gitHubURL + '/discussions/new/choose'
config.assetHostURL = config.gitHubURL.replace('github.com', 'cdn.jsdelivr.net/gh') + '@b57563a/'
config.userLanguage = chatgpt.getUserLanguage()
config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
loadSetting('autoGetDisabled', 'prefixEnabled', 'proxyAPIenabled', 'replyLanguage',
'rqDisabled', 'suffixEnabled', 'widerSidebar')
if (!config.replyLanguage) saveSetting('replyLanguage', config.userLanguage) // init reply language if unset
const convo = [], menuIDs = []
const state = {
symbol: ['✔️', '❌'], word: ['ON', 'OFF'],
separator: getUserscriptManager() == 'Tampermonkey' ? ' — ' : ': ' }
// Init UI flags
let scheme = isDarkMode() ? 'dark' : 'light'
const isFirefox = chatgpt.browser.isFirefox(),
isMobile = chatgpt.browser.isMobile()
// Pre-load LOGO
const appLogoImg = document.createElement('img') ; updateAppLogoSrc()
appLogoImg.onload = () => appLogoImg.loaded = true // for app header tweaks in appShow() + .balloon-tip pos in updateAppStyle()
// Define MESSAGES
const msgsLoaded = new Promise(resolve => {
const msgHostDir = config.assetHostURL + 'greasemonkey/_locales/',
msgLocaleDir = ( config.userLanguage ? config.userLanguage.replace('-', '_') : 'en' ) + '/'
let msgHref = msgHostDir + msgLocaleDir + 'messages.json', msgXHRtries = 0
GM.xmlHttpRequest({ method: 'GET', url: msgHref, onload: onLoad })
function onLoad(resp) {
try { // to return localized messages.json
const msgs = JSON.parse(resp.responseText), flatMsgs = {}
for (const key in msgs) // remove need to ref nested keys
if (typeof msgs[key] == 'object' && 'message' in msgs[key])
flatMsgs[key] = msgs[key].message
resolve(flatMsgs)
} catch (err) { // if bad response
msgXHRtries++ ; if (msgXHRtries == 3) return resolve({}) // try up to 3X (original/region-stripped/EN) only
msgHref = config.userLanguage.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
msgHref.replace(/([^_]*)_[^/]*(\/.*)/, '$1$2') // ...strip region before retrying
: ( msgHostDir + 'en/messages.json' ) // else use default English messages
GM.xmlHttpRequest({ method: 'GET', url: msgHref, onload: onLoad })
}
}
}) ; const msgs = await msgsLoaded
registerMenu()
// Init ENDPOINTS
const openAIendpoints = {
auth: 'https://auth0.openai.com',
session: 'https://chatgpt.com/api/auth/session',
chat: 'https://api.openai.com/v1/chat/completions'
}
const proxyEndpoints = [
[ 'https://api.binjie.fun/api/generateStream', { method: 'POST', stream: true }],
[ 'https://api11.gptforlove.com/chat-process', { method: 'POST', stream: true }],
[ 'https://demo-yj7h.onrender.com/single/chat_messages', { method: 'PUT', stream: true }]
]
// Init ALERTS
const appAlerts = {
waitingResponse: ( msgs.alert_waitingResponse || 'Waiting for ChatGPT response' ) + '...',
login: ( msgs.alert_login || 'Please login' ) + ' @ ',
tooManyRequests: ( msgs.alert_tooManyRequests || 'ChatGPT is flooded with too many requests' ) + '. '
+ ( config.proxyAPIenabled ? ( msgs.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
: ( msgs.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' )),
parseFailed: ( msgs.alert_parseFailed || 'Failed to parse response JSON' ) + '. '
+ ( config.proxyAPIenabled ? ( msgs.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
: ( msgs.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' )),
checkCloudflare: ( msgs.alert_checkCloudflare || 'Please pass Cloudflare security check' ) + ' @ ',
suggestProxy: ( msgs.alert_openAInotWorking || 'OpenAI API is not working' ) + '. '
+ ( msgs.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' ),
suggestOpenAI: ( msgs.alert_proxyNotWorking || 'Proxy API is not working' ) + '. '
+ ( msgs.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
}
// STYLIZE elements
const appStyle = document.createElement('style')
updateAppStyle() ; document.head.append(appStyle)
// Create Brave Search style TWEAKS
const tweaksStyle = document.createElement('style'),
wsbStyles = 'main.main-column, aside.sidebar { max-width: 521px !important }'
+ '.bravegpt { width: 521px }'
updateTweaksStyle() ; document.head.append(tweaksStyle)
// Create/stylize TOOLTIP div
const tooltipDiv = document.createElement('div'),
tooltipStyle = document.createElement('style')
tooltipDiv.classList.add('button-tooltip', 'no-user-select')
tooltipStyle.innerText = '.button-tooltip {'
+ 'background: black ; padding: 5px ; border-radius: 6px ; border: 1px solid #d9d9e3 ;' // bubble style
+ 'font-size: 0.55rem ; color: white ;' // font style
+ 'position: absolute ;' // for updateTooltip() calcs
+ 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 9999 }' // visibility
document.head.append(tooltipStyle)
// Create/classify BRAVEGPT container
const appDiv = document.createElement('div') // create container div
appDiv.classList.add('bravegpt', 'fade-in', // BraveGPT classes
'snippet') // Brave class
// APPEND to Brave
const hostContainer = document.querySelector(isMobile ? '#results' : '.sidebar')
setTimeout(() => {
hostContainer.prepend(appDiv)
setTimeout(() => appDiv.classList.add('active'), 100) // fade in
}, isMobile ? 500 : 100)
// Remove non-visible OVERFLOW STYLES for boundless hover fx
let appAncestor = hostContainer
while (appAncestor) {
if (getComputedStyle(appAncestor).overflow != 'visible') appAncestor.style.overflow = 'visible'
appAncestor = appAncestor.parentElement
}
// Init footer CTA to share feedback
let footerContent = createAnchor(config.feedbackURL, msgs.link_shareFeedback || 'Feedback')
footerContent.classList.add('feedback', 'svelte-8js1iq') // Brave classes
// Show STANDBY mode or get/show ANSWER
if (config.autoGetDisabled
|| config.prefixEnabled && !/.*q=%2F/.test(document.location) // prefix required but not present
|| config.suffixEnabled && !/.*q=.*%3F(&|$)/.test(document.location) // suffix required but not present
) { updateFooterContent() ; appShow('standby', footerContent) }
else {
appAlert('waitingResponse')
const query = `${ new URL(location.href).searchParams.get('q') } (reply in ${ config.replyLanguage })`
convo.push({ role: 'user', content: query })
getShowReply(convo)
}
// Observe/listen for Brave Search + system SCHEME CHANGES to update BraveGPT logo/style scheme
(new MutationObserver(handleSchemeChange)).observe( // class changes from Brave Search theme settings
document.documentElement, { attributes: true, attributeFilter: ['class'] })
window.matchMedia('(prefers-color-scheme: dark)') // window.matchMedia changes from browser/system settings
.addEventListener('change', handleSchemeChange)
function handleSchemeChange() {
const newScheme = isDarkMode() ? 'dark' : 'light'
if (newScheme != scheme) { scheme = newScheme ; updateAppLogoSrc() ; updateAppStyle() }
}
}, 1000)