Greasy Fork is available in English.

BraveGPT 🤖

เพิ่มคำตอบของ ChatGPT ไปยังแถบข้างของการค้นหา Brave (ทำงานโดย GPT-4!)

ติดตั้งสคริปต์นี้?
สคริปต์ที่แนะนำของผู้เขียน

คุณอาจชื่นชอบ DuckDuckGPT 🤖

ติดตั้งสคริปต์นี้
// ==UserScript==
// @name                BraveGPT 🤖
// @description         Adds ChatGPT answers to Brave Search sidebar (powered by GPT-4!)
// @description:af      Voeg ChatGPT-antwoorde by Brave Search-kantbalk by (aangedryf deur GPT-4!)
// @description:am      የChatGPT መልስናወርቃለች እርስዎን በBrave Search የተወሰኑ ገጽታዎችን (ተግባር በGPT-4!) ይጨምሩ
// @description:ar      يضيف إجابات ChatGPT إلى شريط البحث الجانبي في Brave (مدعوم بواسطة GPT-4!)
// @description:az      ChatGPT cavablarını Brave Axtarış yan panelinə əlavə edir (GPT-4 ilə gücləndirilmiş!)
// @description:be      Дадае адказы ChatGPT да бакавой баковай панэлі Brave Search (падтрымліваецца GPT-4!)
// @description:bem     Aziya ChatGPT ndalama ku Brave Search sidebar (muma GPT-4!)
// @description:bg      Добавя ChatGPT отговори към страничната лента на Brave Search (задвижван от GPT-4!)
// @description:bn      Brave সার্চ সাইডবারে ChatGPT উত্তর যোগ করে (পাওয়ারডে GPT-4 দ্বারা!)
// @description:bo      ChatGPT ལེ་བས་ཚད་བདག་སྐྱེད་དེ་བཟུམ་སྒྲིག་ནང་ Brave Search གནས་པ་བརྗོད་པ། (GPT-4བྱ་བ་བརྒྱུད་པ་!)
// @description:bs      Dodaje odgovore ChatGPT-a na bočnu traku Brave pretrage (pokreće GPT-4!)
// @description:ca      Afegeix respostes de ChatGPT a la barra lateral de Brave Search (amb tecnologia GPT-4!)
// @description:ceb     Nagdugang sa mga tubag sa ChatGPT sa sidebar sa Brave Search (gamit ang GPT-4!)
// @description:ckb     وەرگرتنی ڕاستەوخۆیی ChatGPT بۆ پەنجەرەی لاتی لە Brave (بە پشتگیرییی GPT-4!)
// @description:cs      Přidává odpovědi od ChatGPT do bočního panelu Brave Search (poháněno GPT-4!)
// @description:cy      Ychwanega Atebion ChatGPT i'r bar ochr Brave Search (a gryfhawyd gan GPT-4!)
// @description:da      Tilføjer ChatGPT-svar til Brave Search-sidelinjen (drevet af GPT-4!)
// @description:de      Fügt ChatGPT-Antworten zur Seitenleiste der Brave-Suche hinzu (betrieben mit GPT-4!)
// @description:dv      ChatGPT އައިކްސޭޓުގެ ޖަވާބުގެ Brave Search ސައިޓުގައި ފޯރުވާރައުގެ ޑައުން (އެކައުންއައި ވަކި GPT-4!)
// @description:dz      ChatGPT དང་ Brave འབྱུང་ཆུང་ལེ་བས་འཐུས་པ་ལགས་སྤྱོད་སྒྲིག་པ་བརྟགས་བཞུགས། (GPT-4་གི་སྒྲིག་དང་!)
// @description:el      Προσθέτει απαντήσεις ChatGPT στην πλαϊνή γραμμή αναζήτησης του Brave (με την υποστήριξη του GPT-4!)
// @description:eo      Aldonas ChatGPT-respondojn al la flanka breto de Brave Serĉo (funkciigita de GPT-4!)
// @description:es      Agrega respuestas de ChatGPT a la barra lateral de Brave Search (¡impulsado por GPT-4!)
// @description:et      Lisab ChatGPT vastused Brave Search küljepaneelile (toetatud GPT-4 poolt!)
// @description:eu      Gehitu ChatGPT erantzunak Brave Search aldeko alderakoan (GPT-4ren aurrerapenean oinarrituta!)
// @description:fa      ChatGPT پاسخها را به نوار کناری جستجوی Brave اضافه میکند (قدرت گرفته شده توسط GPT-4!)
// @description:fi      Lisää ChatGPT-vastaukset Brave-haun sivupalkkiin (käyttäen GPT-4!)
// @description:fo      Leggur ChatGPT-svar til Brave leitarstika síðupall (drivin av GPT-4!)
// @description:fr      Ajoute les réponses de ChatGPT à la barre latérale de Brave Search (propulsé par GPT-4 !)
// @description:fr-CA   Ajoute les réponses de ChatGPT à la barre latérale de Brave Search (propulsé par GPT-4 !)
// @description:gd      Cur Freagairtean ChatGPT ris an t-siostam-cùlaiche airson Innse Brave (le GPT-4 air!)
// @description:gl      Engade respostas de ChatGPT á barra lateral de busca de Brave (potenciado por GPT-4!)
// @description:gu      બ્રેવ શોધનપટનમાં ChatGPT જવાબો ઉમેરે છે (પાવર્ડપુંજ GPT-4 દ્વારા!)
// @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-4!)
// @description:he      מוסיף תשובות של ChatGPT לסרגל הצד של חיפוש Brave (מופעל על ידי GPT-4!)
// @description:hi      ब्रेव सर्च साइडबार में ChatGPT उत्तर जोड़ता है (GPT-4 द्वारा संचालित!)
// @description:hr      Dodaje odgovore ChatGPT-a na bočnu traku Brave pretraživanja (pokreće GPT-4!)
// @description:ht      Ajoute ChatGPT repons nan sibbard Brave Search la (pouvwa pa GPT-4!)
// @description:hu      Hozzáadja a ChatGPT válaszokat a Brave Search oldalsávjához (GPT-4 által támogatva!)
// @description:hy      Ավելացնում է ChatGPT պատասխանները Brave Search կողմից (ուղղահայացված է GPT-4-ի միջոցով!)
// @description:id      Menambahkan jawaban ChatGPT ke sidebar Brave Search (didukung oleh GPT-4!)
// @description:is      Bætir við ChatGPT svari í hliðarstiku Brave leitarinnar (rekinn með GPT-4!)
// @description:it      Aggiunge le risposte di ChatGPT alla barra laterale di Brave Search (alimentato da GPT-4!)
// @description:ja      Brave 検索サイドバーに ChatGPT の回答を追加します(GPT-4 で動作中!)
// @description:jv      Nambahake jawaban ChatGPT menyang sidebar Pencarian Brave (didheteni GPT-4!)
// @description:ka      დაამატებს ChatGPT პასუხებს Brave Search გვერდის მარჯვნივ (GPT-4-ით გამოყენებული!)
// @description:kab     Izgan-d yemdanen n teblaḍ n Brave Search (ɣef GPT-4!)
// @description:kk      Brave іздеу жағдайындағы ChatGPT жауаптарын қосады (GPT-4-мен жұмыс істейді!)
// @description:km      បន្ថែមចម្លើយ ChatGPT ទៅរបារចំហៀងស្វែងរកក្លាហាន (ដំណើរការដោយ GPT-4!)
// @description:kn      ಬ್ರೇವ್ ಶೋಧನೆ ಪಟ್ಟಿಗೆ ChatGPT ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (GPT-4 ಅನ್ನು ಬಳಸಿ!)
// @description:ko      ChatGPT 답변을 Brave 검색 사이드바에 추가합니다 (GPT-4로 구동됨!)
// @description:ku      Li ser panoya li serê lêgerîna Brave dihêle ChatGPT bersivan (bi alîkariya GPT-4!)
// @description:ky      ChatGPT каардарларын Браве Издөө экибинын көнчыгышына кошотот (GPT-4 менен күтөлгөн!)
// @description:la      Adiungit responsiones ChatGPT ad barra lateralem investigatiois Brave (GPT-4 motore!)
// @description:lb      Füügt ChatGPT Äntwerten zur Brave-Sichbarsäit bäi (gedriwwen vun GPT-4!)
// @description:lo      ຕອບສອນ ChatGPT ໄປຫາແຖບຂັ້ນຕອນຂອງການຄົ້ນຫາ Brave (ໂດຍໃຊ້ GPT-4!)
// @description:lt      Prideda ChatGPT atsakymus į šoninę juostą paieškai Brave (varomi GPT-4!)
// @description:lv      Pievieno ChatGPT atbildes Brave meklēšanas sānjoslai (darbināts ar GPT-4!)
// @description:mg      Mampiditra valiny avy amin'ny ChatGPT ao amin'ny saroka Search Brave (maneho ny GPT-4!)
// @description:mi      Tāpiri atu i ngā whakautu a ChatGPT ki te tara taha o te Rapu Wawata Brave (e whakahaerehia ana e GPT-4!)
// @description:mk      Додава одговори од ChatGPT на страничната лента на Brave Search (поддржано од GPT-4!)
// @description:ml      Brave സേർച്ചിന്റെ സൈഡ്ബാർലേക്ക് ChatGPT ഉത്തരങ്ങൾ ചേർക്കുന്നു (GPT-4-യിൽ പ്രവർത്തിക്കുന്നു!)
// @description:mn      ChatGPT хариултуудыг Brave Хайлтын зааврын зурагт нэмнэ (GPT-4 ашиглаж!)
// @description:ms      Menambah jawapan ChatGPT ke sidebar Carian Brave (dikuasakan oleh GPT-4!)
// @description:mt      Iżżid Risposti ta 'ChatGPT lill-Sidebar Brave Search (pwered by GPT-4!)
// @description:my      ChatGPT အဖြေမှာ Brave ရွေးချယ်ရေးတွင် ChatGPT အဖြေများကိုထည့်သွင်းထားသည် (GPT-4 ဖြင့်အလုပ်လုပ်ပါ!)
// @description:ne      ChatGPT उत्तरहरूलाई ब्रेभ खोजको साइडबारमा थप्छ (GPT-4 द्वारा संचालित!)
// @description:nl      Voegt ChatGPT-antwoorden toe aan de zijbalk van Brave Search (aangedreven door GPT-4!)
// @description:no      Legger til ChatGPT-svar i sidenotatfeltet for Brave Search (drevet av GPT-4!)
// @description:ny      Anayambitsa zambiri za ChatGPT kubanja lovala la Brave Search (liyenera ndi GPT-4!)
// @description:pa      ਬਰੇਵ ਖੋਜ ਦੇ ਸਾਈਡਬਾਰ ਵਿੱਚ ChatGPT ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ (GPT-4 ਦੁਆਰਾ ਸ਼ਕਤੀਸ਼ਾਲਕ ਕੀਤਾ ਗਿਆ!)
// @description:pap     Añadi respuèstanan di ChatGPT na bar lateral di Buskeda Brave (poderá pa GPT-4!)
// @description:pl      Dodaje odpowiedzi ChatGPT do paska bocznego Brave Search (napędzane przez GPT-4!)
// @description:ps      په سپینې د Brave لټونکې توګه ChatGPT جوابونه ورکړي (د GPT-4 لخوا سره!)
// @description:pt      Adiciona respostas do ChatGPT à barra lateral de pesquisa do Brave (alimentado por GPT-4!)
// @description:pt-BR   Adiciona respostas do ChatGPT à barra lateral de pesquisa do Brave (alimentado por GPT-4!)
// @description:rn      Abugira inkomoko za ChatGPT mu gisubizo cya Brave Search (yahindutse na GPT-4!)
// @description:ro      Adaugă răspunsuri ChatGPT în bara laterală de căutare Brave (propulsat de GPT-4!)
// @description:ru      Добавляет ответы ChatGPT в боковую панель поиска Brave (работает на GPT-4!)
// @description:rw      Atera amakuru ya ChatGPT mu bariro bya Brave Search (yarahindutse n'ikoranabuhanga cya GPT-4!)
// @description:sg      Yãngã añ fããmi ChatGPT pũngu loo yaaka yi Brave tãngbala (saango wã yi GPT-4!)
// @description:si      ඔබේ Brave සෙවුම් පිළිතුරුද ChatGPT එක් කිරීමෙන් එක්වන්න (GPT-4 විද්‍යාවලියේ සිදු වේ!)
// @description:sk      Pridáva odpovede ChatGPT do bočnej lišty Brave Search (poháňané GPT-4!)
// @description:sl      Dodaja odgovore ChatGPT na stransko vrstico iskanja Brave (poganja GPT-4!)
// @description:sm      Fa'afaigofie atu ai le tali a ChatGPT i le sidebar o le Search Brave (e avea i le GPT-4!)
// @description:sn      Anoratidza zita reChatGPT pa sidebar yeBrave Search (ine vedzere GPT-4!)
// @description:so      Wax ka dar ChatGPT jawaabaha sidebar Brave Search (u shaqeeya GPT-4!)
// @description:sr      Додаје одговоре ChatGPT-а на страни панел претраге Брејва (покреће ГПТ-4!)
// @description:sv      Lägger till ChatGPT-svar i sidofältet för Brave Sökning (drivet av GPT-4!)
// @description:sw      Inaongeza majibu ya ChatGPT kwenye upau wa pembeni wa Brave Search (inaendeshwa na GPT-4!)
// @description:ta      பேராசை ஆய்வுக் கேட்பதற்கு ChatGPT பதில்களை Brave பக்க பக்கில் சேர்க்கிறது (GPT-4 ஆல் இயங்குகிறது!)
// @description:te      Brave శోధనపై ChatGPT సమాధానాలను జోడిస్తుంది (GPT-4 ద్వారా పెంచబడింది!)
// @description:tg      Шарҳи ChatGPT-ро ба сатҳи барои ҷустуҷӯи Брейв илова мекунад (тавассути GPT-4 мубодиъ будааст!)
// @description:th      เพิ่มคำตอบของ ChatGPT ไปยังแถบข้างของการค้นหา Brave (ทำงานโดย GPT-4!)
// @description:ti      ክልል ጸጋን ChatGPT መልእኽት ንምስርሓብ Brave ውልቀት ይጨምሩ (በቀረበ GPT-4!)
// @description:tk      ChatGPT-nyň jogaplaryny Brave Gözleg çarpyşysyna goşýar (GPT-4-dan güýçlenýär!)
// @description:tn      Enza mavoti ChatGPT ku sidebar yeBrave Search (ine yeduva neGPT-4!)
// @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-4!)
// @description:tpi     Adim na pes bilong ChatGPT long tab bilong Brave Søk (wok long GPT-4!)
// @description:tr      ChatGPT yanıtlarını Brave Arama yan çubuğuna ekler (GPT-4 tarafından desteklenir!)
// @description:uk      Додає відповіді ChatGPT до бічної панелі пошуку Brave (працює на GPT-4!)
// @description:ur      ChatGPT جوابات کو بریو سرچ کے سائڈبار میں شامل کرتا ہے (GPT-4 کے ذریعے!)
// @description:uz      ChatGPT javoblarni Brave Qidiruvning yon paneliga qo'shadi (GPT-4 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-4!)
// @description:xh      Enza amaxwebhu kaChatGPT e sideba yeBrave Search (enokukhuthaziswa nguGPT-4!)
// @description:yi      צוגעבן אַנטוואָרטן פֿון ChatGPT אין די זײַטל-פֿעלד פֿון Brave זוך (געפּאַווערטעד דורך GPT-4!)
// @description:zh      将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4 提供支持!)
// @description:zh-CN   将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4 提供支持!)
// @description:zh-HK   將 ChatGPT 答案添加到 Brave Search 側邊欄 (由 GPT-4 提供支持!)
// @description:zh-SG   将 ChatGPT 答案添加到 Brave Search 侧边栏 (由 GPT-4 提供支持!)
// @description:zh-TW   將 ChatGPT 答案添加到 Brave Search 側邊欄 (由 GPT-4 提供支持!)
// @description:zu      Engeza amaswazi aseChatGPT emugqa wokuqala weBrave Search (ibhulohwe nguGPT-4!)
// @author              KudoAI
// @namespace           https://kudoai.com
// @version             2024.4.12.3
// @license             MIT
// @icon                https://media.bravegpt.com/images/icons/bravegpt/icon48.png
// @icon64              https://media.bravegpt.com/images/icons/bravegpt/icon64.png
// @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             raw.githubusercontent.com
// @connect             greasyfork.org
// @connect             chat.openai.com
// @connect             api.openai.com
// @connect             fanyi.sogou.com
// @connect             api.aigcfun.com
// @require             https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@2.6.8/dist/chatgpt.min.js#sha256-veeaKpmLF7CZ6kxNfn8SMjD36ijLffGCzel9G4ykpPI=
// @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.7/dist/contrib/auto-render.min.js#sha256-nLjaz8CGwpZsnsS6VPSi3EO3y+KzPOwaJ0PYhsf7R6c=
// @require             https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js#sha256-jjsBF/TfS+RSwLavW48KCs+dSt4j0I1V1+MSryIHd2I=
// @require             https://cdn.jsdelivr.net/npm/generate-ip@2.2.4/dist/generate-ip.min.js#sha256-FJO9oo6Fpy0nY5ak5NWIEg8BtX+As53nDJEYrODHRPM=
// @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://github.bravegpt.com/issues
// ==/UserScript==

// NOTE: This script relies on the powerful chatgpt.js library @ https://chatgpt.js.org © 2023–2024 KudoAI & contributors under the MIT license
// ...and KaTeX, the fastest math typesetting library @ https://katex.org © 2013–2020 Khan Academy & contributors under the MIT license

(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] + ' '
                       + ( messages.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' '
                       + state.separator + state.word[+!config.proxyAPIenabled]
        menuIDs.push(GM_registerMenuCommand(pamLabel, () => {
            saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
            notify(( messages.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] + ' '
                       + ( messages.menuLabel_autoGetAnswers || 'Auto-Get Answers' ) + ' '
                       + state.separator + state.word[+config.autoGetDisabled]
        menuIDs.push(GM_registerMenuCommand(agmLabel, () => {
            saveSetting('autoGetDisabled', !config.autoGetDisabled)
            notify(( messages.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] + ' '
                      + ( messages.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(( messages.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] + ' '
                      + ( messages.menuLabel_require || 'Require' ) + ' "/" '
                      + ( messages.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(( messages.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] + ' '
                      + ( messages.menuLabel_require || 'Require' ) + ' "?" '
                      + ( messages.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(( messages.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] + ' '
                           + ( messages.menuLabel_widerSidebar || 'Wider Sidebar' )
                           + state.separator + state.word[+!config.widerSidebar]
            menuIDs.push(GM_registerMenuCommand(wsbLabel, () => toggleSidebar('wider')))

            // Add command to toggle sticky sidebar
            const ssbLabel = state.symbol[+!config.stickySidebar] + ' '
                           + ( messages.menuLabel_stickySidebar || 'Sticky Sidebar' )
                           + state.separator + state.word[+!config.stickySidebar]
            menuIDs.push(GM_registerMenuCommand(ssbLabel, () => toggleSidebar('sticky')))
        }

        // Add command to set reply language
        const rlLabel = '🌐 ' + ( messages.menuLabel_replyLanguage || 'Reply Language' )
                      + state.separator + config.replyLanguage
        menuIDs.push(GM_registerMenuCommand(rlLabel, () => {
            while (true) {
                let replyLanguage = prompt(
                    ( messages.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(( messages.alert_langUpdated || 'Language updated' ) + '!', // title
                        `${ config.appName } ${ messages.alert_willReplyIn || 'will reply in' } `
                            + ( replyLanguage || messages.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 = `💡 ${ messages.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
            '🏷️ ' + ( messages.about_version || 'Version' ) + ': ' + GM_info.script.version + '\n'
                + '⚡ ' + ( messages.about_poweredBy || 'Powered by' ) + ': '
                    + '<a href="https://chatgpt.js.org" target="_blank" rel="noopener">chatgpt.js</a>'
                    + ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
                + '📜 ' + ( messages.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(( messages.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 = (
                '🚀 ' + ( messages.buttonLabel_updateCheck || 'Check for Updates' ))
            else if (/support/i.test(button.textContent)) button.textContent = (
                '🧠 ' + ( messages.buttonLabel_getSupport || 'Get Support' ))
            else if (/review/i.test(button.textContent)) button.textContent = (
                '⭐ ' + ( messages.buttonLabel_leaveReview || 'Leave a Review' ))
            else if (/apps/i.test(button.textContent)) button.textContent = (
                '🤖 ' + ( messages.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(( messages.alert_updateAvail || 'Update available' ) + '! 🚀', // title
                            `${ messages.alert_newerVer || 'An update to' } ${ config.appName } `
                                + `(v${ latestVer }) ${ messages.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') + '"  '
                                    + `>${ messages.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 = messages.buttonLabel_update || 'Update'
                            updateBtns[0].textContent = messages.buttonLabel_dismiss || 'Dismiss'
                        }

                        return
                }}

                // Alert to no update found, nav back
                alert(( messages.alert_upToDate || 'Up-to-date' ) + '!', // title
                    `${ config.appName } (v${ currentVer }) ${ messages.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://chat.openai.com', 'chat.openai.com'),
                ' (', messages.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() ; else updateSSBsvg()
        notify(( messages[`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 updateTweaksStyle() {
        const isStandbyMode = document.querySelector('.standby-btn'),
              answerIsLoaded = document.querySelector('.corner-btn')

        // Update tweaks style based on settings (for tweaks init + appShow() + toggleSidebar())
        tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
                              + ( config.stickySidebar && !isStandbyMode && answerIsLoaded ? ssbStyles : '' )

        // Update <pre> max-height in Sticky Sidebar mode based on RQ visibility (for getShowReply()'s RQ show + menu RQ toggle)
        const answerPre = document.querySelector('.bravegpt pre'),
              relatedQueries = document.querySelector('.related-queries'),
              shorterPreHeight = window.innerHeight - relatedQueries?.offsetHeight - 226,
              longerPreHeight = window.innerHeight - 200
        if (answerPre) answerPre.style.maxHeight = !config.stickySidebar ? 'none' : (
            relatedQueries?.offsetHeight > 0 ? `${ shorterPreHeight }px` : `${ longerPreHeight }px` )
    }

    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 updateSSBsvg() {

        // Init span/SVG/paths
        const ssbSpan = appDiv.querySelector('#ssb-btn'),
              ssbSVG = ssbSpan.querySelector('svg')
        const ssbONpaths = [
            createSVGpath({
                d: 'M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146z' }) ]
        const ssbOFFpaths = [
            createSVGpath({
                d: 'M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146zm.122 2.112v-.002.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a4.507 4.507 0 0 0-.288-.076 4.922 4.922 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a4.924 4.924 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034.114 0 .23-.011.343-.04L9.927 2.028c-.029.113-.04.23-.04.343a1.779 1.779 0 0 0 .062.46z' }) ]

        // Set SVG attributes
        for (const [attr, value] of [['width', 16], ['height', 16], ['viewBox', '0 0 16 16']])
            ssbSVG.setAttribute(attr, value)

        // Update SVG elements
        while (ssbSVG.firstChild) { ssbSVG.removeChild(ssbSVG.firstChild) }
        const ssbSVGpaths = config.stickySidebar ? ssbONpaths : ssbOFFpaths
        ssbSVGpaths.forEach(path => ssbSVG.append(path))
        if (!ssbSpan.contains(ssbSVG)) ssbSpan.append(ssbSVG)
    }

    function updateFooterContent() {
        fetchJSON('https://raw.githubusercontent.com/KudoAI/ads-library/main/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://raw.githubusercontent.com/KudoAI/ads-library/main/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', 'ssb', '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' ? messages.menuLabel_about || 'About'
          : buttonType == 'speak' ? messages.tooltip_playAnswer || 'Play answer'
          : buttonType == 'ssb' ? (( config.stickySidebar ? `${ messages.prefix_exit || 'Exit' } ` :  '' )
                                   + messages.menuLabel_stickySidebar || 'Sticky Sidebar' )
          : buttonType == 'wsb' ? (( config.widerSidebar ? `${ messages.prefix_exit || 'Exit' } ` :  '' )
                                   + messages.menuLabel_widerSidebar || 'Wider Sidebar' )
          : buttonType == 'send' ? messages.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 getAIGCFkey() {
        return new Promise(resolve => {
            const publicKey = GM_getValue(config.keyPrefix + '_aigcfKey')
            if (!publicKey) {
                GM.xmlHttpRequest({ method: 'GET', url: 'https://api.aigcfun.com/fc/key',
                    headers: {
                        'Content-Type': 'application/json',
                        'Referer': 'https://aigcfun.com/',
                        'X-Forwarded-For': ipv4.generate({ verbose: false }) },
                    onload: response => {
                        const newPublicKey = JSON.parse(response.responseText).data
                        if (!newPublicKey) { appError('Failed to get AIGCFun public key') ; return }
                        GM_setValue(config.keyPrefix + '_aigcfKey', newPublicKey)
                        console.info('AIGCFun public key set: ' + newPublicKey)
                        resolve(newPublicKey)
                    },
                    onerror: resolve('')
            })
            } else resolve(publicKey)
    })}

    async function refreshAIGCFendpoint() {
        GM_setValue(config.keyPrefix + '_aigcfKey', false) // clear GM key
        // Determine index of AIGCF in endpoint map
        let aigcfMapIndex = -1
        for (let i = 0 ; i < proxyEndpoints.length ; i++) {
            const endpoint = proxyEndpoints[i]
            if (endpoint.some(item => item.includes('aigcfun'))) {
                aigcfMapIndex = i ; break
        }}
        // Update AIGCF endpoint w/ fresh key (using fresh IP)
        proxyEndpoints[aigcfMapIndex][0] = (
            'https://api.aigcfun.com/api/v1/text?key=' + await getAIGCFkey())
    }

    // Define ANSWER functions

    let endpoint, 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)]
            endpoint = entry[0] ; accessKey = entry[1] ; model = entry[2]
        } else { // use OpenAI API
            endpoint = openAIendpoints.chat
            const timeoutPromise = new Promise((resolve, reject) =>
                setTimeout(() => reject(new Error('Timeout occurred')), 3000))
            accessKey = await Promise.race([getOpenAItoken(), timeoutPromise])
            if (!accessKey) { appAlert('login') ; return }
            model = 'gpt-3.5-turbo'
        }
    }

    function createHeaders(api) {
        let headers = { 'Content-Type': 'application/json' }
        if (api.includes('openai.com')) headers.Authorization = 'Bearer ' + accessKey
        return headers
    }

    function createPayload(api, msgs) {
        const payload = { messages: msgs, model: model }
        if (api.includes('openai.com')) payload.max_tokens = 4000
        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: 'POST', url: endpoint, responseType: 'text', headers: createHeaders(endpoint),
                data: createPayload(endpoint, [{ role: 'user', content: rqPrompt }]),
                onload: event => {
                    let str_relatedQueries = ''
                    if (!config.proxyAPIenabled && event.response) {
                        try { // to parse txt response from OpenAI API
                            str_relatedQueries = JSON.parse(event.response).choices[0].message.content
                        } catch (err) { appError(err) ; reject(err) }
                    } else if (config.proxyAPIenabled && event.responseText) {
                        try { // to parse txt response from proxy API
                            str_relatedQueries = JSON.parse(event.responseText).choices[0].message.content
                        } 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 rqEventHandler(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', rqEventHandler)
                relatedQueryDiv.removeEventListener('keydown', rqEventHandler)
            })
            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 }))
    }}}

    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: 'POST', 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, index) => {
                        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 = messages.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', rqEventHandler)
                            relatedQueryDiv.addEventListener('keydown', rqEventHandler)
                        }, index * 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
            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')) {
                    if (event.response) {
                        try { // to parse txt response from OpenAI endpoint
                            appShow(JSON.parse(event.response).choices[0].message.content, footerContent)
                        } catch (err) {
                            appError(appAlerts.parseFailed + ': ' + err)
                            appError('Response: ' + event.response)
                            appAlert('suggestProxy')
                        }
                    }
                } else if (endpoint.includes('aigcf')) {
                    if (event.responseText) {
                        try { // to parse txt response from AIGCF endpoint
                            const answer = JSON.parse(event.responseText).choices[0].message.content
                            appShow(answer, footerContent) ; getShowReply.triedEndpoints = [] ; getShowReply.attemptCnt = 0
                        } catch (err) {
                            appInfo('Response: ' + event.responseText)
                            if (event.responseText.includes('非常抱歉,根据我们的产品规则,无法为你提供该问题的回答'))
                                appAlert(messages.alert_censored || 'Sorry, according to our product rules, '
                                    + 'we cannot provide you with an answer to this question, please try other questions')
                            else if (event.responseText.includes('维护'))
                                appAlert(( messages.alert_maintenance || 'AI system under maintenance' ) + '. '
                                    + ( messages.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' ))
                            else if (event.responseText.includes('finish_reason')) { // if other AIGCF error encountered
                                await refreshAIGCFendpoint() ; getShowReply(convo, callback) // re-fetch related queries w/ fresh IP
                            } else { // use different endpoint or suggest OpenAI
                                appError(appAlerts.parseFailed + ': ' + err)
                                if (getShowReply.attemptCnt < proxyEndpoints.length) retryDiffHost()
                                else appAlert('suggestOpenAI')
        }}}}}}
    }

    function appShow(answer, footerContent) {
        while (appDiv.firstChild) // clear all children
            appDiv.removeChild(appDiv.firstChild)

        // Create/append '🤖 BraveGPT'
        if (!appLogoImg.loaded) { // create/append robot emoji for logo alt
            const appPrefixSpan = document.createElement('span')
            appPrefixSpan.innerText = '🤖 ' ; appPrefixSpan.classList.add('app-name', 'no-user-select')
            appDiv.append(appPrefixSpan)
        }
        const appLogoAnchor = createAnchor(config.appURL, appLogoImg)
        appLogoAnchor.classList.add('app-name', 'no-user-select') ; appLogoImg.width = 143
        if (!appLogoImg.loaded) appLogoImg.style.marginLeft = '3px' // pos logo alt
        appDiv.append(appLogoAnchor)

        // Create/append 'by KudoAI'
        const kudoAIspan = document.createElement('span')
        kudoAIspan.classList.add('kudo-ai', '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 Sticky Sidebar button
            var ssbSpan = document.createElement('span'),
                ssbSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
            ssbSpan.id = 'ssb-btn' // for updateSSBsvg() + toggleTooltip()
            ssbSpan.className = 'corner-btn' ; ssbSpan.style.margin = '0.01rem 6px 0 0'
            ssbSpan.append(ssbSVG) ; appDiv.append(ssbSpan) ; updateSSBsvg()

            // 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
                })}}
            })
        })
        ssbSVG?.addEventListener('click', () => toggleSidebar('sticky'))
        wsbSVG?.addEventListener('click', () => toggleSidebar('wider'))
        const buttonSpans = [aboutSpan, speakSpan, ssbSpan, 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 = messages.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' ; answerPre.textContent = answer
            appDiv.append(balloonTipSpan) ; appDiv.append(answerPre)
        }

        setTimeout(() => updateTweaksStyle(), 100) // in case sticky mode on

        // 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' ? messages.placeholder_askSomethingElse || 'Ask something else'
                                                         : messages.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 math
        if (answer != 'standby') {
            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)

        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', rqEventHandler)
                    relatedQueryDiv.removeEventListener('keydown', rqEventHandler)
                })
                relatedQueriesDiv.remove()
            } catch (err) {}

            // 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',
        userLanguage: chatgpt.getUserLanguage() }
    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', 'raw.githubusercontent.com') + '/main/'
    config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
    loadSetting('autoGetDisabled', 'prefixEnabled', 'proxyAPIenabled', 'replyLanguage',
                'rqDisabled', 'stickySidebar', '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
    const scheme = isDarkMode() ? 'dark' : 'light',
          isChromium = chatgpt.browser.isChromium(),
          isFirefox = chatgpt.browser.isFirefox(),
          isMobile = chatgpt.browser.isMobile()

    // Pre-load logo
    const appLogoImg = document.createElement('img')
          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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' )
    appLogoImg.alt = config.appName
    appLogoImg.onload = () => appLogoImg.loaded = true // for prefix visibility + img/alt pos in `appShow()`

    // 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(response) {
            try { // to return localized messages.json
                const messages = new Proxy(JSON.parse(response.responseText), {
                    get(target, prop) { // remove need to ref nested keys
                        if (typeof target[prop] == 'object' && target[prop] !== null && 'message' in target[prop]) {
                            return target[prop].message
                }}}) ; resolve(messages)
            } catch (err) { // if 404
                msgXHRtries++ ; if (msgXHRtries === 3) return // 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 messages = await msgsLoaded

    registerMenu()

    // Init endpoints
    const openAIendpoints = {
        auth: 'https://auth0.openai.com',
        session: 'https://chat.openai.com/api/auth/session',
        chat: 'https://api.openai.com/v1/chat/completions' }
    const proxyEndpoints = [[ 'https://api.aigcfun.com/api/v1/text?key=' + await getAIGCFkey(), '', 'gpt-3.5-turbo' ]]

    // Init alerts
    const appAlerts = {
        waitingResponse: ( messages.alert_waitingResponse || 'Waiting for ChatGPT response' ) + '...',
        login: ( messages.alert_login || 'Please login' ) + ' @ ',
        tooManyRequests: ( messages.alert_tooManyRequests || 'ChatGPT is flooded with too many requests' ) + '. '
            + ( config.proxyAPIenabled ? ( messages.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
                                       : ( messages.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' )),
        parseFailed: ( messages.alert_parseFailed || 'Failed to parse response JSON' ) + '. '
            + ( config.proxyAPIenabled ? ( messages.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
                                       : ( messages.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' )),
        checkCloudflare: ( messages.alert_checkCloudflare || 'Please pass Cloudflare security check' ) + ' @ ',
        suggestProxy: ( messages.alert_openAInotWorking || 'OpenAI API is not working' ) + '. '
            + ( messages.alert_suggestProxy || 'Try switching on Proxy Mode in toolbar' ),
        suggestOpenAI: ( messages.alert_proxyNotWorking || 'Proxy API is not working' ) + '. '
            + ( messages.alert_suggestOpenAI || 'Try switching off Proxy Mode in toolbar' )
    }

    // Stylize elements
    const appStyle = document.createElement('style')
    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 ; margin-top: .7em ; border-radius: 13px ; overflow: auto ;'
            + ( scheme == 'dark' ? 'background: #3a3a3a ; color: #f2f2f2 } ' : ' background: #eaeaea ; color: #282828 }' )
        + `.bravegpt footer { margin: ${ isChromium ? 27 : 32 }px 0 -26px 0 ; border-top: none !important }`
        + '.bravegpt .feedback {'
            + 'float: right ; font-family: var(--brand-font) ; font-size: .55rem ;'
            + 'letter-spacing: .02em ; position: relative ; right: -18px ; bottom: 15px ;'
            + `color: ${ scheme == 'dark' ? '#aaa' : 'var(--search-text-06)' }}`
        + '.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 ;'
            + ( isChromium ? 'top: 0.16em ; right: 5rem;' : 'top: 0.25em ; right: 10.03rem ;' )
            + '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
            + ( isChromium ? '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' } }`
        + '.kudo-ai { margin-left: 7px ; font-size: .65rem ; color: #aaa }'
        + '.kudo-ai a { color: #aaa ; text-decoration: none }'
        + `.kudo-ai a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
        + '.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
    )
    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 }',
          ssbStyles = '.bravegpt { position: sticky ; top: 14px }'
                    + '.bravegpt ~ * { display: none }' // hide sidebar contents
                    + 'main > div:first-of-type { overflow: visible !important }'
    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')
    hostContainer.style.overflow = hostContainer.parentNode.style.overflow = 'visible' // for boundless hover fx
    setTimeout(() => {
        hostContainer.prepend(appDiv)
        setTimeout(() => appDiv.classList.add('active'), 100) // fade in
    }, isMobile ? 500 : 100)

    // Init footer CTA to share feedback
    let footerContent = createAnchor(config.feedbackURL, messages.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)
    }

})()