// ==UserScript==
// @name BraveGPT 🤖
// @description Adds AI answers to Brave Search (powered by GPT-4o!)
// @description:af Voeg AI-antwoorde by Brave Search (aangedryf deur GPT-4o!)
// @description:am የ Brave Search ውስጥ AI መልቀቅን አድርግ፣ (GPT-4o በመሣሪያዎቹ ውስጥ!)
// @description:ar يضيف إجابات AI إلى Brave Search (مدعوم بواسطة GPT-4o!)
// @description:as Brave Search-লৈ AI উত্তৰ যোগ দিয়ে (GPT-4o দ্বাৰা পাওৱা হৈছে!)
// @description:az Brave Search-ya AI cavablarını əlavə edir (GPT-4o tərəfindən dəstəklənir!)
// @description:be Дадае ІА адказы на Brave Search (падтрымліваецца GPT-4o!)
// @description:bg Добавя ИИ отговори в Brave Search (поддържан от GPT-4o!)
// @description:bn Brave Search-ত AI উত্তর যোগ করে (GPT-4o দ্বারা প্রচালিত!)
// @description:bs Dodaje AI odgovore na Brave Search (pokreće GPT-4o!)
// @description:ca Afegeix respostes d'IA a Brave Search (impulsat per GPT-4o!)
// @description:ceb Nagdugang ug mga tubag AI ngadto sa Brave Search (gipadagan sa GPT-4o!)
// @description:co Aggiunge risposte AI a Brave Search (supportate da GPT-4o!)
// @description:cs Přidává AI odpovědi do Brave Search (poháněno GPT-4o!)
// @description:cy Ychwanegu atebion AI i Brave Search (a yrrir gan GPT-4o!)
// @description:da Tilføjer AI-svar til Brave Search (drevet af GPT-4o!)
// @description:de Fügt AI-Antworten zu Brave Search hinzu (betrieben von GPT-4o!)
// @description:el Προσθέτει απαντήσεις AI στο Brave Search (τροφοδοτούμενο από GPT-4o!)
// @description:en Adds AI answers to Brave Search (powered by GPT-4o!)
// @description:eo Aldonas AI-respondojn al Brave Search (ebligita de GPT-4o!)
// @description:es Añade respuestas de IA a Brave Search (impulsado por GPT-4o!)
// @description:et Lisab AI-vastused Brave Search'le (juhitud GPT-4o-ga!)
// @description:eu Gehitu IA erantzunak Brave Search-n (GPT-4o-k bultzatuta!)
// @description:fa پاسخهای هوشمصنوعی به Brave Search اضافه میشود (توسط GPT-4o پشتیبانی میشود!)
// @description:fi Lisää tekoälyvastauksia Brave Search:hun (ohjattu GPT-4o:lla!)
// @description:fil Nagdaragdag ng mga sagot ng AI sa Brave Search (pinapagana ng GPT-4o!)
// @description:fo Bætir AI svar við Brave Search (drifin af GPT-4o!)
// @description:fr Ajoute des réponses IA à Brave Search (propulsé par GPT-4o!)
// @description:fr-CA Ajoute des réponses IA à Brave Search (propulsé par GPT-4o!)
// @description:fy Foeget AI-antwurden ta oan Brave Search (dreaun troch GPT-4o!)
// @description:ga Cuirtear freagraí AI le Brave Search (dírítear ag GPT-4o!)
// @description:gd Cur freagairtichean AI ris an Brave Search (air a thug seachad le GPT-4o!)
// @description:gl Engade respostas de IA a Brave Search (impulsado por GPT-4o!)
// @description:gu Brave Search માટે AI જવાબો ઉમેરે છે (GPT-4o દ્વારા પોવરેડ!)
// @description:ha Ƙaddara takardun AI zu Brave Search (da aka fi GPT-4o!)
// @description:haw Hoʻohui aku i nā hoʻopiʻi AI iā Brave Search (hoʻohui ʻia e GPT-4o!)
// @description:he מוסיף תשובות AI ל-Brave Search (מופעל על ידי GPT-4o!)
// @description:hi Brave Search में AI उत्तर जोड़ता है (GPT-4o द्वारा संचालित!)
// @description:hmn Ntxig AI nruab nruab rau Brave Search (pab cuam GPT-4o!)
// @description:hr Dodaje AI odgovore na Brave Search (pokreće GPT-4o!)
// @description:ht Ajoute repons AI nan Brave Search (pòte pa GPT-4o!)
// @description:hu AI válaszokat ad hozzá a Brave Search-hoz (GPT-4o által hajtva!)
// @description:hy Ավելացնում է AI պատասխաններ Brave Search-ում (աջակցված է GPT-4o-ով!)
// @description:ia Adde responas AI a Brave Search (propulsate per GPT-4o!)
// @description:id Menambahkan jawaban AI ke Brave Search (didukung oleh GPT-4o!)
// @description:ig Tinye ihe ndekọ AI n'ụzọ ọgụgụ Brave Search (n'efu na GPT-4o!)
// @description:ii Brave Search ᐸᔦᒪᔪᐃᓃᑦ AI ᓇᑕᐅᒪᐃᑦᓯ (GPT-4o ᓂᑕᔪᑦᓯᐏᑦᑕᒥᔭ!)
// @description:is Bætir AI svar við Brave Search (keyrir á GPT-4o!)
// @description:it Aggiunge risposte AI a Brave Search (alimentato da GPT-4o!)
// @description:iu Brave Search ᑲᑎᒪᔪᖅᑐᖅᑐᐃᓐᓇᓂᒃ AI ᑎᑎᕋᖃᕐᓯᒪᓂᖏᓐ (GPT-4o ᑐᑭᒧᑦᑖᑦ!)
// @description:ja Brave Search に AI 回答を追加します (GPT-4o で動作!)
// @description:jv Nambéhi pirangga AI nganti Brave Search (diduweni déning GPT-4o!)
// @description:ka ამატებს AI პასუხებს Brave Search-ს (იმართება GPT-4o!)
// @description:kk Brave Search-ға AI жауаптарын қосады (GPT-4o арқылы жұмыс істейді!)
// @description:kl Brave Search-mi AI-t Kalaallit Nunaanni iluani (GPT-4o! -nip ilaanni!)
// @description:km បន្ថែមចម្លើយ AI ទៅ Brave Search (ដំណើរការដោយ GPT-4o!)
// @description:kn Brave Search ಗೆ AI ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (GPT-4o ನಿಂದ ನಡೆಸಲ್ಪಡುತ್ತಿದೆ!)
// @description:ko Brave Search에 AI 답변을 추가합니다(GPT-4o 제공!)
// @description:ku Bersivên AI-ê li Brave Search zêde dike (ji hêla GPT-4o ve hatî hêzdar kirin!)
// @description:ky Brave Search'го AI жоопторун кошот (GPT-4o тарабынан иштейт!)
// @description:la Addit AI responsa Brave Search (powered per GPT-4o!)
// @description:lb Füügt AI Äntwerten op Brave Search (ugedriwwen duerch GPT-4o!)
// @description:lg Yambula emisomo ey'ensobi ku Brave Search (enkuuma GPT-4o!)
// @description:ln Ebakisi biyano ya AI na Brave Search (ezali na nguya ya GPT-4o!)
// @description:lo ເພີ່ມຄໍາຕອບ AI ໃຫ້ກັບ Brave Search (ຂັບເຄື່ອນໂດຍ GPT-4o!)
// @description:lt Prideda AI atsakymus į „Brave Search“ (maitina GPT-4o!)
// @description:lv Pievieno AI atbildes Brave Search (darbina GPT-4o!)
// @description:mg Manampy valiny AI amin'ny Brave Search (nampiasain'ny GPT-4o!)
// @description:mi Ka taapirihia nga whakautu AI ki a Brave Search (whakamahia e GPT-4o!)
// @description:mk Додава одговори со вештачка интелигенција на Brave Search (напојувано од GPT-4o!)
// @description:ml Brave Search-യിലേക്ക് AI ഉത്തരങ്ങൾ ചേർക്കുന്നു (GPT-4o നൽകുന്നതാണ്!)
// @description:mn Brave Search-д AI хариултуудыг нэмдэг (GPT-4o-оор ажилладаг!)
// @description:mr Brave Search ला AI उत्तरे जोडते (GPT-4o द्वारे समर्थित!)
// @description:ms Menambahkan jawapan AI pada Brave Search (dikuasakan oleh GPT-4o!)
// @description:mt Iżżid it-tweġibiet AI għal Brave Search (mħaddma minn GPT-4o!)
// @description:my Brave Search (GPT-4o ဖြင့် စွမ်းဆောင်ထားသည့်) တွင် AI အဖြေများကို ပေါင်းထည့်သည်
// @description:na Aeta AI teroma i Brave Search (ira GPT-4o reke akea!)
// @description:nb Legger til AI-svar på Brave Search (drevet av GPT-4o!)
// @description:nd Iyatholakala amaswelelo e-AI kuBrave Search (kuyatholakala ngokulawula uGPT-4o!)
// @description:ne Brave Search मा AI जवाफहरू थप्छ (GPT-4o द्वारा संचालित!)
// @description:ng Ondjova mbelelo dha AI moBrave Search (uumbuli nguGPT-4o!)
// @description:nl Voegt AI-antwoorden toe aan Brave Search (mogelijk gemaakt door GPT-4o!)
// @description:nn Legg til AI-svar på Brave Search (drevet av GPT-4o!)
// @description:no Legger til AI-svar til Brave Search (drevet av GPT-4o!)
// @description:nso Ya go etela ditshenyegi tsa AI mo Brave Search (e dirwang ke GPT-4o!)
// @description:ny Imawonjezera mayankho a AI ku Brave Search (yoyendetsedwa ndi GPT-4o!)
// @description:oc Ajusta de respòstas d'IA a Brave Search (amb GPT-4o!)
// @description:om Deebii AI Brave Search (GPT-4o'n kan hojjetu!) irratti dabalata.
// @description:or Brave Search କୁ AI ଉତ୍ତର ଯୋଗ କରେ (GPT-4o ଦ୍ୱାରା ଚାଳିତ!)
// @description:pa Brave Search (GPT-4o ਦੁਆਰਾ ਸੰਚਾਲਿਤ!) ਵਿੱਚ AI ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ
// @description:pl Dodaje odpowiedzi AI do Brave Search (obsługiwane przez GPT-4o!)
// @description:ps Brave Search ته د AI ځوابونه اضافه کوي (د GPT-4o لخوا پرمخ وړل کیږي!)
// @description:pt Adiciona respostas de IA ao Brave Search (desenvolvido por GPT-4o!)
// @description:pt-BR Adiciona respostas de IA ao Brave Search (desenvolvido por GPT-4o!)
// @description:qu Brave Search (GPT-4o nisqawan kallpachasqa!) nisqaman AI kutichiykunata yapan.
// @description:rm Agiuntescha respostas d'IA a Brave Search (propulsà da GPT-4o!)
// @description:rn Abafasha inyandiko z'IA ku Brave Search (yashyizweho na GPT-4o!)
// @description:ro Adaugă răspunsuri AI la Brave Search (alimentat de GPT-4o!)
// @description:ru Добавляет ответы ИИ в Brave Search (на базе GPT-4o!)
// @description:rw Ongeraho ibisubizo bya AI kuri Brave Search (ikoreshwa na GPT-4o!)
// @description:sa Brave Search (GPT-4o द्वारा संचालितम्!) इत्यत्र AI उत्तराणि योजयति ।
// @description:sat Brave Search ar AI jawab khon ojantok (GPT-4o! sebadha manju)
// @description:sc Agiungit rispostas de IA a Brave Search (motorizadu da GPT-4o!)
// @description:sd شامل ڪري ٿو AI جوابن کي Brave Search (GPT-4o پاران طاقتور!)
// @description:se Lávdegáhtii AI vástid Brave Search (GPT-4o! vuosttas!)
// @description:sg Nâ tî-kûzâ mái vêdáara AI mbi Brave Search (ngâ GPT-4o!)
// @description:si Brave Search වෙත AI පිළිතුරු එක් කරයි (GPT-4o මගින් බලගන්වයි!)
// @description:sk Pridáva odpovede AI do Brave Search (poháňané GPT-4o!)
// @description:sl Dodaja odgovore AI v Brave Search (poganja GPT-4o!)
// @description:sm Faʻaopoopo tali AI ile Brave Search (faʻamalosia e GPT-4o!)
// @description:sn Inowedzera mhinduro dzeAI kuBrave Search (inofambiswa neGPT-4o!)
// @description:so Waxay ku dartay jawaabaha AI Brave Search (waxaa ku shaqeeya GPT-4o!)
// @description:sq Shton përgjigjet e AI në Brave Search (mundësuar nga GPT-4o!)
// @description:sr Додаје АИ одговоре у Brave Search (покреће ГПТ-4о!)
// @description:ss Iphendvulela izindlela zezilungiselelo ku-Brave Search (izenzakalo nge-GPT-4o!)
// @description:st E kopanetse diqoqo tsa AI ka Brave Search (ka sebelisoa ke GPT-4o!)
// @description:su Nambahkeun jawaban AI kana Brave Search (dikuatkeun ku GPT-4o!)
// @description:sv Lägger till AI-svar till Brave Search (driven av GPT-4o!)
// @description:sw Inaongeza majibu ya AI kwa Brave Search (inaendeshwa na GPT-4o!)
// @description:ta Brave Search க்கு AI பதில்களைச் சேர்க்கிறது (GPT-4o மூலம் இயக்கப்படுகிறது!)
// @description:te Brave Searchకి AI సమాధానాలను జోడిస్తుంది (GPT-4o ద్వారా ఆధారితం!)
// @description:tg Ба Brave Search ҷавобҳои AI илова мекунад (аз ҷониби GPT-4o!)
// @description:th เพิ่มคำตอบ AI ให้กับ Brave Search (ขับเคลื่อนโดย GPT-4o!)
// @description:ti ናብ Brave Search (ብGPT-4o ዝሰርሕ!) ናይ AI መልስታት ይውስኸሉ።
// @description:tk Brave Search-a AI jogaplaryny goşýar (GPT-4o bilen işleýär!)
// @description:tl Nagdadagdag ng mga sagot ng AI sa Brave Search (pinapatakbo ng GPT-4o!)
// @description:tn O amogela dipotso tsa AI mo Brave Search (e a nang le GPT-4o!)
// @description:to Tambisa mabizo a AI ku Brave Search (mukutenga na GPT-4o!)
// @description:tr Brave Search'ya yapay zeka yanıtları ekler (GPT-4o tarafından desteklenmektedir!)
// @description:ts Ku engetela tinhlamulo ta AI eka Brave Search (leyi fambiwaka hi GPT-4o!)
// @description:tt Brave Search'ка AI җаваплары өсти (GPT-4o белән эшләнгән!)
// @description:tw Ɔde AI mmuae ka Brave Search (a GPT-4o na ɛma ahoɔden!) ho.
// @description:ug Brave Search ۋەبسېتكە AI جاۋابلار قوشۇدۇ (GPT-4o تەكشۈرگۈچى بىلەن!)
// @description:uk Додає відповіді штучного інтелекту в Brave Search (на базі GPT-4o!)
// @description:ur Brave Search میں AI جوابات شامل کرتا ہے (GPT-4o کے ذریعے تقویت یافتہ!)
// @description:uz Brave Search-ga AI javoblarini qo'shadi (GPT-4o tomonidan quvvatlanadi!)
// @description:vi Thêm câu trả lời AI vào Brave Search (được cung cấp bởi GPT-4o!)
// @description:xh Yongeza iimpendulo ze-AI kwi-Brave Search (ixhaswe yi-GPT-4o!)
// @description:yi לייגט אַי ענטפֿערס צו Brave Search (Powered דורך GPT-4o!)
// @description:yo Ṣe afikun awọn idahun AI si Brave Search (agbara nipasẹ GPT-4o!)
// @description:zh 为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-CN 为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-HK 為 Brave Search 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zh-SG 为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-TW 為 Brave Search 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zu Yengeza izimpendulo ze-AI ku-Brave Search (inikwa amandla yi-GPT-4o!)
// @author KudoAI
// @namespace https://kudoai.com
// @version 2024.7.1.13
// @license MIT
// @icon https://media.bravegpt.com/images/icons/bravegpt/icon48.png?0a9e287
// @icon64 https://media.bravegpt.com/images/icons/bravegpt/icon64.png?0a9e287
// @compatible chrome except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible firefox
// @compatible edge except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible opera after allowing userscript manager access to search page results in opera://extensions
// @compatible brave except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible vivaldi
// @compatible waterfox
// @compatible librewolf
// @compatible ghost
// @compatible qq
// @compatible whale
// @compatible kiwi
// @compatible mask
// @compatible orion
// @match *://search.brave.com/search*
// @include https://auth0.openai.com
// @connect binjie.fun
// @connect chatgpt.com
// @connect gptforlove.com
// @connect greasyfork.org
// @connect jsdelivr.net
// @connect mixerbox.com
// @connect openai.com
// @connect sogou.com
// @require https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@2.9.3/dist/chatgpt.min.js#sha256-EDN+mCc+0Y4YVzJEoNikd4/rAIaJDLAdb+erWvupXTM=
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require https://cdn.jsdelivr.net/npm/generate-ip@2.4.2/dist/generate-ip.min.js#sha256-PRvQIDVWK/a+aAqEFVQv7RePbRe/tX6tWQVM80rAe2M=
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha256-g3pvpbDHNrUrveKythkPMF2j/J7UFoHbUyFQcFe1yEY=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @resource hljsCSS https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css#sha256-v0N76BFFkH0dCB8bUr4cHSVN8A/zCaOopMuSmJWV/5w=
// @resource bsbgCSS https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@d7fd458/styles/css/black-rising-stars.min.css#sha256-bXbVZUD7ciKqK0wU/BLQzh08JwkoNExHHqXITugd/3o=
// @resource wsbgCSS https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@d7fd458/styles/css/white-rising-stars.min.css#sha256-ya9newifevSPO1Q4AzMf42yAF6TE+iZHrDbVj0HyuEM=
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_cookie
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getResourceText
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @noframes
// @homepageURL https://www.bravegpt.com
// @supportURL https://support.bravegpt.com
// @contributionURL https://github.com/sponsors/KudoAI
// ==/UserScript==
// Dependencies:
// ✓ chatgpt.js (https://chatgpt.js.org) © 2023–2024 KudoAI & contributors under the MIT license
// ✓ generate-ip (https://generate-ip.org) © 2024 Adam Lui & contributors under the MIT license
// ✓ highlight.js (https://highlightjs.org) © 2006 Ivan Sagalaev under the BSD 3-Clause license
// ✓ KaTeX (https://katex.org) © 2013–2020 Khan Academy & other contributors under the MIT license
// ✓ Marked (https://marked.js.org) © 2018+ MarkedJS © 2011–2018 Christopher Jeffrey under the MIT license
// Documentation: https://docs.bravegpt.com
setTimeout(async () => {
// Init BROWSER FLAGS
const isChrome = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Chrome'),
isFirefox = chatgpt.browser.isFirefox(),
isEdge = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Edge'),
isBrave = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Brave'),
isMobile = chatgpt.browser.isMobile()
// Init CONFIG
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',
minFontSize: 13, maxFontSize: 24, lineHeightRatio: 1.313,
latestAssetCommitHash: '32b262c' } // for cached messages.json
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.assetHostURL = config.gitHubURL.replace('github.com', 'cdn.jsdelivr.net/gh') + `@${config.latestAssetCommitHash}/`
config.userLanguage = chatgpt.getUserLanguage()
config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
loadSetting('autoGetDisabled', 'autoFocusChatbarDisabled', 'autoScroll', 'bgAnimationsDisabled', 'fgAnimationsDisabled',
'fontSize', 'prefixEnabled', 'proxyAPIenabled', 'replyLanguage', 'rqDisabled', 'scheme',
'streamingDisabled', 'suffixEnabled', 'widerSidebar')
if (!config.replyLanguage) saveSetting('replyLanguage', config.userLanguage) // init reply language if unset
if (!config.fontSize) saveSetting('fontSize', 16) // init reply font size if unset
if ( // disable streaming in unspported envs
!/Tampermonkey|ScriptCat/.test(getUserscriptManager()) // unsupported userscript manager
|| getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave) // TM in browser that triggers STATUS_ACCESS_VIOLATION
) saveSetting('streamingDisabled', true)
// Init FETCHER
const xhr = getUserscriptManager() == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest
// Init API props
const openAIendpoints = { auth: 'https://auth0.openai.com', session: 'https://chatgpt.com/api/auth/session' }
const apis = {
'AIchatOS': {
endpoint: 'https://api.binjie.fun/api/generateStream', expectedOrigin: 'https://chat18.aichatos8.com',
method: 'POST', streamable: true, accumulatesText: false, failFlags: ['很抱歉地', '系统公告'] },
'GPTforLove': {
endpoint: 'https://api11.gptforlove.com/chat-process', expectedOrigin: 'https://ai27.gptforlove.com',
method: 'POST', streamable: true, accumulatesText: true },
'MixerBox AI': {
endpoint: 'https://chatai.mixerbox.com/api/chat/stream', expectedOrigin: 'https://chatai.mixerbox.com',
method: 'POST', streamable: true, accumulatesText: false },
'OpenAI': {
endpoint: 'https://api.openai.com/v1/chat/completions', expectedOrigin: 'https://chatgpt.com',
method: 'POST', streamable: true }
}
const apiIDs = { gptForLove: { parentID: '' }, aiChatOS: { userID: '#/chat/' + Date.now() }}
// Init INPUT EVENTS
const inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
inputEvents[action] = ( window.PointerEvent ? 'pointer' : isMobile ? 'touch' : 'mouse' ) + action)
// Init MESSAGES
let msgs = {}
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
xhr({ method: 'GET', url: msgHref, onload: onLoad })
function onLoad(resp) {
try { // to return localized messages.json
const msgs = JSON.parse(resp.responseText), flatMsgs = {}
for (const key in msgs) // remove need to ref nested keys
if (typeof msgs[key] == 'object' && 'message' in msgs[key])
flatMsgs[key] = msgs[key].message
resolve(flatMsgs)
} catch (err) { // if bad response
msgXHRtries++ ; if (msgXHRtries == 3) return resolve({}) // try up to 3X (original/region-stripped/EN) only
msgHref = config.userLanguage.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
msgHref.replace(/([^_]+_[^_]+)_[^/]*(\/.*)/, '$1$2') // ...strip region before retrying
: ( msgHostDir + 'en/messages.json' ) // else use default English messages
xhr({ method: 'GET', url: msgHref, onload: onLoad })
}
}
}) ; if (!config.userLanguage.startsWith('en')) try { msgs = await msgsLoaded } catch (err) {}
// Init SETTINGS props
const settingsProps = {
proxyAPIenabled: { type: 'toggle',
label: msgs.menuLabel_proxyAPImode || 'Proxy API Mode',
helptip: msgs.helptip_proxyAPImode || 'Uses a Proxy API for no-login access to AI' },
streamingDisabled: { type: 'toggle',
label: msgs.mode_streaming || 'Streaming Mode',
helptip: msgs.helptip_streamingMode || 'Receive replies in a continuous text stream' },
autoGetDisabled: { type: 'toggle',
label: msgs.menuLabel_autoGetAnswers || 'Auto-Get Answers',
helptip: msgs.helptip_autoGetAnswers || 'Auto-send queries to BraveGPT when using search engine' },
autoFocusChatbarDisabled: { type: 'toggle', mobile: false,
label: msgs.menuLabel_autoFocusChatbar || 'Auto-Focus Chatbar',
helptip: msgs.helptip_autoFocusChatbar || 'Auto-focus chatbar whenever it appears' },
autoScroll: { type: 'toggle', mobile: false,
label: `${ msgs.mode_autoScroll || 'Auto-Scroll' } (${ msgs.menuLabel_whenStreaming || 'when streaming' })`,
helptip: msgs.helptip_autoScroll || 'Auto-scroll responses as they generate in Streaming Mode' },
rqDisabled: { type: 'toggle',
label: `${ msgs.menuLabel_show || 'Show' } ${ msgs.menuLabel_relatedQueries || 'Related Queries' }`,
helptip: msgs.helptip_showRelatedQueries || 'Show related queries below chatbar' },
prefixEnabled: { type: 'toggle',
label: `${ msgs.menuLabel_require || 'Require' } "/" ${ msgs.menuLabel_beforeQuery || 'before query' }`,
helptip: msgs.helptip_prefixMode || 'Require "/" before queries for answers to show' },
suffixEnabled: { type: 'toggle',
label: `${ msgs.menuLabel_require || 'Require' } "?" ${ msgs.menuLabel_afterQuery || 'after query' }`,
helptip: msgs.helptip_suffixMode || 'Require "?" after queries for answers to show' },
widerSidebar: { type: 'toggle', mobile: false, centered: false,
label: msgs.menuLabel_widerSidebar || 'Wider Sidebar',
helptip: msgs.helptip_widerSidebar || 'Horizontally expand search page sidebar' },
bgAnimationsDisabled: { type: 'toggle',
label: msgs.menuLabel_bgAnimations || 'Background Animations',
helptip: msgs.helptip_bgAnimations || 'Show animated backgrounds in UI components' },
fgAnimationsDisabled: { type: 'toggle',
label: msgs.menuLabel_fgAnimations || 'Foreground Animations',
helptip: msgs.helptip_fgAnimations || 'Show foreground animations in UI components' },
replyLanguage: { type: 'prompt',
label: msgs.menuLabel_replyLanguage || 'Reply Language',
helptip: msgs.helptip_replyLanguage || 'Language for BraveGPT to reply in' },
scheme: { type: 'modal',
label: msgs.menuLabel_colorScheme || 'Color Scheme',
helptip: msgs.helptip_colorScheme || 'Scheme to display BraveGPT UI components in' },
about: { type: 'modal',
label: `${ msgs.menuLabel_about || 'About' } ${config.appName}...` }
}
// Init MENU objs
const menuIDs = [] // to store registered cmds for removal while preserving order
const menuState = {
symbol: ['❌', '✔️'], separator: getUserscriptManager() == 'Tampermonkey' ? ' — ' : ': ',
word: [(msgs.state_off || 'Off').toUpperCase(), (msgs.state_on || 'On').toUpperCase()]
}
// 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 pmLabel = menuState.symbol[+config.proxyAPIenabled] + ' '
+ settingsProps.proxyAPIenabled.label + ' '
+ menuState.separator + menuState.word[+config.proxyAPIenabled]
menuIDs.push(GM_registerMenuCommand(pmLabel, toggle.proxyMode))
// Add command to launch About modal
const aboutLabel = `💡 ${settingsProps.about.label}`
menuIDs.push(GM_registerMenuCommand(aboutLabel, modals.about.show))
// Add command to launch Settings modal
const settingsLabel = `⚙️ ${ msgs.menuLabel_settings || 'Settings' }`
menuIDs.push(GM_registerMenuCommand(settingsLabel, modals.settings.show))
}
function promptReplyLang() {
while (true) {
let replyLanguage = prompt(
( msgs.prompt_updateReplyLang || 'Update reply language' ) + ':', config.replyLanguage)
if (replyLanguage == null) break // user cancelled so do nothing
else if (!/\d/.test(replyLanguage)) {
replyLanguage = ( // auto-case for menu/alert aesthetics
[2, 3].includes(replyLanguage.length) || replyLanguage.includes('-') ? replyLanguage.toUpperCase()
: replyLanguage.charAt(0).toUpperCase() + replyLanguage.slice(1).toLowerCase() )
saveSetting('replyLanguage', replyLanguage || config.userLanguage)
siteAlert(( msgs.alert_langUpdated || 'Language updated' ) + '!', // title
`${ config.appName } ${ msgs.alert_willReplyIn || 'will reply in' } `
+ ( replyLanguage || msgs.alert_yourSysLang || 'your system language' ) + '.',
'', '', 447) // confirmation width
if (modals.settings.get()) // update settings menu status label
document.querySelector('#replyLanguage-menu-entry span').textContent = replyLanguage
break
}}}
function refreshMenu() {
if (getUserscriptManager() == 'OrangeMonkey') return
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu()
}
function updateCheck() {
// Fetch latest meta
const currentVer = GM_info.script.version
xhr({
method: 'GET', url: config.updateURL + '?t=' + Date.now(),
headers: { 'Cache-Control': 'no-cache' },
onload: resp => { const updateAlertWidth = 489
// Compare versions
const latestVer = /@version +(.*)/.exec(resp.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 updateModalID = siteAlert(( msgs.alert_updateAvail || 'Update available' ) + '! 🚀', // title
`${ msgs.alert_newerVer || 'An update to' } ${ config.appName } `
+ `(v${ latestVer }) ${ msgs.alert_isAvail || 'is available' }! `
+ '<a target="_blank" rel="noopener" style="font-size: 0.93rem" '
+ 'href="' + config.gitHubURL + '/commits/main/greasemonkey/'
+ config.updateURL.replace(/.*\/(.*)meta\.js/, '$1user.js') + '"'
+ `>${ msgs.link_viewChanges || 'View changes' }</a>`,
function update() { // button
safeWindowOpen(config.updateURL.replace('meta.js', 'user.js') + '?t=' + Date.now())
}, '', updateAlertWidth
)
// Localize button labels if needed
if (!config.userLanguage.startsWith('en')) {
const updateAlert = document.querySelector(`[id="${ updateModalID }"]`),
updateBtns = updateAlert.querySelectorAll('button')
updateBtns[1].textContent = msgs.buttonLabel_update || 'Update'
updateBtns[0].textContent = msgs.buttonLabel_dismiss || 'Dismiss'
}
return
}}
// Alert to no update found, nav back
siteAlert(( msgs.alert_upToDate || 'Up-to-date' ) + '!', // title
`${ config.appName } (v${ currentVer }) ${ msgs.alert_isUpToDate || 'is up-to-date' }!`, // msg
'', '', updateAlertWidth)
modals.about.show()
}})}
// Define FEEDBACK functions
function notify(msg, position = '', notifDuration = '', shadow = '') {
chatgpt.notify(`${ config.appSymbol } ${ msg }`, position, notifDuration,
shadow || scheme == 'dark' ? '' : 'shadow' )
}
function siteAlert(title = '', msg = '', btns = '', checkbox = '', width = '') {
return chatgpt.alert(`${ config.appSymbol } ${ title }`, msg, btns, checkbox, width)}
function appAlert(...alerts) {
alerts = alerts.flat() // flatten array args nested by spread operator
while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear appDiv content
const alertP = document.createElement('p') ; alertP.id = 'bravegpt-alert'
alertP.className = 'no-user-select' ; alertP.style.marginBottom = '-22px'
alerts.forEach((alert, idx) => { // process each alert for display
let msg = appAlerts[alert] || alert // use string verbatim if not found in appAlerts
if (idx > 0) msg = ' ' + msg // left-pad 2nd+ alerts
if (msg.includes(appAlerts.login)) deleteOpenAIcookies()
if (msg.includes(appAlerts.waitingResponse)) alertP.classList.add('loading')
// Add login link to login msgs
if (msg.includes('@'))
msg += '<a class="alert-link" target="_blank" rel="noopener" href="https://chatgpt.com">chatgpt.com</a>,'
+ ` ${ msgs.alert_thenRefreshPage || 'then refresh this page' }.`
+ ` (${ msgs.alert_ifIssuePersists || 'If issue persists' },`
+ ` ${( msgs.alert_try || 'Try' ).toLowerCase() }`
+ ` ${ msgs.alert_switchingOn || 'switching on' }`
+ ` ${ msgs.mode_proxy || 'Proxy Mode' })`
// Hyperlink msgs.alert_switching<On|Off>
const foundState = ['On', 'Off'].find(state =>
msg.includes(msgs['alert_switching' + state]) || new RegExp(`\\b${state}\\b`, 'i').test(msg))
if (foundState) { // hyperlink switch phrase for click listener to toggle.proxyMode()
const switchPhrase = msgs['alert_switching' + foundState] || 'switching ' + foundState.toLowerCase()
msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
}
// Create/fill/append msg span
const msgSpan = document.createElement('span')
msgSpan.innerHTML = msg ; alertP.append(msgSpan)
// Activate toggle link if necessary
msgSpan.querySelector('[href="#"]')?.addEventListener('click', toggle.proxyMode)
})
appDiv.append(alertP)
}
function consoleInfo(msg) { console.info(`${ config.appSymbol } ${ config.appName } » ${ msg }`) }
function consoleErr(label, msg) { console.error(`${config.appSymbol} ${config.appName} » ${label}${ msg ? `: ${msg}` : '' }`)}
// Define MODAL functions
const modals = {
init(modal) {
modal.classList.add('.bravegpt-modal')
modal.onwheel = event => event.preventDefault() // disable wheel-scrolling
},
about: {
show() {
if (modals.settings.get()) modals.settings.hide()
// Create/init modal
const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
const aboutModalID = siteAlert(
config.appName, // title
'🏷️ ' + ( msgs.about_version || 'Version' ) + ': ' + GM_info.script.version + '\n'
+ '⚡ ' + ( msgs.about_poweredBy || 'Powered by' ) + ': '
+ '<a href="https://chatgpt.js.org" target="_blank" rel="noopener">chatgpt.js</a>'
+ ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
+ '📜 ' + ( msgs.about_sourceCode || 'Source code' ) + ':\n '
+ `<a href="${ config.gitHubURL }" target="_blank" rel="nopener">`
+ config.gitHubURL + '</a>',
[ // buttons
function checkForUpdates() { updateCheck() },
function getSupport() { safeWindowOpen(config.supportURL) },
function leaveAReview() { modals.feedback.show() },
function moreChatGPTapps() { safeWindowOpen('https://github.com/adamlui/chatgpt-apps') }
], '', 577) // modal width
const aboutModal = document.getElementById(aboutModalID).firstChild
modals.init(aboutModal) // add class, disable wheel-scrolling
// Resize + format buttons to include emoji + localized label + hide Dismiss button
for (const btn of aboutModal.querySelectorAll('button')) {
btn.style.height = '53px' // re-size to fit meaty text content
if (/updates/i.test(btn.textContent)) btn.textContent = (
'🚀 ' + ( msgs.buttonLabel_updateCheck || 'Check for Updates' ))
else if (/support/i.test(btn.textContent)) btn.textContent = (
'🧠 ' + ( msgs.buttonLabel_getSupport || 'Get Support' ))
else if (/review/i.test(btn.textContent)) btn.textContent = (
'⭐ ' + ( msgs.buttonLabel_leaveReview || 'Leave a Review' ))
else if (/apps/i.test(btn.textContent)) btn.textContent = (
'🤖 ' + ( msgs.buttonLabel_moreApps || 'More ChatGPT Apps' ))
else btn.style.display = 'none' // hide Dismiss button
}}
},
feedback: {
show() {
// Create/init modal
const feedbackModalID = siteAlert(`${
msgs.alert_choosePlatform || 'Choose a platform' }:`, '',
[ // buttons
function greasyFork() { safeWindowOpen(
config.greasyForkURL + '/feedback#post-discussion') },
function github() { safeWindowOpen(
config.gitHubURL + '/discussions/new/choose') },
function productHunt() { safeWindowOpen(
'https://www.producthunt.com/products/bravegpt/reviews/new') },
function futurepedia() { safeWindowOpen(
'https://www.futurepedia.io/tool/bravegpt#tool-reviews') },
function alternativeTo() { safeWindowOpen(
'https://alternativeto.net/software/bravegpt/about/') }
], '', 456) // modal width
const feedbackModal = document.getElementById(feedbackModalID).firstChild
modals.init(feedbackModal) // add class, disable wheel-scrolling
// Re-style button cluster
const buttons = feedbackModal.querySelector('.modal-buttons')
buttons.style.cssText += 'display: flex ; flex-wrap: wrap ; justify-content: center ;'
// Format button labels + add v-padding
buttons.querySelectorAll('button').forEach((btn, idx) => {
if (idx == 0) btn.style.display = 'none' // hide Dismiss button
else if (btn.textContent == 'Github') btn.textContent = 'GitHub'
else if (btn.textContent == 'Alternative To') btn.textContent = 'AlternativeTo'
btn.style.marginTop = btn.style.marginBottom = '5px' // v-pad btns
})
}
},
scheme: {
show() {
// Create/init modal
const schemeModalID = siteAlert(`${
config.appName } ${( msgs.menuLabel_colorScheme || 'Color Scheme' ).toLowerCase() }:`, '',
[ function auto() {}, function light() {}, function dark() {} ], // buttons
'', 503) // px width
const schemeModal = document.getElementById(schemeModalID).firstChild
modals.init(schemeModal) // add class, disable wheel-scrolling
// Center button cluster
schemeModal.querySelector('.modal-buttons').style.justifyContent = 'center'
// Re-format each button
const buttons = schemeModal.querySelectorAll('button'),
schemes = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'}
for (const btn of buttons) {
const btnScheme = btn.textContent.toLowerCase()
// Emphasize active scheme
btn.classList = (
config.scheme == btn.textContent.toLowerCase() || (btn.textContent == 'Auto' && !config.scheme)
? 'primary-modal-btn' : '' )
// Prepend emoji + localize labels
if (Object.prototype.hasOwnProperty.call(schemes, btnScheme))
btn.textContent = `${schemes[btnScheme]} ${ // emoji
msgs['scheme_' + btnScheme] || msgs['menuLabel_' + btnScheme] || btnScheme.toUpperCase() }`
else btn.style.display = 'none' // hide Dismiss button
// Clone button to replace listener to not dismiss modal on click
const newBtn = btn.cloneNode(true) ; btn.parentNode.replaceChild(newBtn, btn)
newBtn.onclick = event => {
event.stopPropagation() // disable chatgpt.js dismissAlert()
const newScheme = btnScheme == 'auto' ? ( chatgpt.isDarkMode() ? 'dark' : 'light' ) : btnScheme
saveSetting('scheme', btnScheme == 'auto' ? false : newScheme)
document.querySelector('#scheme-menu-entry span').textContent = btnScheme // update Settings menu status label
update.scheme(newScheme) ; schemeNotify(btnScheme)
schemeModal.querySelectorAll('button').forEach(btn => btn.classList = '') // clear prev emphasized active scheme
newBtn.classList = 'primary-modal-btn' // emphasize newly active scheme
newBtn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis
setTimeout(() => { newBtn.style.pointerEvents = 'auto'; }, 100) // re-enable hover fx after 100ms to flicker emphasis
}
}
function schemeNotify(scheme) {
notify(` ${ msgs.menuLabel_colorScheme || 'Color Scheme' }: `
+ ( scheme == 'light' ? msgs.scheme_light || 'Light' :
scheme == 'dark' ? msgs.scheme_dark || 'Dark'
: msgs.menuLabel_auto || 'Auto' ).toUpperCase()
)}
}
},
settings: {
clickHandler(event) {
if (event.target == event.currentTarget || event.target instanceof SVGPathElement)
modals.settings.hide()
},
createAppend() {
// Init core elems
const settingsContainer = document.createElement('div') ; settingsContainer.id = 'bravegpt-settings-bg'
settingsContainer.classList = 'no-user-select'
const settingsModal = document.createElement('div') ; settingsModal.id = 'bravegpt-settings'
fillStarryBG(settingsModal) // add stars to bg
modals.init(settingsModal) // add class, disable wheel-scrolling
const settingsIcon = icons.braveGPT.create()
settingsIcon.style.cssText = 'width: 59px ; position: relative ; top: -33px ; margin: 0px 41% -8px' // size/pos icon
const settingsTitleDiv = document.createElement('div') ; settingsTitleDiv.id = 'bravegpt-settings-title'
const settingsTitleH4 = document.createElement('h4') ; settingsTitleH4.textContent = msgs.menuLabel_settings || 'Settings'
const settingsTitleIcon = icons.sliders.create()
settingsTitleIcon.style.cssText = 'width: 21px ; height: 21px ; margin-right: -4px ; position: relative ; top: 2px ; right: 10px'
settingsTitleH4.prepend(settingsTitleIcon) ; settingsTitleDiv.append(settingsTitleH4)
const settingsList = document.createElement('ul')
// Create/append setting icons/labels/toggles
Object.keys(settingsProps).forEach((key, idx) => {
const setting = settingsProps[key]
if (isMobile && setting.mobile == false) return
// Create/append item/label elems
const settingItem = document.createElement('li') ; settingItem.id = key + '-menu-entry'
settingItem.title = setting.helptip || '' // for hover assistance
const settingLabel = document.createElement('label') ; settingLabel.textContent = setting.label
settingItem.append(settingLabel) ; settingsList.append(settingItem)
// Create/prepend icons
let settingIcon
if (key == 'proxyAPIenabled') {
settingIcon = icons.sunglasses.create()
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -0.5px ; margin-right: 9px'
} else if (key == 'streamingDisabled') {
settingIcon = icons.signalStream.create()
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: 0.5px ; margin-right: 9px'
} else if (key.includes('autoGet')) {
settingIcon = icons.autoSpeechBalloon.create()
settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
} else if (key == 'autoFocusChatbarDisabled') {
settingIcon = icons.inwardCarets.create()
settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
} else if (key == 'autoScroll') {
settingIcon = icons.downArrows.create()
settingIcon.style.cssText = 'position: relative ; top: 3.5px ; left: -1.5px ; margin-right: 6px'
} else if (key == 'rqDisabled') {
settingIcon = icons.speechBalloon.create()
settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px ; transform: scaleY(-1)'
} else if (key == 'prefixEnabled') {
settingIcon = icons.slash.create()
settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px'
} else if (key == 'suffixEnabled') {
settingIcon = icons.questionMark.create()
settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7px'
} else if (key == 'widerSidebar') {
settingIcon = icons.widescreen.create()
settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7.5px'
} else if (key.includes('bgAnimation')) {
settingIcon = icons.sparkles.create('bg')
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 6.5px'
} else if (key.includes('fgAnimation')) {
settingIcon = icons.sparkles.create('fg')
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 6.5px'
} else if (key == 'replyLanguage') {
settingIcon = icons.language.create()
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 9px'
} else if (key == 'scheme') {
settingIcon = icons.scheme.create()
settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: -1.5px ; margin-right: 8px'
} else if (key == 'about') {
settingIcon = icons.about.create()
settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -3px ; margin-right: 5.5px'
}
settingItem.prepend(settingIcon)
// Create/append toggles/listeners
if (setting.type == 'toggle') {
// Init toggle input
const settingToggle = document.createElement('input'),
settingToggleAttrs = [['type', 'checkbox'], ['disabled', true]]
settingToggleAttrs.forEach(([attr, value]) => settingToggle.setAttribute(attr, value))
settingToggle.checked = config[key] ^ key.includes('Disabled')
settingToggle.style.display = 'none' // hide checkbox
// Create/stylize switch
const switchSpan = document.createElement('span')
const switchStyles = {
position: 'relative', left: '-1px', bottom:'-5.5px', float: 'right',
backgroundColor: settingToggle.checked ? '#ccc' : '#AD68FF', // init opposite final color
width: '26px', height: '13px', '-webkit-transition': '.4s', transition: '0.4s', borderRadius: '28px'
}
Object.assign(switchSpan.style, switchStyles)
// Create/stylize knob
const knobSpan = document.createElement('span')
const knobWidth = 11
const knobStyles = {
position: 'absolute', left: '1px', bottom: '1px',
width: `${ knobWidth }px`, height: `${ knobWidth }px`, content: '""', borderRadius: '28px',
transform: settingToggle.checked ? // init opposite final pos
'translateX(0)' : 'translateX(14px) translateY(0)',
backgroundColor: 'white', '-webkit-transition': '0.2s', transition: '0.2s'
}
Object.assign(knobSpan.style, knobStyles)
// Append elems
switchSpan.append(knobSpan) ; settingItem.append(settingToggle, switchSpan)
// Update visual state w/ animation
setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), idx *25 -25)
// Add click listener
settingItem.onclick = () => {
modals.settings.toggle.switch(settingToggle) // visually switch toggle
// Call specialized toggle funcs
if (key.includes('proxy')) toggle.proxyMode()
else if (key.includes('streaming')) toggle.streaming()
else if (key.includes('rq')) toggle.relatedQueries()
else if (key.includes('Sidebar')) toggle.sidebar(key.match(/(.*?)Sidebar$/)[1])
else if (key.includes('bgAnimation')) toggle.animations('bg')
else if (key.includes('fgAnimation')) toggle.animations('fg')
// ...or generically toggle/notify
else {
saveSetting(key, !config[key]) // update config
notify(`${settingsProps[key].label} ${menuState.word[+key.includes('Disabled') ^ +config[key]]}`)
}
}
// Add config status + listeners to pop-up settings
} else {
const configStatusSpan = document.createElement('span')
configStatusSpan.style.cssText = 'float: right ; font-size: 11px ; margin-top: 3px ;'
+ ( !key.includes('about') ? 'text-transform: uppercase !important' : '' )
if (key.includes('replyLang')) {
configStatusSpan.textContent = config.replyLanguage
settingItem.onclick = promptReplyLang
} else if (key.includes('scheme')) {
configStatusSpan.textContent = config.scheme || 'Auto'
settingItem.onclick = modals.scheme.show
} else if (key.includes('about')) {
const innerDiv = document.createElement('div'),
textGap = '     '
let innerContent = `Version: <span class="about-em">v${ GM_info.script.version + textGap }</span>`
+ `${ msgs.about_poweredBy || 'Powered by' } <span class="about-em">chatgpt.js</span>${textGap}`
for (let i = 0; i < 7; i++) innerContent += innerContent // make it long af
innerDiv.innerHTML = innerContent ; configStatusSpan.append(innerDiv)
settingItem.onclick = modals.about.show
} settingItem.append(configStatusSpan)
}
})
// Create close button
const closeBtn = document.createElement('div') ; closeBtn.id = 'bravegpt-settings-close-btn'
closeBtn.title = msgs.tooltip_close || 'Close'
const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const closeSVGattrs = [['height', '8px'], ['viewBox', '0 0 14 14'], 'fill', 'none']
closeSVGattrs.forEach(([attr, val]) => closeSVG.setAttribute(attr, val))
const closeSVGpath = createSVGelem('path', {
d: 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z' })
closeSVG.append(closeSVGpath) ; closeBtn.append(closeSVG)
// Assemble/append elems
settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsList)
settingsContainer.append(settingsModal) ; document.body.append(settingsContainer)
// Add listeners to dismiss modal
const dismissElems = [settingsContainer, closeBtn, closeSVG]
dismissElems.forEach(elem => elem.onclick = modals.settings.clickHandler)
document.onkeydown = modals.settings.keyHandler
return settingsContainer
},
get() { return document.getElementById('bravegpt-settings') },
hide() {
const settingsContainer = modals.settings.get()?.parentNode
if (!settingsContainer) return
settingsContainer.style.animation = 'alert-zoom-fade-out 0.075s ease-out' // chatgpt.js keyframes
setTimeout(() => settingsContainer.remove(), 50) // delay for fade-out
},
keyHandler() {
const dismissKeys = ['Escape', 'Esc'], dismissKeyCodes = [27]
if (dismissKeys.includes(event.key) || dismissKeyCodes.includes(event.keyCode)) {
const settingsModal = modals.settings.get()
if (settingsModal && settingsModal.style.display !== 'none'
&& (event.key.includes('Esc') || event.keyCode == 27))
modals.settings.hide()
}
},
show() {
const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
settingsContainer.style.display = ''
if (isMobile) { // scale 93% to viewport sides
const settingsModal = settingsContainer.querySelector('#bravegpt-settings'),
scaleRatio = 0.93 * window.innerWidth / settingsModal.offsetWidth
settingsModal.style.transform = `scale(${scaleRatio})`
}
setTimeout(() => { // delay non-0 opacity's for transition fx
settingsContainer.style.backgroundColor = (
`rgba(67, 70, 72, ${ scheme === 'dark' ? 0.62 : 0.33 })`)
settingsContainer.classList.add('animated'); },
100)
},
toggle: {
switch(settingToggle) {
settingToggle.checked = !settingToggle.checked
modals.settings.toggle.updateStyles(settingToggle)
},
updateStyles(settingToggle) { // for .toggle.show() + staggered switch animations in .createAppend()
const switchSpan = settingToggle.parentNode.querySelector('span'),
knobSpan = switchSpan.querySelector('span')
setTimeout(() => {
switchSpan.style.backgroundColor = settingToggle.checked ? '#ad68ff' : '#ccc'
switchSpan.style.boxShadow = settingToggle.checked ? '2px 1px 9px #d8a9ff' : 'none'
knobSpan.style.transform = settingToggle.checked ? 'translateX(14px) translateY(0)' : 'translateX(0)'
}, 1) // min delay to trigger transition fx
}
}
}
}
// Define ICON functions
const icons = {
about: {
create() {
const aboutSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
aboutSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 56.693 56.693']]
aboutSVGattrs.forEach(([attr, value]) => aboutSVG.setAttribute(attr, value))
aboutSVG.append(createSVGelem('path', { stroke: 'none',
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' }
))
return aboutSVG
}
},
autoSpeechBalloon: {
create() {
const autoSpeechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
autoSpeechBalloonSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
autoSpeechBalloonSVGattrs.forEach(([attr, value]) => autoSpeechBalloonSVG.setAttribute(attr, value))
autoSpeechBalloonSVG.append(createSVGelem('path', { stroke: 'none', d: 'M323-41v-247h-10q-105 0-172.5-67T73-528q0-105 74-179t179-74h36l-44-44 69-69 162 162-162 162-69-69 44-44h-36q-64 0-109.5 45.5T171-528q0 64 45.5 109.5T326-373h95v96l96-96h117q64 0 109.5-45.5T789-528q0-64-45.5-109.5T634-683h10v-98h-10q105 0 179 74t74 179q0 105-74 179t-179 74h-77L323-41Z' }))
return autoSpeechBalloonSVG
}
},
braveGPT: {
create() {
const braveGPTicon = document.createElement('img')
braveGPTicon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF2HpUWHRSYXcgcHJvZmlsZSB0eXBlIHhtcAAAWIWtWFuygzYM/dcqugSQbRkvJzfAX2f62eX3HJn3Iw23TWYIMZZ09JaRv//8S/7AR0trEt5hzF1urLVgP5Zy1MbUkmUrNoRedRh/fn5GVawXi1xJOaTYhyb2uYkBezsrErv8yiBMIb/ikKLhFwxDAJFqGMOgTXjnLrxyZyC0nsKs1Yb/7W1DDnwmlAA00UbiCK/6YNnuSFY2WPshRVwotEld7FMjSnBj9qWQdAimPfC0IQawCDkUrLXBQhNK0BD1jVXFvzZkHfHLaxuCaO+LL1wNV6ikzeGrk3oKFNiTNMZoB9VU/CHV63LEtwkvqDNm/+iQsUsHR1wlF34dieKquPZVABDlkOEfWiR3UAsS+HyPAhDgKjhCrbilCiyEHfNzawUGGzMMS1TVsFtf0MBnvC5sWF2E+yC47aFKBzwNwcO0zeyrlSVIwoUBGkBQe1N5qdpzI64JhImup6qQWhm3Z8aIpw6aABEU5l0j3wGfcJ/IVx3k2i6xRwo8EdHJ3jZg2F8oEnKMKVt1yTVzueK+Z84MzXA69oxJPQxGNzLSleyrRYUxc5ZPgqtAvVdOnsiv7C9EI0DF7Jlkj2p8jjEnm6BLkFKesZ1poNoxlkkcmVMJKjNiNJF8tPepDu2iSjysUJNQfhrtkhdYbkbyBy037FOM9ZfMvTqpoEywIvGKhQR+qEmsTtmvnT+t982ObW9MLKiVC9nKnu+OLfzntXEueqyZt2yl1marUNsz5G9Zy8zbC8ULfmU17MnOvVd4j9aUPGUHVAriOCUvGLH0IwBAPmld0YQ7gljomMTaOEJsYeEhhSCSFSEDMmwBckBmZTZ2E3BjNVps4TUBrBhXbpDB8w3GDj3KSN2EICveSV/QOOMXHkM2JCjehw7MW9oJ4ZonyxRrQ2O+hn2N8AJwmVkEK4H0hIAhGPJSOSuCQ1uNrNmhIrmyCF0BNRGLENKhifcBTrGKKTu6DExonEjayYWx9k8GQkCTIUaE4xEdfVrqgAFE7xWt7OEu9oqTvRQMvYR4a0y0DVYMUwGFdxDfuQ0xRCg6Zx/MG11bDQg1Gt9W7TUVjgVPmtrp1CY9XFrZb03dXMzszba52Zpddf96857vJ3qZGJTfMpjoVc7kCdDR4WPNN0+fueS91jxAxNQpL21UWzeiWBXco+NifIqeabRI2CEMU3buqGQiKzdkLWI7LbPJRaQlzikozBLfsD88kRWi9qMMYzfv16BWucv+hVFkiPVVZ0Vb5Rykt2VjpauYZKo7lYxTmCPQ8QrX1RgxKwdGF8ptkwIG9aIB1rkcpkn4LLd1TQ7ar+PCi87dRpRtughn8P1uscsydVGl/iXrZE27L7JugyBnY+AMc5uQQ5/4prNt2THScZczBy1oHr1TMKs/N+b7vixTYx4vA++j6XnHMQvehPnlbH8mY9YanNziYUFS8xbU+3Hr2O0atuxRScr4BQ5IWkoWthmh70eai9lWOdY8VClTkJ0jSbD120gC2sB+h37CPup9hPc8weAIkWokGVtgx3bobTuRiM8YT7gy8D5GkpwmjtYnjlJnDY/o9ptIEvD9XyJJNiPe1sQkoc3IvB7Bxs/RJs98c+8aOfoG6sBCuKrbpdqHtVs9tdHlOQEY8zH5QRpTCiu7+CTSAiiynjn/b1l+NwzKh0HzMsvvXCNPs/zONfLMN/eVQLal4FeVYEp3+WW+N1PVWAqKbObn5yo5RrQre1bYDjXR+KZpUVH+Y0lbVJSLI8JjFZlQ8iyj7od7eTLd3wz32+z/NqPuE0qeZdR9QsmzjLpPKFlHqu204y82Ps9I+2NPkjkyDf/m4Nq81Pjaf/LbingsiDJbYkHw1ensfDiT3emMCBLHKkr0WazzIwKPpYZetw5lDCzi5JGMH7TshlOZS2pP6G5PZ+fDmfzudHYeE+UwJ57n7MtXmcddOB3xvesaO8eXzfXJxRtvnqCqWlpfVss/sZNGK6MFOPYAABOUSURBVHja1Zt5eF1lncc/77nn7rnJzc3SJM3SJCTVNiSAtDQFAauFQsUZFZABER3HQWcUh2UQFFxg1FEftkcdFOcZF4ZlRIWKBcrahqVFWtrQ0pYS2qZN0iZNcpPc3PUs7/xxTva7pVTGeZ/nfZJ7znnf8/6+72//vUdIKZnRviMgBhy0fwvAdEGsHIQThAa6BAkoBvgFCAEo1l/ThLFUO1HPc5jCjZRRkCDT9EzXZ9zDjzQT6MlVGOWvQhEIHaJxcBjgGQRTt9apO0E6QIlZa1etaRDASUAB8NuZ9Kq8myYBaRMuAUWCIRcS8/4RQ/EiDEAGsOlJOz5dE4A5cV+CwIeiPI4hT0GafWBCQQHIBJjGuyJBeVejzQRoDSBPgUQcInEfQ44N6I5SlAnoxRRR82nThlrgOstQR5+E/R7CXbDuUbj5WjhgHMfkJwIAAUgTTA8IL2BA1P0YmmspDplji5kJikzHWelWa7aiaI9iAqVlUBSwOOX/jAMAHIrVE+770DyrcZhzKRQ5REBkAnjWDSlAqGsQ/AwhORFNPQ6hLwdZDeg43AdIHYqQPHobcf8XpuCUaQiVaUCQ2V8lpo2Z+Ks4oFC7mtuvOUp/77cpJwA0AA4QPUgG/jIAmK616J5bMZVWTLxggDD6kGOH0M0PoPgtMTDTsL+YxgZyzjanx2EO19g/hAJ+L9z70M14xflUu+pIyUqkBIceR6ETk9sQPHniABD6tbiP3YlLsbS+oYLmhaSvCr24CkcShGHJ44RFmC7ImWRaYJm6rCIyW1FIMCTUu13AClKmZXqRYEovyBXAEwj+Bck97x4Aky+CeSfuyFzFpasQLYFosW27jSn7Pd2eiVkESQmRMYjHrWseJ/gDtj8xDbQJgKajM31qXQdDTw+eyd1AHIX7jg8Aay2fR3LvpF2e3Zw6lPaDCEOXhKQOLhUCReB0gqJYLIu0HaRRGI6BE2hcBAvrLAIP7Ye3eyAFFDsg4APhsAkxQE9ac48ASUvaCWHNY6ZRohM4GvwcQQoHv8ofADegATqfQvCfWU2sCYzaY/71Ruv/t3dC1y4YGrZ2OWaCDviAxnq49Hw4Zw2c/kEoDFnzjA7Dq5vg+fWw8U/Q1Q/j9jt8QAAIFsLyNljSBk4X3P0TSKSse2YW62LwSyTjOPkdrnwAOAYkWInk4azEC5vjjwLXfx0+9a2peyP90N8Pe7ZB937QNWhdBmedD96CuXMVheC8j1s9GoEX1sP2Vy0uqjvJGrugCkoXTI1xFcCNt1kAKbN0iJgmQiaQ4hHGaMdgyxwy5sQCnxYgaUWjM6cHcQC46GNwy7rJy4ODgxw5coRIJEIqlcLpdBIIBKioqKC8vDwvnRuNRunt7SUcDpNIJFAUBa/XS2lpKdXV1aiqvW9XfBQeXA+N9mZksiIGUMnJeNnFRpkDgEsnt/0B4PKMxB8B6qvh3jfAW0wkEmH79u10d3cTj8cxTRPTNFEUBSEEXq+XmpoaTj31VILBYEbiOzs72bdvH6Ojo+i6zsT6hBC4XC5KS0tpaWmhoaEBIkOwshX29kGdTahIIwoO7kfhM0jg7fwBOBd4IS3rR7EU1t0vwJJzOXDgAB0dHUSjUfx+P6rTCUJMiaKU6JpGLBbD7XZz1lln0dzcPGfXn3vuOQ4fPozH48Hj8YCizJjDNE0S8TiaptHS0sLZZ58Nm1+A1avAAxSl0QcGUMK5FLAJE3g5VzTon5SdV4kzjEloBqomMAhc911Yci7d3d1s2LABVVUJhkIYUqIrCoYQSCEQUqJIiUMIilwuEvE4zz77LEIImpqaAIjH46xfv57BwUGCxcWgKBiAoShIG0iHlCimiS8QQOo6nZ2d6LrOqlWr4Du3ww23WuHubA5QGcLkVcbT+xpzAXBMKrjzMAjNiBYU4BDw4Q/DpV8nEomwceNGnNLAO9KHNgIpBRLSJKlYWDl8RbhD1XgcTly6jsfnA6Cjo4PS0lKKi4t56aWXOHbsGMWhEIYQaA4HcUMj2duNPh6x2B/wCAduKXECoapadu/eTWlpKa3X3wIvPgfrNk7pgynfo4Qoq5E8np8ZjE+yzqUz0BS2yVvggS//BwA7duwgGo0S9HvRtr9MfP9ORkQlQ9W1jFWWo3ncuAcPU7RvEyU4KSqoxF3biscfZDQc5vXXX6e5uZl33nmHomAQY2yE5J4dRPp7GTQ1RrxlxP2FqMkUBUNDhHp6KB7rw3/yUtSaBvx+P52dnTQ2NuK/4174cxsMpSwfwZyhBy7Bx+PprJqaVsGZlGJy/hx2GgW+cDtUNtPf3093dzd+vx/D5SJ13pcYQaMnOUy3W3A0VMV4IERBZJiarp3U7uqEzRsJPf8wymXfwB8o480332R4eNh67VAfybtuY1wp5+jixRw843R6m5cQrqhC1VNUDvZRm9KR/iAOoeDVNNzAyPAwXV1dtLW1wc23wTU3QfEca7AGjRAwnBsASSmSNUhKZgAwBJzRBp+4AYC+vj5isRh+vx9dUUioKkPBUrrL29izYAGxoiLqVJUB02C4dCFCmvjNEfxbw6hjYdTiSgYGBojH4yxYsADjSDeppIextnJ625bw9ukr6WlfQWWoBIA3olG0I0fwHD1KQTiMxzDANFFVlf7+fmuNX/kaPPYAvLITKmeYwTIkaxA8NRuEufkAnbUYXDzH4zOA1X8/Q2sbhgFCYApBSlUZ83oZCAQYLSzkalXld8APFQecfDp9jUsZV0OkqkuQlQ0gBIlEgrGxMVRVRVbXo9cWEVFLGayvp2/5MtpDJTxo2+MP+P0cCIUY9XpJqCqGEJhS4lBVYrHY1Fqv+JzlyZpidnbpYhTW5pMQ0QAtjTYF/5T9nrDRE1raEAJNUUiqKk6Hgw8BC4ELgBohiDe2oTndGP4i8FneoGEY6LpumcxAIaYvgOF2klq2gkSgkFOApUALcCoQdzpJORwYioIprAUqimLNMdECQWutppydYdKRaLkBEDyN4I9zLIMGvDbl8Xk8HsvJkRIFcJgmbl3Hl0qhaRq/BV4H/gs4qOsUGuCScVR/CGFnfl0uFz6fD8MwEIASLMUp43idbvypFJuAZ4CngQ4gGI/j0TRU00Sx/RfDMCyfYaI9u86KPRyzcotOHsPJ0/lEg4MIts65Wgo88QdovQ/O+0eKi4txOp1I00RIiccwCMbjVEYijHk8PFJUxFMuF3FdpzgcZmHvAQoPHsPVdBJCSqSUFBQUEAqF0DUNt8uFc0EVga27KevpprawkL2Kwj/4fCAlIhLh/cPDhOJxvJqGwzRRhEBLpSgqKrLW+JufwS/XQVWa/KJkGzIfJWhx0x4E223Os0NfwAt892rweqk9/WJCoRBDQ0N4VBWPphGKxagLh1FMk5JolLjTiT+VoiISoWbz4wT7j+Be/kEcQDKZpL6+nvr6ejo7O/H6/bjKqyga2MjCpx/HLKsgkEgw6vGgmibl0SgLR0cpjUYtAKTENAwURWHRokXw6K/hs1+yLIBrWmxgie82nLyVnx+gTnqCL2FOA8C0w1Id+PZn8N5RzNKlS3nmmWfw+ny4hSCYSKAAvlSKyrExdEXBZRgU9XQRfP5JChzDqEUVYJrEYjGWLVtGQ0MDXV1dJGIx3GWV+LVhKv7wJK7WZZQ2NJFQVRxSUpBMUhyPE0gmces6DmAsEqG6upqaru1w2WctT7B4miM0pQZezJQ9nqsDPHZ38MqcQaadiFCAmy6i2exj8eLFhIeHEYaB1xaDqtFRGoeGaBoaon54mMqnfkXRoX48Pi9qQREj4TD19fW0tLTg9/tpb28nHo+jeb24g14CxwapeOh+6sNhmgYHaRwaomZ0lFAshk/TcEpJPBrF6XSy0mvA3/6NtXELZkWFEyCk2EximpOXFYAJmXHSgYPkHP/ZsPVBErjpAs5pClFXV0d4eBgjmcRjGBSkUgQTCYLxOEW7N+PbsQWXkkAWlDEcHqWqqopVq1YhbE3e3NxMe3s7saRGIliOM5nE3/EKhVs3U5xIEEwk8Ntzq6ZJZHQUKSXnnVRN6IpPWYRVTYrvFB0GoJKghg5qgOp8RGAKwT4knQiWp32mAuhL4LxpDWt/vIUtJSXs2bOH6Pg4Lrcbp8uFQwjkax2khjQ0r8SpuGhtbaW9vR2n0zljytNOO42CggK2rrufEcPAaRg4X3gRZXGLpZo0jUQqhWmaVFZWsrJhIeUfvwCOJa2kuJ6uZgEk2M4wR3GlT+vNBaB8mtI7xosMsxxnmskNG/XDx1C+tpqV97xCU1MTXV1d9Pf3E4vF0DQNZ2sbhfs2U2bAScYeKga3wAEf1C4Bj8+OP6Kwfw/Nm5+npmcX+4BeFSJnnkbSNBFCECgoIBgMUltby+LyIji3HfYPWUVPPUNyoRe4ipf5CtCTvgAzNx9wtZhCb5yLiPLHSZuaSYscBM44Be7ZyuBgeG5GaOwwFb+/kfIth63FlgNlJVDVYv1+exe8NWQtEoi2L6L3ljsJl1TNzQgtrED9yHLo2Jk+EzS9RYA21rKcJyZzjD/OlRC5WEw3H9XAW0h8OWt8e4Fbvg+X3JT5ud9+A376PRiw8w5jdmJF2LsVAL55C3z19sxz3Pk9uP4bFttnrTcAKuOM08wwRyZ5PSpzKEGX3d2Agx7gNfIpw3nsqk22dul34cHdcPbJEAbK7D4IXNAGW/dmJ55Z75BZymqWmL6GjyNU2zqrIh8rYM7q0JGz+hwD6hQ4+5LcQC18P/z8DfjyFy0QhoCb/wnW7YD6xbnHf+ISqBMWe+eqOgs2TbrCIr0OyGwGp/rmnIsaBU5dC+X1+Vclr70Xmirh5Gr4+k/zH1fXCOdcaHFN1houINiCMl8AnLO6i5cRDGZlNwEs/eD8Cs3HemDoCPT3wkDv/MauOGuKWzOX148heRljDkfPmwPGgNczioG0LYa/OH8CYmH4ymprF49IuOo8iI7kP76w2NqcbOcwBFsRjM9fBJRZ3QEovJiRAxQ7VN6/Lb/FD3fD506DbXstpbQQeGU3rDkNBg7lN8eb2yzrkck8G4CblygGCmf14+AAEGzMqgiDwIafQ+/u7At/40m4cinsPQg1tvYfBOqBHQdgxVJ4eUP2Ofa9Cff/wnLHM55aARQ2kpxM70z14wIAdiKzqB0/0C/hni+mvz90GO7+JFxzIRyLWtcGgKW1sLTGqi8KoGccPrQGrrkEjmbQC1+92nKYirJyZD9hdjICc3pOT/Aqkd6sJFmHxscynioSQB+wfBnc9BCUNcKhnfCnH8CGB6x6ArbdX3khXPBlOOMC69pL6+Hhn8AfnrJKbhMu+VVXwudvhMUtcLgLPn8ZPLPNcoIyHY6KAxU8RgsfJ5nm/iO5PMHPZgTgBjR+lFEUJq4PAEEP1JXBwT7oMSx3txo4/+/g4tuhsjH9HN1dcM+t8OuHp3K3CxywpAr2DsBgciqik1kAaOA6lnAXiTT3/zsXAFdmqB6McxYpXsRL9qNpwl6EZjtIDuDCi+DKu2BBY35K7mAXfP9a+PWfrHkCtmdakMX9nTCLEeDfOJMzeSXtcanVuQD4jUivVCQqz/M2gyzCkwcRYWD5CrjuESip5rha32H450vg6VenolRyOkD7qaQJH2baKLEjV3G0OYNWdaCzmdfR8gQgBSxoOH7iAapqoHoRxF/N73kBONhGLyYp8joFOReAG7MEF2VsJMgnyOd4bgXw6INweB98cz0EyudH/HA/XH4hPPe6ZSbzPREq2YTbDujywSzL+YB0rQ3YkfduSOAwUFsAtz8BzXm6y9s2wSfXQncUFpH5GN3sTVIANycj2JXx+T25wmGRtXci2JXnTtjBCzAwDlefDU/elXvcr+6AFedCX9RKeJAH8VPvfAOFXXO82ek9pyPkyNKtlPljzOeYrmmLgw+49Tq489OZn73+cvjcDZbLWpsj25Nu4xQeI26fYIll6DlF4EqR/SUGTSTZizLPg9YKkAC6gDPb4IfPQFGZHU4PwJWr4fE3rNjAzfxOgQsghY6P9+Hinaxjd+cSAZmlW4cS30blF8z3sLa0w+tGYFMnXNEKB3fC/k64sBWefgOabOU13yPwBhDgFwR4B6cNYKaekwM+JnLbjTGCJOhkIbV5y+jUmT3LM+yz01tJIGJaFZ14+oAlZ0twkBbaqGUsHZvPaA/l8gO+mgchLkY4xEf5Pa8ABVmzxukU40SFKWJa10ptYI6nCcYQfBSTscnkxzzaXAA+nOfIHnbyCKdi8CyCOttbzB8EbJYUx7Hrqp2GUzmAj48A+2dFr/NSTcffDLrw0YSPRzIWJ9IFTPmAk9VJAk7jf2immSj73w0J7/KjKcCNRoBLUfg0MofhkvMkNP0cOiaXs4TLWIieU+b/ogBMT6OrPICDWsTcA8nz5oR0z2t2htpBLS4eOgErP0EAzMwO9wHtwE0ZCUnn1socwKSAGm4kyEp0jkx8l/HXBcDM9gMELQj2nQAOeAuTJZTxI+yv897FZ4LvGQAAbwKLEdwx+W2BzFMkpjjiRwjeh2APqRNP/F8agAkibkDQjoNe+0sU5hzBnS7n1icxPSicgUgbnP8/AwAgyRaWU0cd96EwVRGe2G2HncpSgBJ+hkkdgj+/F0t7bwCQQAKDM7iaFj6EZiuyiRYDCujjAs7hLL7EAOaJUnJ/HQBMNB0oYCPFNGDwQyQJBAnC/Dtn0MBSOhh7T1fE/wLpN+iaYs00vgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wNy0xMlQxMzo1Mzo0MCswMDowMEEzOtoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDctMTJUMTM6NTM6NDArMDA6MDAwboJmAAAAAElFTkSuQmCC'
return braveGPTicon
}
},
downArrows: {
create() {
const downArrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
downArrowsSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 24 24']]
downArrowsSVGattrs.forEach(([attr, value]) => downArrowsSVG.setAttribute(attr, value))
downArrowsSVG.append(
createSVGelem('path', { stroke: 'none', d: 'M18,13H6a1,1,0,0,1,0-2H18a1,1,0,0,1,0,2Z' }),
createSVGelem('path', { stroke: 'none', d: 'M14.71,18.29a1,1,0,0,1,0,1.42l-2,2a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42l.29.3V16a1,1,0,0,1,2,0v2.59l.29-.3A1,1,0,0,1,14.71,18.29ZM11.29,8.71a1,1,0,0,0,1.42,0l2-2a1,1,0,1,0-1.42-1.42l-.29.3V3a1,1,0,0,0-2,0V5.59l-.29-.3A1,1,0,0,0,9.29,6.71Z' })
)
return downArrowsSVG
}
},
fontSize: {
create() {
const fontSizeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
fontSizeSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
fontSizeSVGattrs.forEach(([attr, value]) => fontSizeSVG.setAttribute(attr, value))
fontSizeSVG.append(
createSVGelem('path', { stroke: 'none', d: 'M234.997 448.199h-55.373a6.734 6.734 0 0 1-6.556-5.194l-11.435-48.682a6.734 6.734 0 0 0-6.556-5.194H86.063a6.734 6.734 0 0 0-6.556 5.194l-11.435 48.682a6.734 6.734 0 0 1-6.556 5.194H7.74c-4.519 0-7.755-4.363-6.445-8.687l79.173-261.269a6.734 6.734 0 0 1 6.445-4.781h69.29c2.97 0 5.59 1.946 6.447 4.79l78.795 261.269c1.303 4.322-1.933 8.678-6.448 8.678zm-88.044-114.93l-19.983-84.371c-1.639-6.921-11.493-6.905-13.111.02l-19.705 84.371c-.987 4.224 2.22 8.266 6.558 8.266H140.4c4.346 0 7.555-4.056 6.553-8.286z' }),
createSVGelem('path', { stroke: 'none', d: 'M502.572 448.199h-77.475a9.423 9.423 0 0 1-9.173-7.268l-16-68.114a9.423 9.423 0 0 0-9.173-7.268H294.19a9.423 9.423 0 0 0-9.173 7.268l-16 68.114a9.423 9.423 0 0 1-9.173 7.268h-75.241c-6.322 0-10.851-6.104-9.017-12.155L286.362 70.491a9.422 9.422 0 0 1 9.017-6.69h96.947a9.422 9.422 0 0 1 9.021 6.702l110.245 365.554c1.825 6.047-2.703 12.142-9.02 12.142zM379.385 287.395l-27.959-118.047c-2.293-9.683-16.081-9.661-18.344.029l-27.57 118.047c-1.38 5.91 3.106 11.565 9.175 11.565h55.529c6.082-.001 10.571-5.676 9.169-11.594z' })
)
return fontSizeSVG
}
},
inwardCarets: {
create() {
const caretsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
caretsSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
caretsSVGattrs.forEach(([attr, value]) => caretsSVG.setAttribute(attr, value))
caretsSVG.append(createSVGelem('path', { stroke: '', d: 'M11.29,9.71a1,1,0,0,0,1.42,0l5-5a1,1,0,1,0-1.42-1.42L12,7.59,7.71,3.29A1,1,0,0,0,6.29,4.71Zm1.42,4.58a1,1,0,0,0-1.42,0l-5,5a1,1,0,0,0,1.42,1.42L12,16.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z' }))
return caretsSVG
}
},
language: {
create() {
const languageSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
languageSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
languageSVGattrs.forEach(([attr, value]) => languageSVG.setAttribute(attr, value))
languageSVG.append(createSVGelem('path', { stroke: 'none', d: 'm459-48 188-526h125L960-48H847l-35-100H603L568-48H459ZM130-169l-75-75 196-196q-42-45-78-101t-55-105h117q17 32 40.5 67.5T325-514q35-37 70-93t64-119H0v-106h290v-80h106v80h290v106H572q-23 74-70 152T399-438l82 85-39 111-118-121-194 194Zm508-79h139l-69-197-70 197Z' }) )
return languageSVG
}
},
questionMark: {
create() {
const questionMarkSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
questionMarkSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 -960 960 960']]
questionMarkSVGattrs.forEach(([attr, value]) => questionMarkSVG.setAttribute(attr, value))
questionMarkSVG.append(createSVGelem('path', { stroke: 'none', d: 'M428-383q0-71 16-111t63-74q47-35 58.5-55.5T577-683q0-35-25-57.5T488-763q-26 0-61 18t-50 70l-114-47q27-82 90.5-122.5T488-885q93 0 151.5 59.5T698-682q0 55-17 95t-70 83q-37 29-48.5 55T550-383H428Zm60 265q-41 0-69.5-28.5T390-216q0-41 28.5-69.5T488-314q41 0 69.5 28.5T586-216q0 41-28.5 69.5T488-118Z' }))
return questionMarkSVG
}
},
scheme: {
create() {
const schemeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
schemeSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
schemeSVGattrs.forEach(([attr, value]) => schemeSVG.setAttribute(attr, value))
schemeSVG.append(createSVGelem('path', { stroke: 'none', d: 'M479.92-34q-91.56 0-173.4-35.02t-142.16-95.34q-60.32-60.32-95.34-142.24Q34-388.53 34-480.08q0-91.56 35.02-173.4t95.34-142.16q60.32-60.32 142.24-95.34Q388.53-926 480.08-926q91.56 0 173.4 35.02t142.16 95.34q60.32 60.32 95.34 142.24Q926-571.47 926-479.92q0 91.56-35.02 173.4t-95.34 142.16q-60.32 60.32-142.24 95.34Q571.47-34 479.92-34ZM530-174q113-19 186.5-102.78T790-480q0-116.71-73.5-201.35Q643-766 530-785v611Z' }))
return schemeSVG
}
},
shuffledArrows: {
create() {
const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowsSVGattrs = [['width', 21], ['height', 21], ['viewBox', '-1 -1 32 32']]
arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
arrowsSVG.append(createSVGelem('path', { stroke: '', d: 'M23.707,16.293L28.414,21l-4.707,4.707l-1.414-1.414L24.586,22H23c-2.345,0-4.496-1.702-6.702-3.753c0.498-0.458,0.984-0.92,1.46-1.374C19.624,18.6,21.393,20,23,20h1.586l-2.293-2.293L23.707,16.293zM23,11h1.586l-2.293,2.293l1.414,1.414L28.414,10l-4.707-4.707l-1.414,1.414L24.586,9H23c-2.787,0-5.299,2.397-7.957,4.936C12.434,16.425,9.736,19,7,19H4v2h3c3.537,0,6.529-2.856,9.424-5.618C18.784,13.129,21.015,11,23,11zM11.843,14.186c0.5-0.449,0.995-0.914,1.481-1.377C11.364,11.208,9.297,10,7,10H4v2h3C8.632,12,10.25,12.919,11.843,14.186z' }))
return arrowsSVG
}
},
signalStream: {
create() {
const signalStreamSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
signalStreamSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 0 32 32']]
signalStreamSVGattrs.forEach(([attr, value]) => signalStreamSVG.setAttribute(attr, value))
signalStreamSVG.append(createSVGelem('path', { stroke: '', 'stroke-width': 0.5, d: 'M16 11.75c-2.347 0-4.25 1.903-4.25 4.25s1.903 4.25 4.25 4.25c2.347 0 4.25-1.903 4.25-4.25v0c-0.003-2.346-1.904-4.247-4.25-4.25h-0zM16 17.75c-0.966 0-1.75-0.784-1.75-1.75s0.784-1.75 1.75-1.75c0.966 0 1.75 0.784 1.75 1.75v0c-0.001 0.966-0.784 1.749-1.75 1.75h-0zM3.25 16c0.211-3.416 1.61-6.471 3.784-8.789l-0.007 0.008c0.223-0.226 0.361-0.536 0.361-0.879 0-0.69-0.56-1.25-1.25-1.25-0.344 0-0.655 0.139-0.881 0.363l0-0c-2.629 2.757-4.31 6.438-4.506 10.509l-0.001 0.038c0.198 4.109 1.879 7.79 4.514 10.553l-0.006-0.006c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-2.173-2.307-3.573-5.363-3.774-8.743l-0.002-0.038zM9.363 16c0.149-2.342 1.109-4.436 2.6-6.026l-0.005 0.005c0.224-0.226 0.363-0.537 0.363-0.88 0-0.69-0.56-1.25-1.25-1.25-0.345 0-0.657 0.139-0.883 0.365l0-0c-1.94 2.035-3.179 4.753-3.323 7.759l-0.001 0.028c0.145 3.032 1.384 5.75 3.329 7.79l-0.005-0.005c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-1.49-1.581-2.451-3.676-2.591-5.993l-0.001-0.027zM26.744 5.453c-0.226-0.227-0.54-0.368-0.886-0.368-0.691 0-1.251 0.56-1.251 1.251 0 0.345 0.139 0.657 0.365 0.883l-0-0c2.168 2.31 3.567 5.365 3.775 8.741l0.002 0.040c-0.21 3.417-1.609 6.471-3.784 8.789l0.007-0.008c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c2.628-2.757 4.308-6.439 4.504-10.509l0.001-0.038c-0.198-4.108-1.878-7.79-4.512-10.553l0.006 0.007zM21.811 8.214c-0.226-0.224-0.537-0.363-0.881-0.363-0.69 0-1.25 0.56-1.25 1.25 0 0.343 0.138 0.653 0.361 0.879l-0-0c1.486 1.585 2.447 3.678 2.594 5.992l0.001 0.028c-0.151 2.343-1.111 4.436-2.601 6.027l0.005-0.005c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c1.939-2.036 3.178-4.754 3.323-7.759l0.001-0.028c-0.145-3.033-1.385-5.751-3.331-7.791l0.005 0.005z' }))
return signalStreamSVG
}
},
slash: {
create() {
const slashSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
slashSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 0 15 15']]
slashSVGattrs.forEach(([attr, value]) => slashSVG.setAttribute(attr, value))
slashSVG.append(createSVGelem('path', { stroke: '', d: 'M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z' }))
return slashSVG
}
},
sliders: {
create() {
const slidersSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
slidersSVGattrs = [['width', 20], ['height', 20], ['viewBox', '0 -960 960 960']]
slidersSVGattrs.forEach(([attr, value]) => slidersSVG.setAttribute(attr, value))
slidersSVG.append(createSVGelem('path', { stroke: 'none', d: 'M435.48-102.48V-360H533v80h320v97.52H533v80h-97.52Zm-328.48-80V-280h257.52v97.52H107Zm160-169.04v-80H107v-96.96h160v-80h97.52v256.96H267Zm168.48-80v-96.96H853v96.96H435.48Zm160-168.48v-257.52H693v80h160V-680H693v80h-97.52ZM107-680v-97.52h417.52V-680H107Z' }))
return slidersSVG
}
},
sparkles: {
create(style) { // style = ( 'fg' ? filled front sparkle : 'bg' ? filled rear sparkles )
const sparklesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sparklesSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 512 512']]
sparklesSVGattrs.forEach(([attr, value]) => sparklesSVG.setAttribute(attr, value))
sparklesSVG.append(createSVGelem('path', { // large front sparkle
fill: style == 'bg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
d: 'M259.92,262.91,216.4,149.77a9,9,0,0,0-16.8,0L156.08,262.91a9,9,0,0,1-5.17,5.17L37.77,311.6a9,9,0,0,0,0,16.8l113.14,43.52a9,9,0,0,1,5.17,5.17L199.6,490.23a9,9,0,0,0,16.8,0l43.52-113.14a9,9,0,0,1,5.17-5.17L378.23,328.4a9,9,0,0,0,0-16.8L265.09,268.08A9,9,0,0,1,259.92,262.91Z' }))
sparklesSVG.append(createSVGelem('polygon', { // small(est) rear-left sparkle
fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 24,
points: '108 68 88 16 68 68 16 88 68 108 88 160 108 108 160 88 108 68' }))
sparklesSVG.append(createSVGelem('polygon', { // small rear-right sparkle
fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
points: '426.67 117.33 400 48 373.33 117.33 304 144 373.33 170.67 400 240 426.67 170.67 496 144 426.67 117.33' }))
return sparklesSVG
}
},
speaker: {
create() {
const speakerSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
speakerSVGattrs = [['width', 22], ['height', 22], ['viewBox', '0 0 32 32']]
speakerSVGattrs.forEach(([attr, value]) => speakerSVG.setAttribute(attr, value))
speakerSVG.append(
createSVGelem('path', { 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' }),
createSVGelem('path', { 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' }),
createSVGelem('path', { 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' })
)
return speakerSVG
}
},
speechBalloon: {
create() {
const speechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
speechBalloonSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 -960 960 960']]
speechBalloonSVGattrs.forEach(([attr, value]) => speechBalloonSVG.setAttribute(attr, value))
speechBalloonSVG.append(createSVGelem('path', { stroke: 'none', d: 'M350-212q-32.55 0-55.27-22.73Q272-257.45 272-290v-64h492v-342h63.67q33.33 0 55.83 22.72Q906-650.55 906-618v576L736-212H350ZM54-256v-582.4q0-32.38 22.72-54.99Q99.45-916 132-916h482q32.55 0 55.28 22.72Q692-870.55 692-838v334q0 32.55-22.72 55.27Q646.55-426 614-426H224L54-256Zm540-268v-294H152v294h442Zm-442 0v-294 294Z' }))
return speechBalloonSVG
}
},
sunglasses: {
create() {
const sunglassesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sunglassesSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
sunglassesSVGattrs.forEach(([attr, value]) => sunglassesSVG.setAttribute(attr, value))
sunglassesSVG.append(createSVGelem('path', { stroke: 'none', d: 'M507.44,185.327c-4.029-5.124-10.185-8.112-16.704-8.112c0,0-48.021,0-156.827,0h-65.774H243.87h-65.774c-108.806,0-156.827,0-156.827,0c-6.519,0-12.675,2.988-16.714,8.112c-4.028,5.125-5.486,11.815-3.965,18.152c0,0,12.421,56.269,19.927,82.534c7.506,26.265,26.265,48.772,86.29,48.772s59.827,0,74.828,0c21.258,0,46.256-19.99,55.028-45.023c4.97-14.16,12.756-32.738,19.338-47.876c6.582,15.138,14.368,33.716,19.338,47.876c8.773,25.033,33.77,45.023,55.028,45.023c15.001,0,14.803,0,74.828,0s78.784-22.507,86.29-48.772c7.496-26.264,19.918-82.534,19.918-82.534C512.935,197.142,511.478,190.452,507.44,185.327z M90.339,278.734C45.314,263.732,40.318,198.7,40.318,198.7s22.507,0,55.028,0L90.339,278.734z M340.464,278.734c-45.015-15.001-50.022-80.034-50.022-80.034s22.508,0,55.029,0L340.464,278.734z' }))
return sunglassesSVG
}
},
upArrow: {
create() {
const upArrowSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
upArrowSVGattrs = [['width', 16], ['height', 16], ['viewBox', '4 2 16 16'],
['stroke-width', '2'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round']]
upArrowSVGattrs.forEach(([attr, value]) => upArrowSVG.setAttribute(attr, value))
upArrowSVG.append(createSVGelem('path', { stroke: '', fill: 'none', 'stroke-width': '2', linecap: 'round', 'stroke-linejoin': 'round',
d: 'M7 11L12 6L17 11M12 18V7' }))
return upArrowSVG
}
},
widescreen: {
wideSVGpath() { return createSVGelem('path', {
stroke: '', 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'
})},
tallSVGpath() { return createSVGelem('path', {
stroke: '', 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'
})},
create() {
const widescreenSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
widescreenSVGattrs = [['id', 'ws-svg'], ['width', 18], ['height', 18], ['viewBox', '8 8 20 20']]
widescreenSVGattrs.forEach(([attr, value]) => widescreenSVG.setAttribute(attr, value))
widescreenSVG.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
return widescreenSVG
},
update(widescreenSVG) {
widescreenSVG.removeChild(widescreenSVG.firstChild) // clear path
widescreenSVG.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
return widescreenSVG
}
}
}
// Define UPDATE functions
const update = {
appLogoSrc() {
appLogoImg.onerror = () => appLogoImg.style.display = 'none'
appLogoImg.src = 'data:image/png;base64,'
+ ( scheme == 'light' ? 'iVBORw0KGgoAAAANSUhEUgAAAPoAAAA1CAYAAABoUvZcAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMGGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDQtMTJUMDQ6MDE6MTctMDc6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDQtMTJUMDQ6MDE6MTctMDc6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZTgxNTM4MTQtOWQ0NC1hZDQ1LWEyYzYtMDY3YjIxMjlkMDc1IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDUwZDFiZTItMzY3NC1kNTRiLWJkNDQtZGE2ZGE0MjE2ZjFkIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjdiY2U5OTUtOTdmYS0wODQ4LTg4MjktNmRmMWEwY2MwMDg1IiB0aWZmOk9yaWVudGF0aW9uPSIxIiB0aWZmOlhSZXNvbHV0aW9uPSI3MjAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjcyMDAwMC8xMDAwMCIgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIgZXhpZjpDb2xvclNwYWNlPSIxIiBleGlmOlBpeGVsWERpbWVuc2lvbj0iNzMwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTU1Ij4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSInZ3B0JyIgcGhvdG9zaG9wOkxheWVyVGV4dD0iZ3B0Ii8+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6VGV4dExheWVycz4gPHBob3Rvc2hvcDpEb2N1bWVudEFuY2VzdG9ycz4gPHJkZjpCYWc+IDxyZGY6bGk+YWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjgyNTg1NWVhLThmZjYtNjk0OC04ODZmLWIxMmZmZDBjMGJlMTwvcmRmOmxpPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjI3YmNlOTk1LTk3ZmEtMDg0OC04ODI5LTZkZjFhMGNjMDA4NSIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL3BuZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YzQwMzgzYy03Mzg4LWNlNDgtYTE4MC1kZTVjNGU0YTA0ZmEiIHN0RXZ0OndoZW49IjIwMjMtMDMtMjFUMTk6NDI6MjEtMDc6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmNlOTczNzJkLTdlZTgtZTg0NC1iODdjLTc1ODc3MzcxMzdkZCIgc3RFdnQ6d2hlbj0iMjAyNC0wNC0xMlQwNDowMToxNy0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZTgxNTM4MTQtOWQ0NC1h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'
: 'iVBORw0KGgoAAAANSUhEUgAAAPoAAAA1CAYAAABoUvZcAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMDGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDQtMTJUMDQ6MDItMDc6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDQtMTJUMDQ6MDItMDc6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjdmZjk2MmQtZTkyMi00YTQzLTgyMzYtMDYyY2Q5YWQyYjhiIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YmNjZTNmODEtMjI4Yy0xNjRmLWExMjAtMjRlMjg1ODk2ZGUxIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjdiY2U5OTUtOTdmYS0wODQ4LTg4MjktNmRmMWEwY2MwMDg1IiB0aWZmOk9yaWVudGF0aW9uPSIxIiB0aWZmOlhSZXNvbHV0aW9uPSI3MjAwMDAvMTAwMDAiIHRpZmY6WVJlc29sdXRpb249IjcyMDAwMC8xMDAwMCIgdGlmZjpSZXNvbHV0aW9uVW5pdD0iMiIgZXhpZjpDb2xvclNwYWNlPSIxIiBleGlmOlBpeGVsWERpbWVuc2lvbj0iNzMwIiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTU1Ij4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSInZ3B0JyIgcGhvdG9zaG9wOkxheWVyVGV4dD0iZ3B0Ii8+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6VGV4dExheWVycz4gPHBob3Rvc2hvcDpEb2N1bWVudEFuY2VzdG9ycz4gPHJkZjpCYWc+IDxyZGY6bGk+YWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjgyNTg1NWVhLThmZjYtNjk0OC04ODZmLWIxMmZmZDBjMGJlMTwvcmRmOmxpPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjI3YmNlOTk1LTk3ZmEtMDg0OC04ODI5LTZkZjFhMGNjMDA4NSIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yMFQyMzowNzowOC0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL3BuZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YzQwMzgzYy03Mzg4LWNlNDgtYTE4MC1kZTVjNGU0YTA0ZmEiIHN0RXZ0OndoZW49IjIwMjMtMDMtMjFUMTk6NDI6MjEtMDc6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmU1OWQ5NzU4LTk1ZjEtZGE0Ni05YmFiLTM2YjM1YWE0YzhiNiIgc3RFdnQ6d2hlbj0iMjAyNC0wNC0xMlQwNDowMi0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjdmZjk2MmQtZTkyMi00YTQzLTgyMzYt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' )
},
appStyle() {
appStyle.innerText = (
'.no-user-select { -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none }'
+ ( // 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
+ '.cursor-overlay {' // for fontSizeSlider.createAppend() drag listeners to show resize cursor everywhere
+ 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ; z-index: 9999 ; cursor: ew-resize }'
+ `#bravegpt { word-wrap: break-word ; white-space: pre-wrap ; margin-bottom: ${ isMobile ? -29 : 20}px ;`
+ 'clip-path: polygon(-8% -5%, 108% -5%, 109% 105%, -8% 105%) ;' // show tooltips but bound starry bg
+ 'padding: 24px 23px 45px 23px ;'
+ `background: radial-gradient(ellipse at bottom, ${ scheme == 'dark' ? '#2f3031 0%, #090a0f' : 'white 0%, white' } 100%) ;`
+ `border: ${ scheme == 'dark' ? 'none' : '1px solid var(--color-divider-subtle)' } ; border-radius: 18px }`
+ '#bravegpt:hover { box-shadow: 0 9px 28px rgba(0, 0, 0, 0.09) }'
+ '#bravegpt p { margin: 0 }'
+ `#bravegpt .alert-link { color: ${ scheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }}`
+ ( scheme == 'dark' ? '#bravegpt a { text-decoration: underline }' : '' ) // underline dark-mode links in alerts
+ '.app-name { font-size: 20px ; font-family: var(--brand-font) ; text-decoration: none ;'
+ `color: ${ scheme == 'dark' ? 'white' : 'black' } !important }`
+ '.kudoai { margin-left: 7px ; font-size: .65rem ; color: #aaa }'
+ '.kudoai a { color: #aaa ; text-decoration: none !important }'
+ `.kudoai a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' }}`
+ '.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: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' } ;`
+ `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1.185) ; transition: transform 0.05s ease' }}`
+ '#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% }'
+ '#font-size-slider-track { width: 98% ; height: 10px ; margin: -8px auto -9px ; padding: 15px 0 ;'
+ 'background-color: #ccc ; box-sizing: content-box; background-clip: content-box ; -webkit-background-clip: content-box }'
+ '#font-size-slider-track::before {' // to add finger cursor to unpadded core only
+ 'content: "" ; position: absolute ; top: 10px ; left: 0 ; right: 0 ; height: calc(100% - 20px) ; cursor: pointer }'
+ '#font-size-slider-thumb { width: 10px ; height: 27px ; border-radius: 30% ; position: relative ; top: -9px ;'
+ `transition: transform 0.05s ease ; background-color: ${ scheme == 'dark' ? 'white' : '#4a4a4a' } ;`
+ 'box-shadow: rgba(0, 0, 0, 0.21) 1px 1px 9px 0px ; cursor: ew-resize }'
+ ( config.fgAnimationsDisabled ? '' : '#font-size-slider-thumb:hover { transform: scale(1.125) }' )
+ '.standby-btn { width: 100% ; padding: 13px 0 ; cursor: pointer ; margin: 14px 0 20px ;'
+ `color: ${ scheme == 'dark' ? 'white' : 'black' } ;`
+ `border-radius: 4px ; border: 1px solid ${ scheme == 'dark' ? '#fff' : '#000' } ;`
+ 'transform: scale(1) ; transition: transform 0.1s ease }'
+ '.standby-btn:hover { border-radius: 4px ;'
+ `${ scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' };`
+ `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1.025)' }}`
+ '#bravegpt > pre {'
+ `font-size: ${config.fontSize}px ; font-family: Consolas, Menlo, Monaco, monospace ; white-space: pre-wrap ;`
+ `line-height: ${ config.fontSize * config.lineHeightRatio }px ; overscroll-behavior: contain ;`
+ 'margin-top: 12px ; padding: 1.2em 1.2em 0 1.2em ; border-radius: 13px ; overflow: auto ;'
+ `${ scheme == 'dark' ? 'background: #2b3a40cf ; color: #f2f2f2 ; border: 1px solid white'
: 'background: #eaeaeacf ; color: #282828 ; border: none' }}`
+ `#bravegpt footer { margin: ${ isFirefox ? 32 : 27 }px 0 -26px 0 ; border-top: none !important }`
+ '#bravegpt .feedback {'
+ 'float: right ; font-family: var(--brand-font) ; font-size: .55rem; color: #aaa ;'
+ 'letter-spacing: .02em ; position: relative ; right: -18px ; bottom: 15px }'
+ '#bravegpt .feedback .icon {'
+ ' fill: currentColor ; color: currentColor ; --size: 12px ; position: relative ; top: 0.19em ; right: 2px }'
+ `#bravegpt footer a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
+ '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
+ '.balloon-tip { content: "" ; position: relative ; border: 7px solid transparent ;'
+ 'float: left ; left: 7px ; margin: 36px -13px 0 0 ;' // positioning
+ 'border-bottom-style: solid ; border-bottom-width: 16px ; border-top: 0 ; border-bottom-color:'
+ ( scheme == 'dark' ? '#0000' : '#eaeaeacf' ) + '}'
+ '.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 }'
+ '#app-chatbar {'
+ `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: 12px 51px 12px 10px ;'
+ 'height: 43px ; line-height: 17px ; width: 100% ; max-height: 200px ; resize: none ;'
+ `background: ${ scheme == 'dark' ? '#5151519e' : '#eeeeee9e' };`
+ `position: relative ; z-index: 555 ; color: ${ scheme == 'dark' ? '#eee' : '#222' }}`
+ '.related-queries { display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: -18px ;'
+ 'position: relative ; top: -3px ;' // scooch up to hug feedback gap
+ `${ isFirefox ? '' : 'margin-top: -31px' }}`
+ '.related-query { margin: 4px 4px 2px 0 ; padding: 8px 13px 7px 14px ;'
+ `color: ${ scheme == 'dark' ? '#f2f2f2' : '#767676' } ;`
+ `background: ${ scheme == 'dark' ? '#595858d6' : '#fbfbfbb0' } ;`
+ `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)' };`
+ `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1) ; transition: transform 0.1s ease !important' }}`
+ '.related-query:hover, .related-query:focus {'
+ ( config.fgAnimationsDisabled ? '' : 'transform: scale(1.025) !important ;' )
+ `background: ${ scheme == 'dark' ? '#a2a2a270': '#dae5ffa3 ; 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(10px) ; transition: opacity 0.5s ease, transform 0.5s ease }'
+ '.fade-in-less { opacity: 0 ; transition: opacity 0.2s ease }'
+ '.fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) }'
+ '.chatbar-btn { z-index: 560 ;'
+ `border: none ; float: right ; position: relative ; bottom: ${ isFirefox ? 28 : 32 }px ; background: none ; cursor: pointer ;`
+ `${ scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }}`
+ '.chatbar-btn:hover {'
+ `${ scheme == 'dark' ? 'color: #white ; fill: #white ; stroke: #white' : 'color: #638ed4 ; fill: #638ed4 ; stroke: #638ed4' }}`
+ ( // markdown styles
'#bravegpt > pre h1 { font-size: 1.25em } #bravegpt > pre h2 { font-size: 1.1em }' // size headings
+ '#bravegpt > pre ul { margin: -10px 0 -6px ; }' // reduce v-spacing
+ '#bravegpt > pre ol { margin: -33px 0 -6px ; }' // reduce v-spacing
+ '#bravegpt > pre li { margin: -10px 0 ; list-style: inside }' ) // reduce v-spacing, show left symbols
+ '.katex-html { display: none }' // hide unrendered math
+ '.chatgpt-modal > div { padding: 24px 20px 24px 20px !important ;' // increase modal padding
+ 'background-color: white !important ; color: #202124 }'
+ '.chatgpt-modal h2 { font-size: 32px ; margin: 0 ; padding: 0 }' // shrink margin/padding around alert title + shrink it
+ '.modal-close-btn { top: -7px !important ; right: -7px !important }' // re-pos modal close button
+ `.modal-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}`
+ `.modal-close-btn:hover { background-color: #${ scheme == 'dark' ? '666464' : 'f2f2f2' } !important }`
+ '.chatgpt-modal p { margin: 14px 0 -20px 4px ; font-size: 1.115rem }' // pos/size modal msg
+ `.chatgpt-modal a { color: #${ scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
+ `.modal-buttons { margin: 38px 0px 6px ${ isMobile ? 0 : -7 }px !important }` // pos modal buttons
+ '.chatgpt-modal button {' // alert buttons
+ 'font-size: 0.72rem ; text-transform: uppercase ; min-width: 123px ; '
+ `padding: ${ isMobile? '5px' : '4px 3px' } !important ;`
+ 'cursor: pointer ; border-radius: 0 !important ; height: 39px ;'
+ 'border: 1px solid ' + ( scheme == 'dark' ? 'white' : 'black' ) + ' !important }'
+ '.primary-modal-btn { background: black !important ; color: white !important }'
+ '.chatgpt-modal button:hover { background-color: #9cdaff !important ; color: black !important ;'
+ `box-shadow: ${ scheme == 'dark' ? '2px 1px 54px #00cfff' : '2px 1px 30px #9cdaff' } !important }`
+ ( scheme == 'dark' ? // additional darkmode modal 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 }'
+ '.chatgpt-modal button:hover { background-color: #00cfff !important ; color: black !important }' ) : '' )
// Settings modal
+ '#bravegpt-settings-bg {'
+ 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
+ 'background-color: rgba(67, 70, 72, 0) ;' // init dim bg but no opacity
+ 'transition: background-color 0.05s ease ;' // speed to transition in show alert routine
+ 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
+ '#bravegpt-settings { font-family: var(--brand-font) ;'
+ 'opacity: 0 ; transform: translateX(-2px) translateY(3px) ; min-width: 288px ; max-width: 75vw ; word-wrap: break-word ;'
+ 'transition: opacity 0.1s cubic-bezier(.165,.84,.44,1), transform 0.3s cubic-bezier(.165,.84,.44,1) ;'
+ 'background-image: linear-gradient(180deg,'
+ `${ scheme == 'dark' ? '#75c451 -70%, black 57%' : '#4ef900 -31%, white 33%' }) ;`
+ `border: 1px solid ${ scheme == 'dark' ? 'white ; color: white' : '#b5b5b5 ; color: black' } ;`
+ 'padding: 11px ; margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
+ 'clip-path: polygon(-28% -3%, 128% -3%, 128% 125%, -28% 125%) ;' // bound starry bg
+ `${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}` // icon color
+ '#bravegpt-settings-bg.animated > div { opacity: 0.98 ; transform: translateX(0) translateY(0) }'
+ '@keyframes alert-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }'
+ '50% { opacity: 0.25 ; transform: scale(1.05) }'
+ '100% { opacity: 0 ; transform: scale(1.35) }}'
+ '#bravegpt-settings-title { font-weight: bold ; line-height: 19px ; text-align: center ; margin: 0 -6px -3px 0 }'
+ '#bravegpt-settings-title h4 { font-size: 26px ; font-weight: bold ; margin-top: -31px }' // 'Settings'
+ '#bravegpt-settings-close-btn {'
+ 'cursor: pointer ; width: 20px ; height: 20px ; border-radius: 17px ; float: right ;'
+ 'position: absolute ; top: 10px ; right: 13px }'
+ `#bravegpt-settings-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
+ '#bravegpt-settings-close-btn svg { margin: 6.5px }' // center SVG for hover underlay
+ `#bravegpt-settings-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
+ '#bravegpt-settings ul { list-style: none ; margin: 0 }' // hide bullets, override Brave ul margins
+ '#bravegpt-settings li { font-size: 14.5px ; transition: transform 0.1s ease ;'
+ `padding: 7px 10px ; border-bottom: 1px dotted ${ scheme == 'dark' ? 'white' : 'black' } ;` // add settings separators
+ 'border-radius: 3px }' // make highlight strips slightly rounded
+ '#bravegpt-settings li label { padding-right: 20px }' // right-pad labels so toggles don't hug
+ '#bravegpt-settings li:last-of-type { border-bottom: none }' // remove last bottom-border
+ '#bravegpt-settings li, #bravegpt-settings li label { cursor: pointer }' // add finger on hover
+ '#bravegpt-settings li:hover {'
+ 'background: rgba(100, 149, 237, 0.88) ; color: white ; fill: white ; stroke: white ;' // add highlight strip
+ `${ config.fgAnimationsDisabled || isMobile ? '' : 'transform: scale(1.16)' }}` // add zoom
+ '#bravegpt-settings li > input { float: right }' // pos toggles
+ `#about-menu-entry span { color: ${ scheme == 'dark' ? '#28ee28' : 'green' }}`
+ '#about-menu-entry > span {' // outer About status span
+ 'width: 92px ; overflow: hidden ; mask-image: linear-gradient(to right, transparent, black 20%, black 89%, transparent) }'
+ '#about-menu-entry > span > div { animation: ticker linear 60s infinite ; text-wrap: nowrap }'
+ '@keyframes ticker { 0% { transform: translateX(100%) } 100% { transform: translateX(-2000%) }}'
+ `.about-em { color: ${ scheme == 'dark' ? 'white' : 'green' } !important }`
)
},
footerContent() {
get.json('https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json',
(err, advertisersData) => { if (err) return
// Init vars
let chosenAdvertiser, adSelected
const re_appName = new RegExp(config.appName.toLowerCase(), 'i')
const currentDate = (() => { // in YYYYMMDD format
const today = new Date(), year = today.getFullYear(),
month = String(today.getMonth() + 1).padStart(2, '0'),
day = String(today.getDate()).padStart(2, '0')
return year + month + day
})()
// Select random, active advertiser
for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData))))
if (details.campaigns.text) { chosenAdvertiser = advertiser ; break }
// Fetch a random, active creative
if (chosenAdvertiser) {
const campaignsURL = 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/'
+ chosenAdvertiser + '/text/campaigns.json'
get.json(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(chatgpt.randomFloat() * 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 shuffle(list) {
let currentIdx = list.length, tempValue, randomIdx
while (currentIdx != 0) { // elements remain to be shuffled
randomIdx = Math.floor(chatgpt.randomFloat() * 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
}
},
scheme(newScheme) { scheme = newScheme ; update.appLogoSrc() ; update.appStyle() ; update.stars() },
stars() {
['sm', 'med', 'lg'].forEach(size =>
document.querySelectorAll(`[id*="stars-${size}"]`).forEach(starsDiv =>
starsDiv.id = config.bgAnimationsDisabled ? `stars-${size}-off`
: `${ scheme == 'dark' ? 'white' : 'black' }-stars-${size}`
))
},
titleAnchor() {
if (appDiv.querySelector('.loading, #bravegpt-alert')) return // only update reply UI
const appTitleVisible = !!appDiv.querySelector('.app-name'),
logoVisible = !!appDiv.querySelector('img')
// Create/fill/classify/style/append/update title anchor
if (!appTitleVisible || !logoVisible) {
const appTitleAnchor = createAnchor(config.appURL, (() => {
if (appLogoImg.loaded) { // size/return app logo img
appLogoImg.width = 143 ; return appLogoImg
} else { // create/fill/pos/return app name span
const appNameSpan = document.createElement('span')
appNameSpan.innerText = '🤖 ' + config.appName
appNameSpan.style.marginLeft = '3px'
return appNameSpan
}
})())
appTitleAnchor.classList.add('app-name', 'no-user-select')
if (!appTitleVisible) appDiv.append(appTitleAnchor)
else appDiv.querySelector('.app-name').replaceWith(appTitleAnchor) // for appLogoImg.onload() callback
}
},
tooltip(buttonType) { // text & position
const cornerBtnTypes = ['about', 'settings', 'speak', 'font-size', 'wsb']
.filter(type => appDiv.querySelector(`#${type}-btn`)) // exclude invisible ones
const [ctrAddend, spreadFactor] = appDiv.querySelector('.standby-btn') ? [9, 25] : [5, 28],
iniRoffset = spreadFactor * ( buttonType == 'send' ? 1.35
: buttonType == 'shuffle' ? 2.35
: cornerBtnTypes.indexOf(buttonType) +1 ) + ctrAddend
// Update text
tooltipDiv.innerText = (
buttonType == 'about' ? msgs.menuLabel_about || 'About'
: buttonType == 'settings' ? msgs.menuLabel_settings || 'Settings'
: buttonType == 'speak' ? msgs.tooltip_playAnswer || 'Play answer'
: buttonType == 'font-size' ? msgs.tooltip_fontSize || 'Font size'
: buttonType == 'wsb' ? (( config.widerSidebar ? `${ msgs.prefix_exit || 'Exit' } ` : '' )
+ ( msgs.menuLabel_widerSidebar || 'Wider Sidebar' ))
: buttonType == 'send' ? msgs.tooltip_sendReply || 'Send reply'
: buttonType == 'shuffle' ? msgs.tooltip_askRandQuestion || 'Ask random question' : '' )
// Update position
tooltipDiv.style.top = `${ !/send|shuffle/.test(buttonType) ? -6
: tooltipDiv.eventYpos - appDiv.getBoundingClientRect().top - 34 }px`
tooltipDiv.style.right = `${ iniRoffset - tooltipDiv.getBoundingClientRect().width / 2 }px`
},
tweaksStyle() {
// Update tweaks style based on settings (for tweaks init + show.reply() + toggle.sidebar())
tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
// Update 'by KudoAI' visibility based on corner space available
const kudoAIspan = appDiv.querySelector('.kudoai')
if (kudoAIspan) kudoAIspan.style.display = (
appDiv.querySelectorAll('.corner-btn').length < ( config.widerSidebar ? 10 : 5 )) ? '' : 'none'
}
}
// 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 fillStarryBG(targetNode) {
['sm', 'med', 'lg'].forEach((size, idx) => {
const starsDiv = document.createElement('div')
starsDiv.id = config.bgAnimationsDisabled ? `stars-${size}-off`
: `${ scheme == 'dark' ? 'white' : 'black' }-stars-${size}`
starsDiv.style.height = `${ idx +1 }px` // so toggle.bgAnimations() doesn't change height
targetNode.append(starsDiv)
})}
const fontSizeSlider = {
fadeInDelay: 5, // ms
hWheelDistance: 10, // px
createAppend: function() {
// Create/append slider elems
fontSizeSlider.cursorOverlay = document.createElement('div')
fontSizeSlider.cursorOverlay.classList.add('cursor-overlay') // for resize cursor
const slider = document.createElement('div') ; slider.id = 'font-size-slider-track'
slider.className = 'fade-in-less' ; slider.style.display = 'none'
const sliderThumb = document.createElement('div') ; sliderThumb.id = 'font-size-slider-thumb'
slider.append(sliderThumb)
appDiv.insertBefore(slider, appDiv.querySelector('.btn-tooltip,' // desktop
+ 'pre')) // mobile
// Init thumb pos
setTimeout(() => {
const iniLeft = (config.fontSize - config.minFontSize) / (config.maxFontSize - config.minFontSize) // left ratio
* (slider.offsetWidth - sliderThumb.offsetWidth) // slider width
sliderThumb.style.left = iniLeft + 'px'
}, fontSizeSlider.fadeInDelay) // to ensure visibility for accurate dimension calcs
// Add event listeners for dragging thumb
let isDragging = false, startX, startLeft
sliderThumb.addEventListener(inputEvents.down, event => {
event.preventDefault() // prevent text selection
isDragging = true ; startX = event.clientX ; startLeft = sliderThumb.offsetLeft
document.body.appendChild(fontSizeSlider.cursorOverlay)
})
document.addEventListener(inputEvents.move, event => {
if (isDragging) moveThumb(startLeft + event.clientX - startX) })
document.addEventListener(inputEvents.up, () => {
isDragging = false
if (fontSizeSlider.cursorOverlay.parentNode)
fontSizeSlider.cursorOverlay.parentNode.removeChild(fontSizeSlider.cursorOverlay)
})
// Add event listener for wheel-scrolling thumb
if (!isMobile) slider.onwheel = event => {
event.preventDefault()
moveThumb(sliderThumb.offsetLeft - Math.sign(event.deltaY) * fontSizeSlider.hWheelDistance)
}
// Add event listener for seek/dragging by inputEvents.down on track
slider.addEventListener(inputEvents.down, event => {
const clientX = event.clientX || event.touches?.[0]?.clientX
moveThumb(clientX - slider.getBoundingClientRect().left - sliderThumb.offsetWidth / 2)
isDragging = true ; startX = clientX ; startLeft = sliderThumb.offsetLeft // manually init dragging
document.body.appendChild(fontSizeSlider.cursorOverlay)
})
function moveThumb(newLeft) {
// Bound thumb
const sliderWidth = slider.offsetWidth - sliderThumb.offsetWidth
if (newLeft < 0) newLeft = 0
if (newLeft > sliderWidth) newLeft = sliderWidth
// Move thumb
sliderThumb.style.left = newLeft + 'px'
// Adjust font size based on thumb position
const answerPre = appDiv.querySelector('pre'),
fontSizePercent = newLeft / sliderWidth,
fontSize = config.minFontSize + fontSizePercent * (config.maxFontSize - config.minFontSize)
answerPre.style.fontSize = fontSize + 'px'
answerPre.style.lineHeight = fontSize * config.lineHeightRatio + 'px'
saveSetting('fontSize', fontSize)
}
return slider
},
toggle: function(state = '') {
const slider = document.getElementById('font-size-slider-track') || fontSizeSlider.createAppend()
// Toggle visibility
const balloonTip = appDiv.querySelector('.balloon-tip')
if (state == 'on' || (!state && slider.style.display == 'none')) {
slider.style.display = '' ; balloonTip.style.display = 'none'
setTimeout(() => slider.classList.add('active'), fontSizeSlider.fadeInDelay)
} else if (state == 'off' || (!state && slider.style.display != 'none')) {
slider.classList.remove('active') ; balloonTip.style.display = ''
setTimeout(() => slider.style.display = 'none', 55)
}
}
}
function handleRQevent(event) { // for attachment/removal in `get.reply()` + `show.reply().handleSubmit()`
const keys = [' ', 'Spacebar', 'Enter', 'Return'], keyCodes = [32, 13]
if (keys.includes(event.key) || keyCodes.includes(event.keyCode) || event.type == 'click') {
event.preventDefault() // prevent scroll on space taps
appDiv.querySelector('.related-queries').remove() // remove related queries
// Send related query
const chatbar = appDiv.querySelector('textarea')
if (chatbar) {
chatbar.value = event.target.textContent
show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
chatbar.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', bubbles: true, cancelable: true }))
}
}}
// Define FACTORY functions
function createAnchor(linkHref, displayContent, attrs = {}) {
const anchor = document.createElement('a'),
defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
finalAttrs = { ...defaultAttrs, ...attrs }
Object.entries(finalAttrs).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
}
function createStyle(content) {
const style = document.createElement('style')
if (content) style.innerText = content
return style
}
function createSVGelem(type, attrs) {
const elem = document.createElementNS('http://www.w3.org/2000/svg', type)
for (const attr in attrs) elem.setAttributeNS(null, attr, attrs[attr])
return elem
}
// Define TOGGLE functions
const toggle = {
animations(layer) {
saveSetting(layer + 'AnimationsDisabled', !config[layer + 'AnimationsDisabled'])
update[layer == 'bg' ? 'stars' : 'appStyle']()
notify(`${settingsProps[layer + 'AnimationsDisabled'].label} ${menuState.word[+!config[layer + 'AnimationsDisabled']]}`)
},
proxyMode() {
saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
notify(( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' ' + menuState.word[+config.proxyAPIenabled])
refreshMenu()
if (modals.settings.get()) { // update visual state of Settings toggle
const proxyToggle = document.querySelector('[id*="proxy"][id*="menu-entry"] input')
if (proxyToggle.checked != config.proxyAPIenabled) modals.settings.toggle.switch(proxyToggle)
}
if (appDiv.querySelector('bravegpt-alert')) location.reload() // re-send query if user alerted
},
relatedQueries() {
saveSetting('rqDisabled', !config.rqDisabled)
const relatedQueriesDiv = appDiv.querySelector('.related-queries')
if (relatedQueriesDiv) // update visibility based on latest setting
relatedQueriesDiv.style.display = config.rqDisabled ? 'none' : 'flex'
if (!config.rqDisabled && !relatedQueriesDiv) { // get related queries for 1st time
const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
get.related(lastQuery).then(queries => show.related(queries))
.catch(err => { consoleErr(err.message)
if (get.related.status != 'done') api.tryNew(get.related) })
}
update.tweaksStyle() // toggle <pre> max-height
notify(( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' ' + menuState.word[+!config.rqDisabled])
},
sidebar(mode) {
saveSetting(mode + 'Sidebar', !config[mode + 'Sidebar'])
update.tweaksStyle()
const wsbSVGs = document.querySelectorAll('#ws-svg')
if (mode == 'wider' && wsbSVGs.length > 0)
wsbSVGs.forEach(svg => icons.widescreen.update(svg))
notify(( msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' )
+ ' ' + menuState.word[+config[mode + 'Sidebar']])
},
streaming() {
const streamingToggle = document.querySelector('[id*="streaming"][id*="menu-entry"] input'),
scriptCatLink = isFirefox ? 'https://addons.mozilla.org/firefox/addon/scriptcat/'
: isEdge ? 'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh'
: 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf'
if (!/Tampermonkey|ScriptCat/.test(getUserscriptManager())) { // alert userscript manager unsupported, suggest TM/SC
siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
`${settingsProps.streamingDisabled.label} ${ msgs.alert_isOnlyAvailFor || 'is only available for' }`
+ ( !isEdge && !isBrave ? // suggest TM for supported browsers
` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${ msgs.alert_and || 'and' }`
: '' )
+ ` <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a>.` // suggest SC
+ ` (${ msgs.alert_userscriptMgrNoStream || 'Your userscript manager does not support returning stream responses' }.)`
)
if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
modals.settings.toggle.switch(streamingToggle)
} else if (getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave)) { // alert TM/browser unsupported, suggest SC
siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
`${settingsProps.streamingDisabled.label} ${ msgs.alert_isUnsupportedIn || 'is unsupported in' } `
+ `${ isChrome ? 'Chrome' : isEdge ? 'Edge' : 'Brave' } ${ msgs.alert_whenUsing || 'when using' } Tampermonkey. `
+ `${ msgs.alert_pleaseUse || 'Please use' } <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a> `
+ `${ msgs.alert_instead || 'instead' }.`
)
if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
modals.settings.toggle.switch(streamingToggle)
} else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
let msg = `${settingsProps.streamingDisabled.label} `
+ `${ msgs.alert_isCurrentlyOnlyAvailBy || 'is currently only available by' } `
+ `${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }. `
+ `(${ msgs.alert_openAIsupportSoon || 'Support for OpenAI API will be added shortly' }!)`
const switchPhrase = msgs.alert_switchingOn || 'switching on'
msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
const alertID = siteAlert(`${ msgs.mode_streaming || 'Streaming Mode' } ${ msgs.alert_unavailable || 'unavailable' }`, msg),
alert = document.getElementById(alertID)
alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() }
if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
modals.settings.toggle.switch(streamingToggle)
} else { // functional toggle
saveSetting('streamingDisabled', !config.streamingDisabled)
notify(settingsProps.streamingDisabled.label + ' ' + menuState.word[+!config.streamingDisabled])
}
},
tooltip(event) { // visibility
tooltipDiv.eventYpos = event.currentTarget.getBoundingClientRect().top // for update.tooltip() y-pos calc
update.tooltip(event.currentTarget.id.replace(/-btn$/, ''))
tooltipDiv.style.opacity = event.type == 'mouseover' ? 1 : 0
}
}
// 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() {
GM_deleteValue(config.keyPrefix + '_openAItoken')
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')
consoleInfo('OpenAI access token: ' + accessToken)
if (!accessToken) {
xhr({ url: openAIendpoints.session, onload: resp => {
if (isBlockedbyCloudflare(resp.responseText)) {
appAlert('checkCloudflare') ; return }
try {
const newAccessToken = JSON.parse(resp.responseText).accessToken
GM_setValue(config.keyPrefix + '_openAItoken', newAccessToken)
resolve(newAccessToken)
} catch { if (get.reply.api == 'OpenAI') appAlert('login') ; return }
}})
} else resolve(accessToken)
})}
function generateGPTforLoveKey() {
let nn = Math.floor(new Date().getTime() / 1e3)
const fD = e => {
let t = CryptoJS.enc.Utf8.parse(e),
o = CryptoJS.AES.encrypt(t, 'fjfsd我w4真3dd服iuhf了wf', {
mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
})
return o.toString()
}
return fD(nn)
}
// Define API functions
const api = {
pick: function(caller) {
const logPrefix = `get.${caller.name}() » `
const untriedAPIs = Object.keys(apis).filter(api =>
api != ( caller == get.reply ? 'OpenAI' : '' ) // exclude OpenAI for get.reply() since Proxy Mode
&& !caller.triedAPIs.some(entry => Object.prototype.hasOwnProperty.call(entry, api)) // exclude tried APIs
&& (config.streamingDisabled || apis[api].streamable)) // exclude unstreamable APIs if config.streamingDisabled
const chosenAPI = untriedAPIs[ // pick random array entry
Math.floor(chatgpt.randomFloat() * untriedAPIs.length)]
if (!chosenAPI) { consoleErr('No proxy APIs left untried') ; return null }
// Log chosen API endpoint
consoleInfo(logPrefix + 'Endpoint used: ' + apis[chosenAPI].endpoint)
return chosenAPI
},
tryNew: function(caller, reason = 'err') {
consoleErr(`Error using ${apis[caller.api].endpoint} due to ${reason}`)
if (caller.attemptCnt < Object.keys(apis).length -+(caller == get.reply)) {
consoleInfo('Trying another endpoint...')
caller.triedAPIs.push({ [caller.api]: reason }) ; caller.attemptCnt++
caller(caller == get.reply ? msgChain : stripQueryAugments(msgChain)[msgChain.length - 1].content)
.then(result => { if (caller == get.related) show.related(result) ; else return })
} else {
consoleInfo('No remaining untried endpoints')
if (caller == get.reply) appAlert('proxyNotWorking', 'suggestOpenAI')
}
},
clearTimedOut: function(triedAPIs) { // to retry on new queries
triedAPIs.splice(0, triedAPIs.length, // empty apiArray
...triedAPIs.filter(entry => Object.values(entry)[0] != 'timeout')) // replace w/ err'd APIs
},
createHeaders: function(api) {
const ip = ipv4.generate({ verbose: false })
let headers = { 'Content-Type': 'application/json', 'X-Forwarded-For': ip, 'X-Real-IP': ip }
if (api == 'OpenAI') headers.Authorization = 'Bearer ' + config.openAIkey
headers.Referer = headers.Origin = apis[api].expectedOrigin || '' // preserve expected traffic src
return headers
},
createPayload: function(api, msgs) {
let payload = {}
if (api == 'OpenAI')
payload = { messages: msgs, model: 'gpt-3.5-turbo', max_tokens: 4000 }
else if (api == 'AIchatOS') {
payload = {
prompt: msgs[msgs.length - 1].content,
withoutContext: false, userId: apiIDs.aiChatOS.userID, network: true
}
} else if (api == 'GPTforLove') {
payload = {
prompt: msgs[msgs.length - 1].content,
secret: generateGPTforLoveKey(), top_p: 1, temperature: 0.8,
systemMessage: 'You are ChatGPT, the version is GPT-4o, a large language model trained by OpenAI. Follow the user\'s instructions carefully.'
}
if (apiIDs.gptForLove.parentID) payload.options = { parentMessageId: apiIDs.gptForLove.parentID }
} else if (api == 'MixerBox AI')
payload = { prompt: msgs, model: 'gpt-3.5-turbo' }
return JSON.stringify(payload)
}
}
// Define QUERY AUGMENT functions
function augmentQuery(query) { return query + ` (reply in ${config.replyLanguage})` }
function stripQueryAugments(msgChain) {
const augmentCnt = augmentQuery.toString().match(/\+/g).length
return msgChain.map(msg => { // stripped chain
if (msg.role == 'user') {
let content = msg.content
const augments = content.match(/\s*\([^)]*\)\s*/g)
if (augments) for (let i = 0 ; i < augmentCnt ; i++) // strip augments
content = content.replace(augments[augments.length - 1 - i], '')
return { ...msg, content: content.trim() }
} else return msg // agent's unstripped
})
}
// Define GET functions
const get = {
async reply(msgChain) {
// Init API attempt props
get.reply.status = 'waiting'
if (!get.reply.triedAPIs) get.reply.triedAPIs = []
if (!get.reply.attemptCnt) get.reply.attemptCnt = 1
// Pick API
get.reply.api = config.proxyAPIenabled ? api.pick(get.reply) : 'OpenAI'
if (!get.reply.api) { // no more proxy APIs left untried
appAlert('proxyNotWorking', 'suggestOpenAI') ; return }
if (!config.proxyAPIenabled) // init OpenAI key
config.openAIkey = await Promise.race([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])
else setTimeout(() => { // try diff API after 6-9s of no response
if (config.proxyAPIenabled && get.reply.status != 'done' && !get.reply.sender)
api.tryNew(get.reply, 'timeout') }, config.streamingDisabled ? 9000 : 6000)
// Get/show answer from ChatGPT
xhr({
method: apis[get.reply.api].method, url: apis[get.reply.api].endpoint,
responseType: config.streamingDisabled || !config.proxyAPIenabled ? 'text' : 'stream',
headers: api.createHeaders(get.reply.api), data: api.createPayload(get.reply.api, msgChain),
onload: resp => dataProcess.text(get.reply, resp),
onloadstart: resp => dataProcess.stream(get.reply, resp),
onerror: err => { consoleErr(err.message)
if (!config.proxyAPIenabled) appAlert(!config.openAIkey ? 'login' : ['openAInotWorking', 'suggestProxy'])
else if (get.reply.status != 'done') api.tryNew(get.reply)
}
})
// Get/show related queries if enabled on 1st get.reply()
if (!config.rqDisabled && get.reply.attemptCnt == 1) {
const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
get.related(lastQuery).then(queries => show.related(queries))
.catch(err => { consoleErr(err.message)
if (get.related.status != 'done') api.tryNew(get.related) })
}
update.footerContent()
},
json(url, callback) { // for dynamic footer
xhr({ method: 'GET', url: url, onload: resp => {
if (resp.status >= 200 && resp.status < 300) {
try { const data = JSON.parse(resp.responseText) ; callback(null, data) }
catch (err) { callback(err, null) }
} else callback(new Error('Failed to load data: ' + resp.statusText), null)
}})
},
async related(query) {
// Init API attempt props
get.related.status = 'waiting'
if (!get.related.triedAPIs) get.related.triedAPIs = []
if (!get.related.attemptCnt) get.related.attemptCnt = 1
// Pick API
get.related.api = api.pick(get.related)
if (!get.related.api) return // no more proxy APIs left untried
// Init OpenAI key
if (get.related.api == 'OpenAI')
config.openAIkey = await Promise.race([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])
// Try diff API after 7s of no response
setTimeout(() => { if (get.related.status != 'done') api.tryNew(get.related, 'timeout') }, 7000)
return new Promise(resolve => {
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.'
+ ` Reply in ${config.replyLanguage}`
xhr({
method: apis[get.related.api].method, url: apis[get.related.api].endpoint,
responseType: 'text', headers: api.createHeaders(get.related.api),
data: api.createPayload(get.related.api, [{ role: 'user', content: rqPrompt }]),
onload: resp => dataProcess.text(get.related, resp).then(resolve),
onerror: err => { consoleErr(err.message) ; if (get.related.status != 'done') api.tryNew(get.related) }
})})
}
}
// Define PROCESS functions
const dataProcess = {
text(caller, resp) {
return new Promise(resolve => {
let respText = ''
const logPrefix = `get.${caller.name}() » dataProcess.text() » `
if (caller == get.reply && config.proxyAPIenabled && !config.streamingDisabled || caller.status == 'done')
return
if (resp.status != 200) {
consoleErr(logPrefix + 'Response status', resp.status)
consoleErr(logPrefix + 'Response', JSON.stringify(resp))
if (caller == get.reply && caller.api == 'OpenAI')
appAlert(resp.status == 401 ? 'login'
: resp.status == 403 ? 'checkCloudflare'
: resp.status == 429 ? ['tooManyRequests', 'suggestProxy']
: ['openAInotWorking', 'suggestProxy'] )
else if (caller.status != 'done')
api.tryNew(caller)
} else if (caller.api == 'OpenAI') {
if (resp.response) {
try { // to show response or return related queries
respText = JSON.parse(resp.response).choices[0].message.content
caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
} catch (err) { // suggest proxy or try diff API
consoleInfo(logPrefix + 'Response text: ' + resp.response)
consoleErr(logPrefix + appAlerts.parseFailed, err)
if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
else if (caller.status != 'done') api.tryNew(caller)
}
} else { // suggest proxy or try diff API
if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
else if (caller.status != 'done') api.tryNew(caller)
}
} else if (caller.api == 'AIchatOS') {
if (resp.responseText
&& !new RegExp([apis.AIchatOS.expectedOrigin, ...apis.AIchatOS.failFlags]
.map(str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // escape special chars
.join('|')).test(resp.responseText)) {
try { // to show response or return related queries
const text = resp.responseText, chunkSize = 1024
let currentIdx = 0
while (currentIdx < text.length) {
const chunk = text.substring(currentIdx, currentIdx + chunkSize)
currentIdx += chunkSize ; respText += chunk
}
if (!respText) throw new Error()
caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
} catch (err) { // try diff API
consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
consoleErr(logPrefix + appAlerts.parseFailed, err)
if (caller.status != 'done') api.tryNew(caller)
}
} else if (caller.status != 'done') api.tryNew(caller)
} else if (caller.api == 'GPTforLove') {
if (resp.responseText && !resp.responseText.includes('Fail')) {
try { // to show response or return related queries
let chunks = resp.responseText.trim().split('\n'),
lastObj = JSON.parse(chunks[chunks.length - 1])
if (lastObj.id) apiIDs.gptForLove.parentID = lastObj.id
respText = lastObj.text
caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
} catch (err) { // try diff API
consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
consoleErr(logPrefix + appAlerts.parseFailed, err)
if (caller.status != 'done') api.tryNew(caller)
}
} else if (caller.status != 'done') api.tryNew(caller)
} else if (caller.api == 'MixerBox AI') {
if (resp.responseText) {
try { // to show response or return related queries
const extractedData = Array.from(resp.responseText.matchAll(/data:(.*)/g), match => match[1]
.replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
.filter(match => !/(?:message_(?:start|end)|done)/.test(match))
respText = extractedData.join('')
caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
} catch (err) { // try diff API
consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
consoleErr(logPrefix + appAlerts.parseFailed, err)
if (caller.status != 'done') api.tryNew(caller)
}
} else if (caller.status != 'done') api.tryNew(caller)
}
function arrayify(strList) { // for get.related() calls
return (strList.match(/\d+\.\s*(.*?)(?=\n|$)/g) || [])
.slice(0, 5) // limit to 1st 5
.map(match => match.replace(/^\d+\.\s*/, '')) // strip numbering
}
})
},
stream(caller, stream) {
if (config.streamingDisabled || !config.proxyAPIenabled) return
const reader = stream.response.getReader() ; let accumulatedChunks = ''
reader.read().then(processStreamText).catch(err => consoleErr('Error processing stream', err.message))
function processStreamText({ done, value }) {
if (done) {
caller.status = 'done' ; caller.sender = null
api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
return
}
let chunk = new TextDecoder('utf8').decode(new Uint8Array(value))
if (caller.api == 'MixerBox AI') { // pre-process chunks
const extractedChunks = Array.from(chunk.matchAll(/data:(.*)/g), match => match[1]
.replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
.filter(match => !/(?:message_(?:start|end)|done)/.test(match))
chunk = extractedChunks.join('')
}
accumulatedChunks = apis[caller.api].accumulatesText ? chunk : accumulatedChunks + chunk
if (/['"]?status['"]?:\s*['"]Fail['"]/.test(accumulatedChunks)) { // GPTforLove fail
consoleErr('Response', accumulatedChunks)
if (caller.status != 'done' && !caller.sender) api.tryNew(caller)
return
}
try { // to show stream text
let textToShow
if (caller.api == 'GPTforLove') { // extract parentID + latest chunk text
const jsonLines = accumulatedChunks.split('\n'),
nowResult = JSON.parse(jsonLines[jsonLines.length - 1])
if (nowResult.id) apiIDs.gptForLove.parentID = nowResult.id // for contextual replies
textToShow = nowResult.text
} else textToShow = accumulatedChunks
if (textToShow && caller.status != 'done') { // text ready, app waiting or sending
if (!caller.sender) caller.sender = caller.api // app is waiting, become sender
if (caller.sender == caller.api) show.reply(textToShow, footerContent)
}
} catch (err) { consoleErr('Error showing stream', err.message) }
return reader.read().then(({ done, value }) => {
if (caller.sender == caller.api) // am designated sender, recurse
processStreamText({ done, value })
}).catch(err => consoleErr('Error reading stream', err.message))
}
}
}
// Define SHOW functions
const show = {
reply(answer) {
// Hide font size slider if visible
if (appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')
// Build answer interface up to reply section if missing
if (!appDiv.querySelector('pre')) {
while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear app content
fillStarryBG(appDiv) // add stars to bg
update.titleAnchor() // create/append app title anchor + byline
// Create/append About button
const aboutSpan = document.createElement('span'),
aboutSVG = icons.about.create()
aboutSpan.id = 'about-btn' // for toggle.tooltip()
aboutSpan.className = 'corner-btn' ; aboutSpan.style.marginTop = '0.8px'
aboutSpan.append(aboutSVG) ; appDiv.append(aboutSpan)
// Create/append Settings button
const settingsSpan = document.createElement('span'),
settingsSVG = icons.sliders.create()
settingsSpan.id = 'settings-btn' // for toggle.tooltip()
settingsSpan.className = 'corner-btn' ; settingsSpan.style.margin = '2px 9px 0 0'
settingsSpan.append(settingsSVG) ; appDiv.append(settingsSpan)
// Create/append Speak button
if (answer != 'standby') {
var speakerSpan = document.createElement('span'),
speakerSVG = icons.speaker.create()
speakerSpan.id = 'speak-btn' // for toggle.tooltip()
speakerSpan.className = 'corner-btn' ; speakerSpan.style.marginRight = '7px'
speakerSpan.append(speakerSVG) ; appDiv.append(speakerSpan)
}
// Create/append Font Size button
if (answer != 'standby') {
var fontSizeSpan = document.createElement('span'),
fontSizeSVG = icons.fontSize.create()
fontSizeSpan.id = 'font-size-btn' // for toggle.tooltip()
fontSizeSpan.className = 'corner-btn' ; fontSizeSpan.style.margin = '1px 10px 0 2px'
fontSizeSpan.append(fontSizeSVG) ; appDiv.append(fontSizeSpan)
}
// Create/append Wider Sidebar button
if (!isMobile) {
var wsbSpan = document.createElement('span'),
wsbSVG = icons.widescreen.create()
wsbSpan.id = 'wsb-btn' // for toggle.sidebar() + toggle.tooltip()
wsbSpan.className = 'corner-btn' ; wsbSpan.style.margin = '0.151em 11px 0 0'
wsbSpan.append(wsbSVG) ; appDiv.append(wsbSpan)
}
// Add tooltips
if (!isMobile) appDiv.append(tooltipDiv)
// Add corner button listeners
aboutSVG.onclick = modals.about.show
settingsSVG.onclick = modals.settings.show
if (speakerSVG) speakerSVG.onclick = () => {
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: appDiv.querySelector('pre').textContent, 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()
xhr({ // 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 resp => {
if (resp.status != 200) chatgpt.speak(answer, { voice: 2, pitch: 1, speed: 1.5 })
else {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
audioContext.decodeAudioData(resp.response, buffer => {
const audioSrc = audioContext.createBufferSource()
audioSrc.buffer = buffer
audioSrc.connect(audioContext.destination) // connect source to speakers
audioSrc.start(0) // play audio
})}}
})
}
if (fontSizeSVG) fontSizeSVG.onclick = () => fontSizeSlider.toggle()
if (wsbSVG) wsbSVG.onclick = () => toggle.sidebar('wider')
if (!isMobile) // add hover listeners for tooltips
[aboutSpan, settingsSpan, speakerSpan, fontSizeSpan, wsbSpan].forEach(span => {
if (span) span.onmouseover = span.onmouseout = toggle.tooltip })
// Create/append 'by KudoAI'
const kudoAIspan = document.createElement('span')
kudoAIspan.classList.add('kudoai', 'no-user-select') ; kudoAIspan.textContent = 'by '
kudoAIspan.style.cssText = 'position: relative ; bottom: 8px ; font-size: 12px'
kudoAIspan.append(createAnchor('https://www.kudoai.com', 'KudoAI'))
appDiv.querySelector('.app-name').insertAdjacentElement('afterend', kudoAIspan)
update.tweaksStyle() // show/hide based on corner space available
// Show standby state if prefix/suffix mode on
if (answer == 'standby') {
const standbyBtn = document.createElement('button')
standbyBtn.className = 'standby-btn'
standbyBtn.textContent = msgs.buttonLabel_sendQueryToGPT || 'Send search query to GPT'
appDiv.append(standbyBtn)
standbyBtn.onclick = () => {
appAlert('waitingResponse')
msgChain.push({ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) })
show.reply.submitSrc = 'click' ; show.reply.chatbarFocused = false
get.reply(msgChain)
}
// Otherwise create/append answer bubble
} else {
const answerPre = document.createElement('pre'),
balloonTipSpan = document.createElement('span')
balloonTipSpan.className = 'balloon-tip'
appDiv.append(balloonTipSpan, answerPre)
}
}
// Build reply section if missing
if (!appDiv.querySelector('#app-chatbar')) {
// Init/clear reply section content/classes
const replySection = appDiv.querySelector('section') || document.createElement('section')
while (replySection.firstChild) replySection.removeChild(replySection.firstChild)
replySection.classList.remove('loading', 'no-user-select')
// Create/append section elems
const replyForm = document.createElement('form'),
continueChatDiv = document.createElement('div'),
chatTextarea = document.createElement('textarea')
continueChatDiv.className = 'continue-chat'
chatTextarea.id = 'app-chatbar' ; chatTextarea.rows = '1'
chatTextarea.placeholder = ( answer == 'standby' ? msgs.placeholder_askSomethingElse || 'Ask something else'
: msgs.tooltip_sendReply || 'Send reply' ) + '...'
continueChatDiv.append(chatTextarea)
replyForm.append(continueChatDiv) ; replySection.append(replyForm)
appDiv.insertBefore(replySection, appDiv.querySelector('footer'))
// Create/append send button
const sendBtn = document.createElement('button'),
sendSVG = icons.upArrow.create()
sendBtn.id = 'send-btn' ; sendBtn.className = 'chatbar-btn'
sendBtn.style.right = '12px'
sendBtn.append(sendSVG) ; continueChatDiv.append(sendBtn)
// Create/append shuffle button
const shuffleBtn = document.createElement('div')
shuffleBtn.id = 'shuffle-btn' ; shuffleBtn.className = 'chatbar-btn'
shuffleBtn.style.right = '20px'
const shuffleSVG = icons.shuffledArrows.create()
shuffleBtn.append(shuffleSVG) ; continueChatDiv.append(shuffleBtn)
// Init/fill/append footer
const appFooter = appDiv.querySelector('footer') || document.createElement('footer')
appFooter.append(footerContent)
if (!appDiv.querySelector('footer')) appDiv.append(appFooter)
// Add reply section listeners
replyForm.onkeydown = handleEnter ; replyForm.onsubmit = handleSubmit
chatTextarea.oninput = autosizeChatbar
shuffleBtn.onclick = () => {
const randQAprompt = 'Generate a single random question on any topic then answer it.'
+ `${ !config.proxyAPIenabled ? 'Don\'t talk about Canberra, Tokyo, blue whales, photosynthesis,'
+ ' deserts, mindfulness meditation, the Fibonacci sequence,'
+ ' Jupiter, the Great Wall of China, Sheakespeare or da Vinci.' : '' }`
+ 'Try to give an answer that is 25-50 words.'
+ 'Do not type anything but the question and answer. Reply in markdown.'
chatTextarea.value = augmentQuery(randQAprompt)
show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
chatTextarea.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter', bubbles: true, cancelable: true }))
}
if (!isMobile) { // add hover listeners for tooltips
sendBtn.onmouseover = sendBtn.onmouseout = toggle.tooltip
shuffleBtn.onmouseover = shuffleBtn.onmouseout = toggle.tooltip
}
// Scroll to top on mobile if user interacted
if (isMobile && show.reply.submitSrc) {
document.body.scrollTop = 0 // Safari
document.documentElement.scrollTop = 0 // Chromium/FF/IE
}
}
// Render/show answer if query sent
if (answer != 'standby') {
const answerPre = appDiv.querySelector('pre')
answerPre.innerHTML = marked.parse(answer) // render markdown
hljs.highlightAll() // highlight code
// Typeset math
answerPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons
codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';<br>') })
const elemsToRenderMathIn = [answerPre, ...answerPre.querySelectorAll('*')]
elemsToRenderMathIn.forEach(elem => {
renderMathInElement(elem, { // typeset math
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
})})
// Auto-scroll if active
if (config.autoScroll && !isMobile && config.proxyAPIenabled && !config.streamingDisabled)
window.scrollBy({ top: appDiv.querySelector('footer').getBoundingClientRect().bottom - window.innerHeight + 13 })
}
// Focus chatbar conditionally
if (!config.autoFocusChatbarDisabled && !show.reply.chatbarFocused // do only once if enabled
&& !isMobile // exclude mobile devices to not auto-popup OSD keyboard
&& ( appDiv.offsetHeight < window.innerHeight - appDiv.getBoundingClientRect().top )) { // app fully above fold
appDiv.querySelector('#app-chatbar').focus() ; show.reply.chatbarFocused = true }
show.reply.submitSrc = 'none' // for reply section builder's mobile scroll-to-top if user interacted
function handleEnter(event) {
if (event.key == 'Enter' || event.keyCode == 13) {
if (event.ctrlKey) { // add newline
const chatTextarea = appDiv.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()
const chatTextarea = appDiv.querySelector('#app-chatbar')
// No reply, change placeholder + focus chatbar
if (chatTextarea.value.trim() == '') {
chatTextarea.placeholder = `${ msgs.placeholder_typeSomething || 'Type something' }...`
chatTextarea.focus()
// Yes reply, submit it + transform to loading UI
} else {
// Modify/submit msg chain
if (msgChain.length > 2) msgChain.splice(0, 2) // keep token usage maintainable
msgChain = stripQueryAugments(msgChain)
const prevReplyTrimmed = appDiv.querySelector('pre')?.textContent.substring(0, 250 - chatTextarea.value.length) || ''
msgChain.push({ role: 'assistant', content: prevReplyTrimmed })
msgChain.push({ role: 'user', content: augmentQuery(chatTextarea.value) })
get.reply(msgChain)
// Hide/remove elems
appDiv.querySelector('.related-queries')?.remove() // remove related queries
if (!isMobile) tooltipDiv.style.opacity = 0 // hide 'Send reply' tooltip post-send btn click
const appFooter = appDiv.querySelector('footer')
while (appFooter.firstChild) appFooter.removeChild(appFooter.firstChild)
// Show loading status
const replySection = appDiv.querySelector('section')
replySection.classList.add('loading', 'no-user-select')
replySection.innerText = appAlerts.waitingResponse
show.reply.chatbarFocused = false // for auto-focus routine
}
}
// Autosize chatbar function
const chatTextarea = appDiv.querySelector('#app-chatbar')
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, 10) < 55) { // if down to one line
chatTextarea.style.height = '43px' } // ...reset to original height
}
chatTextarea.style.height = `${ chatTextarea.scrollHeight > 60 ? ( chatTextarea.scrollHeight +2 ) : 43 }px`
prevLength = newLength
}
},
related(queries) {
if (!show.related.greenlit) { // wait for get.reply() to finish showing answer
show.related.statusChecker = setInterval(() => {
if (get.reply.status != 'waiting') {
show.related.greenlit = true
show.related(queries)
clearInterval(show.related.statusChecker)
}}, 500, queries)
} else { // show queries from latest statusChecker call
show.related.greenlit = false
if (queries && !appDiv.querySelector('.related-queries')) {
// 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
queries.forEach((query, idx) => {
const relatedQueryDiv = document.createElement('div'),
relatedQuerySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
relatedQuerySVGpath = document.createElementNS('http://www.w3.org/2000/svg','path')
// Add attributes
relatedQueryDiv.title = msgs.tooltip_sendRelatedQuery || 'Send related query'
relatedQueryDiv.classList.add('related-query', 'fade-in', 'no-user-select')
relatedQueryDiv.setAttribute('tabindex', 0)
relatedQueryDiv.textContent = query
// 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 elems
relatedQuerySVG.append(relatedQuerySVGpath) ; relatedQueryDiv.prepend(relatedQuerySVG)
relatedQueriesDiv.append(relatedQueryDiv)
// Add fade + listeners
setTimeout(() => {
relatedQueryDiv.classList.add('active')
relatedQueryDiv.onclick = relatedQueryDiv.onkeydown = handleRQevent
}, idx * 100)
})
update.tweaksStyle() // to shorten <pre> max-height
}}}
}
// Run MAIN routine
registerMenu() // create browser toolbar menu
// Init ALERTS
const appAlerts = {
waitingResponse: `${ msgs.alert_waitingResponse || 'Waiting for ChatGPT response' }...`,
login: `${ msgs.alert_login || 'Please login' } @ `,
checkCloudflare: `${ msgs.alert_checkCloudflare || 'Please pass Cloudflare security check' } @ `,
tooManyRequests: `${ msgs.alert_tooManyRequests || 'API is flooded with too many requests' }.`,
parseFailed: `${ msgs.alert_parseFailed || 'Failed to parse response JSON' }.`,
proxyNotWorking: `${ msgs.mode_proxy || 'Proxy Mode' } ${ msgs.alert_notWorking || 'is not working' }.`,
openAInotWorking: `OpenAI API ${ msgs.alert_notWorking || 'is not working' }.`,
suggestProxy: `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }`,
suggestOpenAI: `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOff || 'switching off' } ${ msgs.mode_proxy || 'Proxy Mode' }`
}
// Init scheme var
let scheme = config.scheme || ( isDarkMode() ? 'dark' : 'light' )
// Pre-load LOGO
const appLogoImg = document.createElement('img') ; update.appLogoSrc()
appLogoImg.onload = () => { appLogoImg.loaded = true ; update.titleAnchor() }
// Create/ID/classify/listenerize BRAVEGPT container
const appDiv = document.createElement('div') ; appDiv.id = 'bravegpt'
appDiv.classList.add('fade-in', // BraveGPT class
'snippet') // Brave class
appDiv.addEventListener(inputEvents.down, event => { // to dismiss visible font size slider
let elem = event.target
while (elem && !(elem.id?.includes('font-size'))) // find font size elem parent to exclude handling down event
elem = elem.parentNode
if (!elem && appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')
})
// Stylize APP elems
const appStyle = createStyle() ; update.appStyle() ; document.head.append(appStyle);
['hljs', 'wsbg', 'bsbg'].forEach(cssType => // code highlighting, white stars, black stars
document.head.append(createStyle(GM_getResourceText(`${cssType}CSS`))))
// Stylize SITE elems
const tweaksStyle = createStyle(),
wsbStyles = 'main.main-column, aside.sidebar { max-width: 521px !important }'
+ '#bravegpt { width: 521px }'
update.tweaksStyle() ; document.head.append(tweaksStyle)
// Create/stylize TOOLTIPs
if (!isMobile) {
var tooltipDiv = document.createElement('div') ; tooltipDiv.classList.add('btn-tooltip', 'no-user-select')
document.head.append(createStyle('.btn-tooltip {'
+ 'background-color: rgba(0, 0, 0, 0.64) ; padding: 5px 6px 3px ; border-radius: 6px ; border: 1px solid #d9d9e3 ;' // bubble style
+ 'font-size: 0.58rem ; color: white ;' // font style
+ 'position: absolute ;' // for update.tooltip() calcs
+ 'box-shadow: 3px 5px 16px 0px rgb(0 0 0 / 21%) ;' // drop shadow
+ 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 9999 }' // visibility
))
}
// APPEND to Brave
const hostContainer = document.querySelector(isMobile ? '#results' : '.sidebar')
setTimeout(() => {
hostContainer.prepend(appDiv)
setTimeout(() => appDiv.classList.add('active'), 100) // fade in
}, isMobile ? 500 : 100)
// Remove non-visible OVERFLOW STYLES for boundless hover fx
let appAncestor = hostContainer
while (appAncestor) {
if (getComputedStyle(appAncestor).overflow != 'visible') appAncestor.style.overflow = 'visible'
appAncestor = appAncestor.parentElement
}
// Init footer CTA to share feedback
let footerContent = createAnchor('#', msgs.link_shareFeedback || 'Share feedback', { target: '_self' })
footerContent.classList.add('feedback', 'svelte-8js1iq') // Brave classes
footerContent.onclick = modals.feedback.show
// Show STANDBY mode or get/show ANSWER
let msgChain = [{ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) }]
if (config.autoGetDisabled && !/src=(?:first-run|asktip)/.test(location.href) // Auto-Get disabled and not queried from other site or 1st run
|| config.prefixEnabled && !/.*q=%2F/.test(document.location) // prefix required but not present
|| config.suffixEnabled && !/.*q=.*(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(document.location)) { // suffix required but not present
show.reply('standby', footerContent)
if (!config.rqDisabled) {
const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
get.related(lastQuery).then(queries => show.related(queries))
.catch(err => { consoleErr(err.message)
if (get.related.status != 'done') api.tryNew(get.related) })
}
} else { appAlert('waitingResponse') ; get.reply(msgChain) }
// Observe/listen for Brave Search + system SCHEME CHANGES to update BraveGPT scheme if auto-scheme mode
(new MutationObserver(handleSchemeChange)).observe( // class changes from Brave Search theme settings
document.documentElement, { attributes: true, attributeFilter: ['class'] })
window.matchMedia('(prefers-color-scheme: dark)') // window.matchMedia changes from browser/system settings
.onchange = handleSchemeChange
function handleSchemeChange() {
if (config.scheme) return // since light/dark hard-set
const newScheme = isDarkMode() ? 'dark' : 'light'
if (newScheme != scheme) update.scheme(newScheme)
}
}, 1500)