// ==UserScript== // @name BraveGPT 🤖 // @description Add 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 Add 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 2025.7.31 // @license MIT // @icon https://assets.bravegpt.com/images/icons/app/icon48.png?v=e8ca7c2 // @icon64 https://assets.bravegpt.com/images/icons/app/icon64.png?v=e8ca7c2 // @compatible brave // @compatible chrome // @compatible chromebeta // @compatible chromecanary // @compatible chromedev // @compatible edge // @compatible edgebeta // @compatible edgecanary // @compatible edgedev // @compatible fennec // @compatible firefox // @compatible firefoxbeta // @compatible firefoxnightly // @compatible ghost // @compatible iceraven // @compatible ironfox // @compatible lemur // @compatible librewolf // @compatible mises // @compatible opera after allowing userscript manager access to search page results in opera://extensions // @compatible operaair after allowing userscript manager access to search page results in opera://extensions // @compatible operagx after allowing userscript manager access to search page results in opera://extensions // @compatible qq // @compatible quetta // @compatible safari // @compatible orion // @compatible vivaldi // @compatible waterfox // @compatible whale // @match *://search.brave.com/search* // @include https://auth0.openai.com // @connect am.aifree.site // @connect api.binjie.fun // @connect api.openai.com // @connect api11.gptforlove.com // @connect bravegpt.com // @connect cdn.jsdelivr.net // @connect chat-share.kudoai.workers.dev // @connect chatai.mixerbox.com // @connect chatgpt.com // @connect fanyi.sogou.com // @connect raw.githubusercontent.com // @require https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@3.8.2/dist/chatgpt.min.js#sha256-eo9NbR9z1wtCKV279LzVIkWkm60gbud2VFhAiBiaaB4= // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w= // @require https://cdn.jsdelivr.net/npm/json5@2.2.3/dist/index.min.js#sha256-S7ltnVPzgKyAGBlBG4wQhorJqYTehj5WQCrADCKJufE= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@88bcc7b/assets/js/components/chatbot/buttons.js#sha256-aZlxmH1b2UdwQrZ/1zZQbaTATtQ8yQKhWR4qd/utC3Y= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@0329ace/assets/js/components/chatbot/icons.js#sha256-p89CrELJj8rguE8M4IjdMA4CYwX7iid+RNzvX3oOT2A= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@31b78fe/assets/js/components/chatbot/menus.js#sha256-Bj0pz4oKLEzoaJxiqtddJ4E5VtMbpDB2sqoE4kDBfSs= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@97173cc/assets/js/components/chatbot/replyBubble.js#sha256-sOVj/ESmlNMLCA13w2ZzqP5hVtcnqlQpbY9yvkFWNek= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@f10be07/assets/js/components/chatbot/tooltip.js#sha256-u01988VokQnOZmjIIIPRN2mLJtaID/I8G77iMNcDOp8= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@bb5451a/assets/js/lib/chatbot/api.js#sha256-nCFc1tcSAfGJT260Sn07YGEczKPrhXdj8UlrKi+ac8M= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@ecaeb55/assets/js/lib/chatbot/feedback.js#sha256-9Hm3fBS96DtWFdT5VwGDGvwZMpYIRfxGAQRaCGECeqA= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@f4da9d4/assets/js/lib/chatbot/log.js#sha256-kjt26UXbx44I0/iDOf50F/LbRtsYcSwMHrexImR4D5A= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@199128d/assets/js/lib/chatbot/prompts.js#sha256-6U2C3dVLpYixR3UCNABCfvNpRa/9gJZYR8fElXmhGVk= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@9b048ff/assets/js/lib/chatbot/session.js#sha256-S6MOdBjx8Hci4GDvYl4JlhSdrDk2oaRLU9DrdxyiIss= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@4275a76/assets/js/lib/chatbot/ui.js#sha256-j2G0yOX1nHMYmwhV9oLlycdX05oZHLxqCW4voSfbchQ= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@b1e28ff/assets/js/lib/chatbot/userscript.js#sha256-SytCWuD3YOcYFDaVfpF8Pq67zDbV8cZcIENz+0zpZ40= // @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@9b048ff/assets/js/lib/dom.js/dist/dom.min.js#sha256-IGNj9Eoecq7QgY7SAs75wONajgN9Wg0NmCjKTCfu9CY= // @require https://cdn.jsdelivr.net/npm/generate-ip@2.4.4/dist/generate-ip.min.js#sha256-aQQKAQcMgCu8IpJp9HKs387x0uYxngO+Fb4pc5nSF4I= // @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 bgptIcon https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@a76e718/assets/images/icons/bravegpt/icon64.png.b64#sha256-Abqr6XIwT+g72ig2haUUkniR89b5UlxL28cAI6BVT/c= // @resource bgptLSlogo https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@a76e718/assets/images/logos/bravegpt/lightmode/logo730x155.png.b64#sha256-gGomHdYcs/AE4Ep8dAJhPFbCX6uyHmb38vi9hWYJZLI= // @resource bgptDSlogo https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@a76e718/assets/images/logos/bravegpt/darkmode/logo730x155.png.b64#sha256-2Qx4bTS8s7dKj4m2dsJdPnijThaYRwYQMi30+KjtopI= // @resource hljsCSS https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/base16/railscasts.min.css#sha256-nMf0Oxaj3sYJiwGCsfqNpGnBbcofnzk+zz3xTxtdLEQ= // @resource rpgCSS https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/gray.min.css#sha256-48sEWzNUGUOP04ur52G5VOfGZPSnZQfrF3szUr4VaRs= // @resource rpwCSS https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/white.min.css#sha256-6xBXczm7yM1MZ/v0o1KVFfJGehHk47KJjq8oTktH4KE= // @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 // @downloadURL https://gm.bravegpt.com // @updateURL https://gm.bravegpt.com // @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–2025 KudoAI & contributors under the MIT license // ✓ generate-ip (https://generate-ip.org) © 2024–2025 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 (async () => { // Init ENV context window.env = { browser: { language: chatgpt.getUserLanguage() }, scriptManager: { name: (() => { try { return GM_info.scriptHandler } catch (err) { return 'unknown' }})(), version: (() => { try { return GM_info.version } catch (err) { return 'unknown' }})() } } ; ['Chromium', 'Firefox', 'Chrome', 'Edge', 'Brave', 'Mobile'].forEach(platform => env.browser[`is${ platform == 'Firefox' ? 'FF' : platform }`] = chatgpt.browser['is' + platform]()) env.browser.isPortrait = env.browser.isMobile && ( innerWidth < innerHeight ) env.browser.isPhone = env.browser.isMobile && innerWidth <= 480 env.userLocale = env.browser.language.includes('-') ? env.browser.language.split('-')[1].toLowerCase() : '' env.scriptManager.supportsStreaming = /Tampermonkey|ScriptCat/.test(env.scriptManager.name) env.scriptManager.supportsTooltips = env.scriptManager.name == 'Tampermonkey' && parseInt(env.scriptManager.version.split('.')[0]) >= 5 window.inputEvents = {} ; ['down', 'move', 'up'].forEach(action => inputEvents[action] = ( window.PointerEvent ? 'pointer' : env.browser.isMobile ? 'touch' : 'mouse' ) + action) window.xhr = typeof GM != 'undefined' && GM.xmlHttpRequest || GM_xmlhttpRequest // Init APP data window.app = { version: GM_info.script.version, chatgptjsVer: /chatgpt\.js@([\d.]+)/.exec(GM_info.scriptMetaStr)[1], commitHashes: { app: '0a06922', // for cached .json aiweb: '70ba16f' // for cached .json } } app.urls = { resourceHost: `https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@${app.commitHashes.app}` } const remoteData = { app: await new Promise(resolve => xhr({ method: 'GET', url: `${app.urls.resourceHost}/assets/data/app.json`, onload: resp => resolve(JSON.parse(resp.responseText)) })), msgs: await new Promise(resolve => { const msgHostDir = app.urls.resourceHost + '/greasemonkey/_locales/', msgLocaleDir = ( env.browser.language ? env.browser.language.replace('-', '_') : 'en' ) + '/' let msgHref = msgHostDir + msgLocaleDir + 'messages.json', msgXHRtries = 0 function fetchMsgs() { xhr({ method: 'GET', url: msgHref, onload: handleMsgs })} function handleMsgs(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 original/region-stripped/EN only msgHref = env.browser.language.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try... msgHref.replace(/(_locales\/[^_]+)_[^_]+(\/)/, '$1$2') // ...strip region before retrying : ( msgHostDir + 'en/messages.json' ) // else use default English messages fetchMsgs() } } fetchMsgs() }) } Object.assign(app, { ...remoteData.app, urls: { ...app.urls, ...remoteData.app.urls }, msgs: remoteData.msgs }) app.urls.aiwebAssets = app.urls.aiwebAssets.replace('@latest', `@${app.commitHashes.aiweb}`) app.alerts = { waitingResponse: `${app.msgs.alert_waitingFor} ${app.name} ${app.msgs.alert_response}...`, login: `${app.msgs.alert_login} @ `, checkCloudflare: `${app.msgs.alert_checkCloudflare} @ `, tooManyRequests: `${app.msgs.alert_tooManyRequests}.`, parseFailed: `${app.msgs.alert_parseFailed}.`, proxyNotWorking: `${app.msgs.mode_proxy} ${app.msgs.alert_notWorking}.`, apiNotWorking: `API ${app.msgs.alert_notWorking}.`, suggestProxy: `${app.msgs.alert_try} ${app.msgs.alert_switchingOn} ${app.msgs.mode_proxy}`, suggestDiffAPI: `${app.msgs.alert_try} ${app.msgs.alert_selectingDiff} API`, suggestOpenAI: `${app.msgs.alert_try} ${app.msgs.alert_switchingOff} ${app.msgs.mode_proxy}` } app.katexDelimiters = await new Promise(resolve => xhr({ // used in show.reply() method: 'GET', onload: resp => resolve(JSON.parse(resp.responseText)), url: `${app.urls.aiwebAssets}/data/katex-delimiters.json` })) // Init API data window.apis = Object.assign(Object.create(null), await new Promise(resolve => xhr({ method: 'GET', onload: resp => resolve(JSON5.parse(resp.responseText)), url: `${app.urls.aiwebAssets}/data/ai-chat-apis.json5` }))) apis.AIchatOS.userID = '#/chat/' + Date.now() // Init SETTINGS window.config = {} window.settings = { load(...keys) { keys.flat().forEach(key => config[key] = GM_getValue(`${app.configKeyPrefix}_${key}`, initDefaultVal(key))) function initDefaultVal(key) { const ctrlData = settings.controls?.[key] return ctrlData?.defaultVal ?? ( ctrlData?.type == 'slider' ? 100 : ctrlData?.type == 'toggle' ) } }, save(key, val) { GM_setValue(`${app.configKeyPrefix}_${key}`, val) ; config[key] = val }, typeIsEnabled(key) { // for toggle.Gen auto-toggles + notifs const reInvertSuffixes = /disabled|hidden/i return reInvertSuffixes.test(key) // flag in control key name && !reInvertSuffixes.test(this.controls[key]?.label || '') // but not in label msg key name ? !config[key] : config[key] // so invert since flag reps opposite type state, else don't } } settings.load('debugMode') ; log.debug('Initializing settings...') Object.assign(settings, { controls: { // displays top-to-bottom, left-to-right in Settings modal proxyAPIenabled: { type: 'toggle', icon: 'sunglasses', defaultVal: false, label: app.msgs.menuLabel_proxyAPImode, helptip: app.msgs.helptip_proxyAPImode }, preferredAPI: { type: 'modal', icon: 'lightning', defaultVal: false, label: `${app.msgs.menuLabel_preferred} API`, helptip: app.msgs.helptip_preferredAPI }, streamingDisabled: { type: 'toggle', icon: 'signalStream', defaultVal: false, label: app.msgs.mode_streaming, helptip: app.msgs.helptip_streamingMode }, autoGetDisabled: { type: 'toggle', icon: 'speechBalloonLasso', defaultVal: false, label: app.msgs.menuLabel_autoAnswer, helptip: app.msgs.helptip_autoGetAnswers }, autoSummarize: { type: 'toggle', icon: 'summarize', defaultVal: false, label: app.msgs.menuLabel_autoSummarizeResults, helptip: app.msgs.helptip_autoSummarizeResults }, autoFocusChatbarDisabled: { type: 'toggle', mobile: false, icon: 'caretsInward', defaultVal: true, label: app.msgs.menuLabel_autoFocusChatbar, helptip: app.msgs.helptip_autoFocusChatbar }, autoScroll: { type: 'toggle', mobile: false, icon: 'arrowsDown', defaultVal: false, label: `${app.msgs.mode_autoScroll} (${app.msgs.menuLabel_whenStreaming})`, helptip: app.msgs.helptip_autoScroll }, rqDisabled: { type: 'toggle', icon: 'speechBalloons', defaultVal: false, label: `${app.msgs.menuLabel_show} ${app.msgs.menuLabel_relatedQueries}`, helptip: app.msgs.helptip_showRelatedQueries }, prefixEnabled: { type: 'toggle', icon: 'slash', defaultVal: false, label: `${app.msgs.menuLabel_require} "/" ${app.msgs.menuLabel_beforeQuery}`, helptip: app.msgs.helptip_prefixMode }, suffixEnabled: { type: 'toggle', icon: 'questionMark', defaultVal: false, label: `${app.msgs.menuLabel_require} "?" ${app.msgs.menuLabel_afterQuery}`, helptip: app.msgs.helptip_suffixMode }, widerSidebar: { type: 'toggle', mobile: false, icon: 'widescreenTall', defaultVal: false, label: app.msgs.menuLabel_widerSidebar, helptip: app.msgs.helptip_widerSidebar }, stickySidebar: { type: 'toggle', mobile: false, icon: 'sidebar', defaultVal: false, label: app.msgs.menuLabel_stickySidebar, helptip: app.msgs.helptip_stickySidebar }, anchored: { type: 'toggle', mobile: false, icon: 'anchor', defaultVal: false, label: app.msgs.mode_anchor, helptip: app.msgs.helptip_anchorMode }, bgAnimationsDisabled: { type: 'toggle', icon: 'sparkles', defaultVal: false, label: `${app.msgs.menuLabel_background} ${app.msgs.menuLabel_animations}`, helptip: app.msgs.helptip_bgAnimations }, fgAnimationsDisabled: { type: 'toggle', icon: 'sparkles', defaultVal: false, label: `${app.msgs.menuLabel_foreground} ${app.msgs.menuLabel_animations}`, helptip: app.msgs.helptip_fgAnimations }, replyLang: { type: 'prompt', icon: 'languageChars', label: app.msgs.menuLabel_replyLanguage, helptip: app.msgs.helptip_replyLanguage }, scheme: { type: 'modal', icon: 'scheme', label: app.msgs.menuLabel_colorScheme, helptip: app.msgs.helptip_colorScheme }, debugMode: { type: 'toggle', icon: 'bug', defaultVal: false, label: app.msgs.mode_debug, helptip: app.msgs.helptip_debugMode }, about: { type: 'modal', icon: 'questionMarkCircle', label: `${app.msgs.menuLabel_about} ${app.name}...` } }}) Object.assign(config, { lineHeightRatio: 1.313, maxFontSize: 24, minFontSize: 11 }) settings.load(Object.keys(settings.controls), 'expanded', 'fontSize', 'minimized') if (!config.replyLang) settings.save('replyLang', env.browser.language) // init reply language if unset if (!config.fontSize) settings.save('fontSize', 12.8791) // init reply font size if unset if (!env.scriptManager.supportsStreaming) settings.save('streamingDisabled', true) // disable Streaming in unspported env log.debug(`Success! config = ${log.prettifyObj(config)}`) // Define UI functions const themes = { apply(theme) { if (!this.styleNode) document.head.append(this.styleNode = dom.create.style()) this.styleNode.textContent = this.styles[theme] }, selectors: { btn: { get after() { return this.shared.split(',').map(sel => `${sel}::after`).join(', ') }, get before() { return this.shared.split(',').map(sel => `${sel}::before`).join(', ') }, get hover() { return this.shared.split(',').map(sel => `${sel}:hover`).join(', ') }, get hoverAfter() { return this.hover.split(',').map(sel => `${sel}::after`).join(', ') }, get hoverBefore() { return this.hover.split(',').map(sel => `${sel}::before`).join(', ') }, get hoverSVG() { return this.hover.split(',').map(sel => `${sel} svg`).join(', ') }, modal: `body:has(#${app.slug}) .modal-buttons button`, modalPrimary: `body:has(#${app.slug}) .primary-modal-btn`, get shared() { return `${this.modal},${this.standby}` }, get span() { return this.shared.split(',').map(sel => `${sel} span`).join(', ') }, standby: `button.${app.slug}-standby-btn`, get svg() { return this.shared.split(',').map(sel => `${sel} svg`).join(', ') } } }, styles: { get lines() { const { selectors } = themes ; return ` /* General button styles */ ${selectors.btn.shared} { --content-color: ${ env.ui.app.scheme == 'light' ? '0,0,0' : '255,255,255' }; --side-line-fill: linear-gradient(rgb(var(--content-color)), rgb(var(--content-color))) ; --skew: skew(-13deg) ; --counter-skew: skew(13deg) ; --btn-svg-zoom: scale(1.2) ; --btn-transition: 0.1s ease all ; position: relative ; border-width: 1px ; cursor: crosshair ; border: 1px solid rgb(var(--content-color)) ; background: /* side lines */ var(--side-line-fill) left / 2px 50% no-repeat, var(--side-line-fill) right / 2px 50% no-repeat ; background-position-y: 81% ; background-color: #ffffff00 ; /* clear bg */ color: rgba(var(--content-color), ${ env.ui.app.scheme == 'light' ? 0.85 : 1 }) ; font-size: 10px ; font-family: "Roboto", sans-serif ; text-transform: uppercase } ${selectors.btn.svg} { stroke: rgba(var(--content-color), ${ env.ui.app.scheme == 'light' ? 0.65 : 1 }) ; ${ config.fgAnimationsDisabled ? '' : `transition: var(--btn-transition) ; -webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ; -o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition)` }} ${selectors.btn.span} { font-weight: 600 ; display: inline-block } /* text */ ${selectors.btn.before}, ${selectors.btn.after} { /* top/bottom lines */ content: "" ; position: absolute ; background: rgb(var(--content-color)) ; ${ config.fgAnimationsDisabled ? '' : `transition: var(--btn-transition) ; -webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ; -o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition)` }} ${selectors.btn.before} { top: 0 ; left: 10% ; width: 65% ; height: 1px } /* top line */ ${selectors.btn.after} { bottom: 0 ; right: 10% ; width: 80% ; height: 1px } /* bottom line */ ${selectors.btn.hover} { color: rgb(var(--content-color)) ; background: /* extend side lines */ var(--side-line-fill) left / 2px 100% no-repeat, var(--side-line-fill) right / 2px 100% no-repeat !important } ${selectors.btn.hoverBefore} { left: 0 ; width: 20px } /* top line on hover */ ${selectors.btn.hoverAfter} { right: 0 ; width: 20px } /* bottom line on hover */ ${selectors.btn.hoverSVG} { transform: var(--btn-svg-zoom) ; stroke: rgba(var(--content-color),1) } /* Modal styles */ .${modals.class} { border-radius: 0 !important } /* square the corners to match the buttons */ /* Modal button styles */ ${selectors.btn.modal} { --modal-btn-y-offset: 2px ; --glow-color: #a0fdff ; --modal-btn-zoom: scale(1.075) ; --modal-btn-transition: transform 0.1s ease, background 0.2s ease, box-shadow 5s ease ; ${ config.fgAnimationsDisabled ? /* override chatgpt.js transitions */ `transition: none ; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none` : `transition: var(--modal-btn-transition) ; -webkit-transition: var(--modal-btn-transition) ; -moz-transition: var(--modal-btn-transition) ; -o-transition: var(--modal-btn-transition) ; -ms-transition: var(--modal-btn-transition)` }} ${selectors.btn.modalPrimary} { ${ env.ui.app.scheme == 'dark' ? 'background-color: white !important ; color: black' : 'background-color: black !important ; color: white' }} ${selectors.btn.modal}:nth-child(odd) { transform: translateY(calc(-1 * var(--modal-btn-y-offset))) } ${selectors.btn.modal}:nth-child(even) { transform: translateY(var(--modal-btn-y-offset)) } ${selectors.btn.modal}:nth-child(odd):hover { transform: translateY(calc(-1 * var(--modal-btn-y-offset))) ${ env.browser.isMobile ? '' : 'var(--modal-btn-zoom)' }} ${selectors.btn.modal}:nth-child(even):hover { transform: translateY(var(--modal-btn-y-offset)) ${ env.browser.isMobile ? '' : 'var(--modal-btn-zoom)' }} ${selectors.btn.modal}:hover { /* add glow */ background-color: var(--glow-color) !important ; box-shadow: 2px 1px 30px var(--glow-color) ; -webkit-box-shadow: 2px 1px 30px var(--glow-color) ; -moz-box-shadow: 2px 1px 30px var(--glow-color) } /* Standby button styles */ ${selectors.btn.standby} { --standby-btn-transition: transform 0.18s ease, background 0.2s ease ; font-size: 10px ; width: 80% ; height: 43px ; margin-bottom: 13px } ${selectors.btn.standby}:nth-child(odd) { margin-right: 20% ; margin-left: 15px } ${selectors.btn.standby}:nth-child(even) { margin-left: 20% ; margin-bottom: 19px } ${selectors.btn.standby}:hover { border : 1px rgba(var(--content-color), ${ env.ui.app.scheme == 'dark' ? '1) solid' : '0.6) dotted' }}` } } } window.update = { replyPreMaxHeight() { // for various mode toggles const replyPre = app.div.querySelector('.reply-pre'), relatedQueries = app.div.querySelector(`.${app.slug}-related-queries`), shorterPreHeight = innerHeight - relatedQueries?.offsetHeight - 304, longerPreHeight = innerHeight - 278 if (replyPre) replyPre.style.maxHeight = ( config.stickySidebar ? ( relatedQueries?.offsetHeight > 0 ? `${shorterPreHeight}px` : `${longerPreHeight}px` ) : config.anchored ? `${ longerPreHeight - ( config.expanded ? 115 : 365 ) }px` : 'none' ) }, appBottomPos() { app.div.style.bottom = `${ config.minimized ? 48 - app.div.offsetHeight : -32 }px` }, appStyle() { // used in toggle.animations() + update.scheme() + main's app init const isParticlizedDS = env.ui.app.scheme == 'dark' && !config.bgAnimationsDisabled, willNotZoom = config.fgAnimationsDisabled || env.browser.isMobile modals.stylize() // update modal styles if (!app.styles?.isConnected) document.head.append(app.styles ||= dom.create.style()) app.styles.textContent = ( // Init vars `:root { --app-bg-color-light-scheme: white ; --app-bg-color-dark-scheme: #1b1b1f ; --pre-bg-color-light-scheme: #b7b7b736 ; --pre-bg-color-dark-scheme: #3a3a3a ; --reply-header-bg-color-light-scheme: #d7d4d4 ; --reply-header-bg-color-dark-scheme: ${ !isParticlizedDS ? '#545454' : '#0e0e0e24' }; --reply-header-fg-color-light-scheme: white ; --reply-header-fg-color-dark-scheme: white ; --chatbar-btn-hover-color-light-scheme: #638ed4 ; --chatbar-btn-hover-color-dark-scheme: white ; --font-color-light-scheme: #4e4e4e ; --font-color-dark-scheme: #e3e3e3 ; --app-border: ${ isParticlizedDS ? 'none' : '1px solid var(--color-divider-subtle)' }; --app-gradient-bg: linear-gradient(180deg, ${ env.ui.app.scheme == 'dark' ? '#99a8a6 -245px, black 185px' : '#b6ebff -163px, white 65px' }) ; --app-shadow: 0 2px 3px rgb(0,0,0,0.06) ; --app-hover-shadow-light-scheme: 0 9px 28px rgba(0,0,0,0.09) ; --app-hover-shadow-dark-scheme: 0 9px 28px rgba(0,0,0,0.39) ; --app-anchored-shadow: 0 15px 52px rgb(0,0,${ env.ui.app.scheme == 'light' ? '7,0.06' : '11,0.22' }) ; --app-transition: opacity 0.5s ease, transform 0.5s ease, /* for 1st fade-in */ bottom 0.1s cubic-bezier(0,0,0.2,1), /* smoothen Anchor Y min/restore */ width 0.167s cubic-bezier(0,0,0.2,1) ; /* smoothen Anchor X expand/shrink */ --app-shadow-transition: box-shadow 0.15s ease ; /* for app:hover to not trigger on hover-off */ --standby-btn-zoom: scale(1.055) ; --standby-btn-transition: all 0.18s ease ; --btn-transition: transform 0.15s ease, /* for hover-zoom */ opacity 0.25s ease-in-out ; /* + btn-zoom-fade-out + .app-hover-only shows */ --font-size-slider-thumb-transition: transform 0.05s ease ; /* for hover-zoom */ --reply-pre-transition: max-height 0.167s cubic-bezier(0, 0, 0.2, 1) ; /* for Anchor changes */ --rq-transition: opacity 0.55s ease, transform 0.1s ease !important ; /* for fade-in + hover-zoom */ --fade-in-less-transition: opacity 0.2s ease } /* used by Font Size slider + Pin menu */` // Animations + `.fade-in { opacity: 0 ; transform: translateY(10px) } .fade-in-less { opacity: 0 ; transition: var(--fade-in-less-transition) ; -webkit-transition: var(--fade-in-less-transition) ; -moz-transition: var(--fade-in-less-transition) ; -o-transition: var(--fade-in-less-transition) ; -ms-transition: var(--fade-in-less-transition) } .fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) } @keyframes btn-zoom-fade-out { 0% { opacity: 1 } 55% { opacity: 0.25 ; transform: scale(1.85) } 75% { opacity: 0.05 ; transform: scale(2.15) } 100% { opacity: 0 ; transform: scale(6.85) }} @keyframes icon-scroll { 0% { transform: translateX(0) } 100% { transform: translateX(-14px) }} @keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }} @keyframes rotate { from { transform: rotate(0deg) } to { transform: rotate(360deg) }} @keyframes spinY { 0% { transform: rotateY(0deg) } 100% { transform: rotateY(360deg) }}` // Main styles + `.no-user-select { -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none } .no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent } /* stylize scrollbars in Chromium/Safari */ #${app.slug} *::-webkit-scrollbar { width: 7px } #${app.slug} *::-webkit-scrollbar-thumb { background: #cdcdcd } #${app.slug} *::-webkit-scrollbar-thumb:hover { background: #a6a6a6 } #${app.slug} *::-webkit-scrollbar-track { background: none } #${app.slug} * { 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 } #${app.slug} { /* main app div */ color: var(--font-color-${env.ui.app.scheme}-scheme) ; background: var(--app-bg-color-${env.ui.app.scheme}-scheme) ; position: sticky ; z-index: 1111 ; padding: 24px 22px 42px ; border-radius: 18px ; word-wrap: break-word ; white-space: pre-wrap ; transition: var(--app-transition) ; -webkit-transition: var(--app-transition) ; -moz-transition: var(--app-transition) ; -o-transition: var(--app-transition) ; -ms-transition: var(--app-transition) } #${app.slug}:has(.${app.slug}-alert) { /* app alerts */ border: var(--app-border) ; box-shadow: var(--app-shadow) ; -webkit-box-shadow: var(--app-shadow) ; -moz-box-shadow: var(--app-shadow) ; ${ config.bgAnimationsDisabled ? `background: var(--app-bg-color-${env.ui.app.scheme}-scheme)` : 'background-image: var(--app-gradient-bg)' }} #${app.slug}:has(.${app.slug}-alert):hover, #${app.slug}:has(.${app.slug}-alert):active { box-shadow: var(--app-hover-shadow-${env.ui.app.scheme}-scheme) ; transition: var(--app-shadow-transition) ; -webkit-transition: var(--app-shadow-transition) ; -moz-transition: var(--app-shadow-transition) ; -o-transition: var(--app-shadow-transition) ; -ms-transition: var(--app-shadow-transition) } ${ env.browser.isPhone ? '' : env.ui.app.scheme != env.ui.site.scheme ? // add hover shadow to bordered/un-anchored desktop app div `#${app.slug}:hover, #${app.slug}:active { box-shadow: var(--app-hover-shadow-${env.ui.app.scheme}-scheme) ; transition: var(--app-shadow-transition) ; -webkit-transition: var(--app-shadow-transition) ; -moz-transition: var(--app-shadow-transition) ; -o-transition: var(--app-shadow-transition) ; -ms-transition: var(--app-shadow-transition) }` : '' } #${app.slug} .app-hover-only { /* hide app-hover-only elems */ position: absolute ; left: -9999px ; opacity: 0 ; /* using position to support transitions */ width: 0 } /* to support width calcs */ /* show app-hover-only elems on hover + Font Size button when slider visible */ #${app.slug}:hover .app-hover-only, #${app.slug}:active .app-hover-only, #${app.slug}:has([id$=font-size-slider-track].active) [id$=font-size-btn] { position: relative ; left: auto ; width: auto ; opacity: 1 } #${app.slug} p { margin: 0 } #${app.slug} .alert-link { color: ${ env.ui.app.scheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }} .${app.slug}-name { font-size: 20px ; font-family: var(--brand-font) ; text-decoration: none ; color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' } !important } .byline { /* header byline */ position: relative ; bottom: 5.5px ; margin-left: 7px ; color: #aaa ; font-size: 11px ; --byline-transition: 0.15s ease-in-out ; transition: var(--byline-transition) ; -webkit-transition: var(--byline-transition) ; -moz-transition: var(--byline-transition) ; -o-transition: var(--byline-transition) ; -ms-transition: var(--byline-transition) } .byline a { color: #aaa ; text-decoration: none !important } .byline a:hover { color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }; transition: var(--byline-transition) ; -webkit-transition: var(--byline-transition) ; -moz-transition: var(--byline-transition) ; -o-transition: var(--byline-transition) ; -ms-transition: var(--byline-transition) } #${app.slug}-header-btns { display: flex ; direction: rtl ; gap: 2px ; float: right ; position: relative ; bottom: 3px } .${app.slug}-header-btn { float: right ; cursor: pointer ; position: relative ; top: 4px ; ${ env.ui.app.scheme == 'dark' ? 'fill: white ; stroke: white' : 'fill: #adadad ; stroke: #adadad' }} .${app.slug}-header-btn:hover svg { /* highlight/zoom header button on hover */ ${ env.ui.app.scheme == 'dark' ? 'fill: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' }; ${ env.browser.isMobile ? '' : 'transform: scale(1.285)' }} ${ config.fgAnimationsDisabled ? '' : `.${app.slug}-header-btn, .${app.slug}-header-btn svg { /* smooth header button fade-in + hover-zoom */ transition: var(--btn-transition) ; -webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ; -o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition) }`} .${app.slug}-header-btn:active { ${ env.ui.app.scheme == 'dark' ? 'fill: #999999 ; stroke: #999999' : 'fill: #638ed4 ; stroke: #638ed4' }} #${app.slug}-logo, .${app.slug}-header-btn svg { filter: drop-shadow(${ env.ui.app.scheme == 'dark' ? '#7171714d 10px' : '#aaaaaa21 7px' } 7px 3px) } #${app.slug} .loading { margin-bottom: -55px ; /* offset vs. app div bottom-padding footer accomodation */ color: #b6b8ba ; fill: #b6b8ba ; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite } #${app.slug} section.loading { padding: 0 0 14px 5px ; font-size: 90% } /* pad loading status */ #${app.slug}-font-size-slider-track { width: 98% ; height: 7px ; margin: -5px auto ${ env.browser.isPhone ? -4 : -9 }px ; padding: 15px 0 ; background-color: #ccc ; box-sizing: content-box; background-clip: content-box ; -webkit-background-clip: content-box } #${app.slug}-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 } #${app.slug}-font-size-slider-tip { z-index: 1 ; position: absolute ; bottom: 20px ; border-left: 4.5px solid transparent ; border-right: 4.5px solid transparent ; border-bottom: 16px solid #ccc } #${app.slug}-font-size-slider-thumb { z-index: 2 ; width: 7px ; height: 25px ; border-radius: 30% ; position: relative ; top: -8px ; cursor: ew-resize ; background-color: ${ env.ui.app.scheme == 'dark' ? 'white' : '#4a4a4a' }; --shadow: rgba(0,0,0,0.21) 1px 1px 9px 0 ; box-shadow: var(--shadow) ; -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow) ; ${ willNotZoom ? '' : `transition: var(--font-size-slider-thumb-transition) -webkit-transition: var(--font-size-slider-thumb-transition) ; -moz-transition: var(--font-size-slider-thumb-transition) ; -o-transition: var(--font-size-slider-thumb-transition) ; -ms-transition: var(--font-size-slider-thumb-transition)` }} ${ env.browser.isMobile ? '' : `#${app.slug}-font-size-slider-thumb:hover { transform: scale(1.125) }`} .${app.slug}-standby-btns { margin: 14px 0 22px } .${app.slug}-standby-btn { --skew: skew(-13deg) ; --counter-skew: skew(13deg) ; --content-color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }; display: flex ; align-items: center ; justify-content: center ; gap: 8px ; width: 90% ; height: 47px ; margin-bottom: 9px ; padding: 12px 0 ; cursor: pointer ; transform: var(--skew) ; border: 1px solid var(--content-color) ; background: none ; box-shadow: #aaaaaa12 7px 7px 3px 0px ; color: var(--content-color) ; font-size: 12px ; ${ config.fgAnimationsDisabled ? '' : `will-change: transform ; transition: var(--standby-btn-transition) ; -webkit-transition: var(--standby-btn-transition) ; -moz-transition: var(--standby-btn-transition) ; -o-transition: var(--standby-btn-transition) ; -ms-transition: var(--standby-btn-transition)` }} .${app.slug}-standby-btn:hover { color: var(--content-color) ; transform: var(--skew) ${ env.browser.isMobile ? '' : 'var(--standby-btn-zoom)' }} .${app.slug}-standby-btn > span { transform: var(--counter-skew) } .${app.slug}-standby-btn > svg { position: relative ; stroke: var(--content-color) ; fill: stroke: var(--content-color) ; transform: var(--counter-skew) } .${app.slug}-standby-btn:nth-child(odd) { margin-right: 10% } .${app.slug}-standby-btn:nth-child(even) { margin-left: 10% ; margin-bottom: 19px } .${app.slug}-standby-btn:first-of-type svg { /* Query button icon */ width: 11px ; height: 11px ; top: -1.5px ; right: -1.5px } .${app.slug}-standby-btn:nth-of-type(2) svg { /* Summarize button icon */ width: 17.5px ; height: 17.5px }` // AI reply elem styles + `#${app.slug} .reply-tip { content: "" ; position: relative ; border: 7px solid transparent ; float: left ; left: 7px ; margin: 4px -13px 0 -3px ; /* positioning */ border-bottom-style: solid ; border-bottom-width: 20px ; border-top: 0 ; border-bottom-color: ${ // hide reply tip for terminal aesthetic isParticlizedDS ? '#0000' : `var(--reply-header-bg-color-${env.ui.app.scheme}-scheme)` }} #${app.slug} .reply-header { display: flex ; align-items: center ; position: relative ; width: 100% ; top: 12px ; padding: 16px 14px ; height: 18px ; border-radius: 12px 12px 0 0 ; ${ env.ui.app.scheme == 'light' ? 'border-bottom: 1px solid white' : isParticlizedDS ? 'border: 1px solid ; border-bottom-color: transparent' : '' }; background: var(--reply-header-bg-color-${env.ui.app.scheme}-scheme) ; color: var(--reply-header-fg-color-${env.ui.app.scheme}-scheme) ; fill: var(--reply-header-fg-color-${env.ui.app.scheme}-scheme) ; stroke: var(--reply-header-fg-color-${env.ui.app.scheme}-scheme) } #${app.slug} .reply-header-txt { flex-grow: 1 ; font-size: 12px ; font-family: monospace } #${app.slug} .reply-header-btns { margin: 10.5px -5px 0 } #${app.slug} .reply-pre { font-size: ${config.fontSize}px ; white-space: pre-wrap ; font-family: Consolas, Menlo, Monaco, monospace ; line-height: ${ config.fontSize * config.lineHeightRatio }px ; overscroll-behavior: contain ; position: relative ; z-index: 1 ; /* allow top-margin to overlap header in light scheme */ margin-top: ${ env.ui.app.scheme == 'light' ? 10 : 12 }px ; padding: 1.2em 1.2em 0 1.2em ; border-radius: 0 0 12px 12px ; overflow: auto ; ${ config.bgAnimationsDisabled ? // classic opaque bg `background: var(--pre-bg-color-${env.ui.app.scheme}-scheme) ; color: var(--font-color-${env.ui.app.scheme}-scheme)` : env.ui.app.scheme == 'dark' ? // slightly tranluscent bg 'background: #2b3a40cf ; color: var(--font-color-dark-scheme) ; border: 1px solid white' : /* light scheme */ `background: var(--pre-bg-color-light-scheme) ; color: var(--font-color-light-scheme) ; border: none` }; ${ config.fgAnimationsDisabled ? '' : // smoothen Anchor mode expand/shrink `transition: var(--reply-pre-transition) ; -webkit-transition: var(--reply-pre-transition) ; -moz-transition: var(--reply-pre-transition) ; -o-transition: var(--reply-pre-transition) ; -ms-transition: var(--reply-pre-transition)` }} #${app.slug} .reply-pre a, #${app.slug} .reply-pre a:visited { color: #4495d4 } #${app.slug} .reply-pre a:hover { color: ${ env.ui.app.scheme == 'dark' ? 'white' : '#ea7a28' }} #${app.slug} .code-header { display: flex ; direction: rtl ; gap: 9px ; align-items: center ; height: 11px ; margin: 3px -2px 0 } #${app.slug} .code-header btn { cursor: pointer } #${app.slug} .code-header svg { height: 13px ; width: 13px ; fill: white }` // Rendered markdown styles + `#${app.slug} .reply-pre h1 { font-size: 1.25em } #${app.slug} .reply-pre h2 { font-size: 1.1em } #${app.slug} .reply-pre ul { margin: 2px 0 -6px } /* reduce v-spacing */ #${app.slug} .reply-pre ol { margin: -5px 0 -6px 7px } #${app.slug} .reply-pre li { /* reduce v-spacing, show hollow bullets */ margin: -10px 0 -6px 12px ; list-style: circle } #${app.slug} .reply-pre ul ul { margin-top: 0 } /* push sub-lists down */ #${app.slug} .reply-pre ul ul > li { list-style: disc } /* fill sub-bullets */` // Rendered code styles + `#${app.slug} ${GM_getResourceText('hljsCSS') // color code .replace(/\/\*[^*]+\*\//g, '') // strip comments .trim().replace(/([,}])(.)(?![^{]*\})/g, `$1#${app.slug} $2`)} /* scope selectors to app */ #${app.slug} pre:has(> code) { padding: 0 } /* remove padded border around code blocks */ #${app.slug} code { font-size: 0.85em } /* shrink code vs. regular text */` // Rendered math styles + '.katex-html { display: none } /* hide unrendered math */' // Chatbar styles + `#${app.slug}-chatbar { border: solid 1px ${ isParticlizedDS ? '#aaa' : env.ui.app.scheme == 'dark' ? '#777' : '#555' }; border-radius: 15px 16px 15px 0 ; margin: -6px 0 -7px 0 ; padding: 12px 51px 12px 10px ; position: relative ; z-index: 555 ; color: ${ env.ui.app.scheme == 'dark' ? '#eee' : '#222' }; height: 43px ; line-height: 17px ; width: 100% ; max-height: 200px ; resize: none ; background: ${ env.ui.app.scheme == 'light' ? '#eeeeee9e' : `#515151${ config.bgAnimationsDisabled ? '' : '9e' }`}; ${ env.ui.app.scheme == 'dark' ? '' : `--chatbar-inset-shadow: 0 1px 2px rgba(15,17,17,0.1) inset ; box-shadow: var(--chatbar-inset-shadow) ; -webkit-box-shadow: var(--chatbar-inset-shadow) ; -moz-box-shadow: var(--chatbar-inset-shadow) ;` } transition: box-shadow 0.15s ease ; -webkit-transition: box-shadow 0.15s ease ; -moz-transition: box-shadow 0.15s ease ; -o-transition: box-shadow 0.15s ease ; -ms-transition: box-shadow 0.15s ease } ${ isParticlizedDS ? '' : // add inset shadow to chatbar on hover `#${app.slug}-chatbar:hover:not(:focus) { --chatbar-hover-inset-shadow: 0 ${ env.ui.app.scheme == 'dark' ? '3px 2px' : '1px 7px' } rgba(15,17,17,0.15) inset ; box-shadow: var(--chatbar-hover-inset-shadow) ; -webkit-box-shadow: var(--chatbar-hover-inset-shadow) ; -moz-box-shadow: var(--chatbar-hover-inset-shadow) ; transition: box-shadow 0.25s ease ; -webkit-transition: box-shadow 0.25s ease ; -moz-transition: box-shadow 0.25s ease ; -o-transition: box-shadow 0.25s ease ; -ms-transition: box-shadow 0.25s ease }`} #${app.slug}-chatbar:focus-visible { /* fallback outline chatbar + reduce inset shadow on focus */ outline: -webkit-focus-ring-color auto 1px ; ${ isParticlizedDS ? '' : `--inset-shadow: 0 ${ env.ui.app.scheme == 'dark' ? '3px -1px' : '1px 2px' } rgba(0,0,0,0.3) inset ; box-shadow: var(--inset-shadow) ; -webkit-box-shadow: var(--inset-shadow) ; -moz-box-shadow: var(--inset-shadow)`}} .${app.slug}-chatbar-btn { z-index: 560 ; border: none ; float: right ; position: relative ; background: none ; cursor: pointer ; bottom: ${ env.browser.isFF ? 28 : 32 }px ; ${ env.ui.app.scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }} .${app.slug}-chatbar-btn:hover { color: var(--chatbar-btn-hover-color-${env.ui.app.scheme}-scheme) ; fill: var(--chatbar-btn-hover-color-${env.ui.app.scheme}-scheme) ; stroke: var(--chatbar-btn-hover-color-${env.ui.app.scheme}-scheme) }` // Related Queries styles + `.${app.slug}-related-queries { display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: -28px ; padding: 0 5px ; position: relative ; top: -3px } /* scooch up to hug feedback gap */ .${app.slug}-related-query { font-size: 0.77em ; cursor: pointer ; will-change: transform ; box-sizing: border-box ; width: fit-content ; max-width: 100% ; /* confine to outer div */ margin: 4px 12px 7px 0 ; padding: 8px 13px 7px 14px ; color: ${ env.ui.app.scheme == 'dark' ? ( config.bgAnimationsDisabled ? '#ccc' : '#f2f2f2' ) : '#767676' }; background: ${ env.ui.app.scheme == 'dark' ? '#7e7e7e4f' : '#fdfdfdb0' }; border: 1px solid ${ env.ui.app.scheme == 'dark' ? ( config.bgAnimationsDisabled ? '#5f5f5f' : '#777' ) : '#e1e1e1' }; border-radius: 0 13px 12px 13px ; flex: 0 0 auto ; --rq-shadow: 1px 4px 8px -6px rgba(169,169,169,0.75) ; box-shadow: var(--rq-shadow) ; -webkit-box-shadow: var(--rq-shadow) ; -moz-box-shadow: var(--rq-shadow) ; ${ willNotZoom ? '' : `transition: var(--rq-transition) ; -webkit-transition: var(--rq-transition) ; -moz-transition: var(--rq-transition) ; -o-transition: var(--rq-transition) ; -ms-transition: var(--rq-transition)` }} .${app.slug}-related-query:hover, .${app.slug}-related-query:focus { ${ config.fgAnimationsDisabled ? '' : 'transform: scale(1.055) !important ;' } background: ${ env.ui.app.scheme == 'dark' ? '#a2a2a270' : '#dae5ffa3 ; color: #000000a8 ; border-color: #a3c9ff' }} .${app.slug}-related-query svg { /* related query icon */ float: left ; margin: 0.09em 6px 0 0 ; color: ${ env.ui.app.scheme == 'dark' ? '#aaa' : '#c1c1c1' }}` // Footer styles + `#${app.slug} .feedback { float: right ; font-family: var(--brand-font) ; font-size: .55rem; color: #aaa ; letter-spacing: .02em ; position: relative ; right: -18px ; bottom: 15px } #${app.slug} footer { text-align: right ; margin: ${ env.browser.isFF ? 32 : 27 }px 18px -26px 0 ; padding-bottom: 12px } #${app.slug} footer a:hover { color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }; text-decoration: none }` // Notif styles + `.chatgpt-notif { fill: white ; stroke: white ; font-size: 25px !important ; padding: 6.5px 14px 8.5px 11.5px !important } .notif-close-btn { display: none !important }` // hide notif close btn // Menu styles + `.${app.slug}-menu { position: absolute ; z-index: 2250 ; padding: 3.5px 5px 4.5px !important ; font-family: "Source Sans Pro", sans-serif ; font-size: 12px } .${app.slug}-menu ul { margin: 0 ; padding: 0 ; list-style: none } .${app.slug}-menu-item { padding: 0 5px ; line-height: 20.5px } .${app.slug}-menu-item:not(.${app.slug}-menu-header):hover { cursor: pointer ; background: white ; color: black ; fill: black }` // Wider Sidebar styles + `#${app.slug}.wider { width: 489px } main.main-column:has(~ .sidebar #${app.slug}.wider), .sidebar:has(#${app.slug}.wider) { max-width: 505px !important }` // Sticky Sidebar styles + `#${app.slug}.sticky { position: sticky ; top: 83px } #${app.slug}.sticky ~ * { display: none }` // hide sidebar contents // Anchor Mode styles + `#${app.slug}.anchored { position: fixed ; bottom: -7px ; right: 35px ; width: 441px ; z-index: 8888 ; border: var(--app-border) ; box-shadow: var(--app-anchored-shadow) ; ${ config.bgAnimationsDisabled ? `background: var(--app-bg-color-${env.ui.app.scheme}-scheme)` : 'background-image: var(--app-gradient-bg)' }} #${app.slug}.expanded { width: 538px !important } #${app.slug}.anchored .anchored-hidden { display: none } /* hide non-Anchor elems in mode */ #${app.slug}:not(.anchored) .anchored-only { display: none } /* hide Anchor elems outside mode */` // Touch device styles + `@media (hover: none) { #${app.slug} .app-hover-only { /* show app-hover-only elems */ position: relative ; left: auto ; width: auto ; opacity: 1 } }` // Phone styles + `@media screen and (max-width: 480px) { #${app.slug} { border: var(--app-border) ; ${ config.bgAnimationsDisabled ? `background: var(--app-bg-color-${env.ui.app.scheme}-scheme)` : 'background-image: var(--app-gradient-bg)' }} #${app.slug} #${app.slug}-logo { width: calc(100% - 118px) } /* widen logo till btns */ #${app.slug} .byline { display: none !important } /* hide byline */ #${app.slug} .reply-tip { display: none } /* hide reply tip */ .${app.slug}-related-queries { padding: 0 } /* remove RQ parent padding */ }` ) themes.apply(config.theme) }, bylineVisibility() { if (env.browser.isPhone) return // since byline hidden by app.styles // Init header elems const headerElems = { byline: app.div.querySelector('.byline') } if (!headerElems.byline) return // since in loading state Object.assign(headerElems, { btns: app.div.querySelectorAll(`#${app.slug}-header-btns > btn`), logo: app.div.querySelector(`#${app.slug}-logo`) }) // Calc/store widths of app/x-padding + header elems const appDivStyle = getComputedStyle(app.div) const widths = { appDiv: app.div.getBoundingClientRect().width, appDivXpadding: parseFloat(appDivStyle.paddingLeft) + parseFloat(appDivStyle.paddingRight) } Object.entries(headerElems).forEach(([key, elem]) => widths[key] = dom.get.computedWidth(elem)) // Hide/show byline based on space available const availSpace = widths.appDiv - widths.appDivXpadding - widths.logo - widths.btns -10 Object.assign(headerElems.byline.style, widths.byline > availSpace ? { position: 'absolute', left: '-9999px', opacity: 0 } // hide using position to support transition : { position: '', left: '', opacity: 1 } // show ) }, async footerContent() { // Init advertisers data const advertisersData = await get.json( 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json' ).catch(err => log.error(err.message)) ; if (!advertisersData) return // Pick random advertiser let chosenAdvertiser for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData)))) if (details.campaigns.text) { chosenAdvertiser = advertiser ; break } if (!chosenAdvertiser) return // Init chosen advertiser's campaigns data const campaignsData = await get.json( `https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/${ chosenAdvertiser}/text/campaigns.json` ).catch(err => log.error(err.message)) ; if (!campaignsData) return // Init vars for ad selection const reAppName = new RegExp(app.name.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 })() ; let adSelected = false // 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-group for other apps /^self$/i.test(groupName) && !reAppName.test(campaignName) || ( // non-self group for this app reAppName.test(campaignName) && !/^self$/i.test(groupName)) || 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... // ...but user locale is missing or excluded !env.userLocale || !adGroup.targetLocations.some( loc => loc.includes(env.userLocale) || env.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) continue // to next group since no ads active const chosenAd = activeAds[Math.floor(chatgpt.randomFloat() * activeAds.length)] // 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', app.name.toLowerCase()) queryParams.set('utm_content', 'app_footer_link') destinationURL = `${baseURL}?${queryParams.toString()}` } // Update footer content const newFooterContent = destinationURL ? dom.create.anchor(destinationURL) : dom.create.elem('span') newFooterContent.className = braveClassList app.footerContent.replaceWith(newFooterContent) ; app.footerContent = newFooterContent app.footerContent.textContent = chosenAd.text app.footerContent.setAttribute('title', chosenAd.tooltip || '') adSelected = true ; break // out of group loop } if (adSelected) break // out of campaign loop } 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) / 100 const 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 } }, replyPrefix() { const firstP = app.div.querySelector('pre p') if (!firstP) return const prefixNeeded = env.ui.app.scheme == 'dark' && !config.bgAnimationsDisabled && !/shuffle|summarize/.test(get.reply.src) const prefixExists = firstP.textContent.startsWith('>> ') if (prefixNeeded && !prefixExists) firstP.prepend('>> ') else if (!prefixNeeded && prefixExists) firstP.textContent = firstP.textContent.replace(/^>> /, '') }, risingParticles() { ['sm', 'med', 'lg'].forEach(size => document.querySelectorAll(`[id*=particles-${size}]`).forEach(particlesDiv => particlesDiv.id = config.bgAnimationsDisabled ? `particles-${size}-off` : `${ env.ui.app.scheme == 'dark' ? 'white' : 'gray' }-particles-${size}` )) }, rqVisibility() { const rqsDiv = app.div.querySelector(`.${app.slug}-related-queries`) if (rqsDiv) // update visibility based on latest setting rqsDiv.style.display = config.rqDisabled || config.anchored ? 'none' : 'flex' }, scheme(newScheme) { env.ui.app.scheme = newScheme ; logos.bravegpt.update() ; update.appStyle() update.risingParticles() ; update.replyPrefix() ; modals.settings.updateSchemeStatus() } } // Define TOGGLE functions window.toggle = { anchorMode(state = '') { const prevState = config.anchored // for restraining notif if no change from Pin menu 'Sidebar' click let sidebarModeToggled = false // to extend this notif duration // Save new state + disable incompatible Sidebar modes if (state == 'on' || !state && !config.anchored) { settings.save('anchored', true) ;['sticky', 'wider'].forEach(mode => { if (config[`${mode}Sidebar`]) { toggle.sidebar(mode) ; sidebarModeToggled = true }}) } else { settings.save('anchored', false) if (config.expanded) { toggle.expandedMode('off') ; sidebarModeToggled = true } } if (prevState == config.anchored) return // Apply changed state to UI app.div.classList.toggle('anchored', config.anchored) update.rqVisibility() ; update.replyPreMaxHeight() ; update.bylineVisibility() if (modals.settings.get()) { // update visual state of Settings toggle const anchorToggle = document.querySelector('[id*=anchor] input') if (anchorToggle.checked != config.anchored) modals.settings.toggle.switch(anchorToggle) } feedback.notify(`${app.msgs.mode_anchor} ${menus.toolbar.state.words[+config.anchored]}`, null, sidebarModeToggled ? 2.75 : null) // +1s duration if conflicting mode notif shown }, animations(layer) { const configKey = `${layer}AnimationsDisabled` settings.save(configKey, !config[configKey]) update.appStyle() ; if (layer == 'bg') { update.risingParticles() ; update.replyPrefix() } if (layer == 'fg' && modals.settings.get()) { // toggle ticker-scroll of About status label const aboutStatusLabel = document.querySelector('#about-settings-entry > span > div') aboutStatusLabel.innerHTML = modals.settings.aboutContent[ config.fgAnimationsDisabled ? 'short' : 'long'] aboutStatusLabel.style.float = config.fgAnimationsDisabled ? 'right' : '' } feedback.notify(`${settings.controls[configKey].label} ${menus.toolbar.state.words[+!config[configKey]]}`) }, autoGen(mode) { const validModes = ['get', 'summarize'], modeKey = `auto${log.toTitleCase(mode)}${ mode == 'get' ? 'Disabled' : '' }` let conflictingModeToggled = false // to extend this notif duration settings.save(modeKey, !config[modeKey]) if (settings.typeIsEnabled(modeKey)) { // this Auto-Gen mode toggled on, disable other one + Manual-Gen const otherMode = validModes[+(mode == validModes[0])], otherModeKey = `auto${log.toTitleCase(otherMode)}${ otherMode == 'get' ? 'Disabled' : '' }` if (settings.typeIsEnabled(otherModeKey)) { toggle.autoGen(otherMode) ; conflictingModeToggled = true } ['prefix', 'suffix'].forEach(mode => { if (config[`${mode}Enabled`]) { toggle.manualGen(mode) ; conflictingModeToggled = true }}) } feedback.notify(`${settings.controls[modeKey].label} ${menus.toolbar.state.words[+settings.typeIsEnabled(modeKey)]}`, null, conflictingModeToggled ? 2.75 : null) // +1s duration if conflicting mode notif shown if (modals.settings.get()) { // update visual state of Settings toggle const modeToggle = document.querySelector(`[id*=${modeKey}] input`) if (modeToggle.checked != settings.typeIsEnabled(modeKey)) modals.settings.toggle.switch(modeToggle) } }, expandedMode(state = '') { const toExpand = state == 'on' || !state && !config.expanded settings.save('expanded', toExpand) ; app.div.classList.toggle('expanded', toExpand) if (config.minimized) toggle.minimized('off') // since user wants to see stuff if (getComputedStyle(app.div).transitionProperty.includes('width')) // update byline visibility app.div.addEventListener('transitionend', function onTransitionEnd(event) { // ...after width transition if (event.propertyName == 'width') { update.bylineVisibility() ; app.div.removeEventListener('transitionend', onTransitionEnd) }}) const expandBtn = app.div.querySelector(`#${app.slug}-arrows-btn`) if (expandBtn) expandBtn.firstChild.replaceWith( icons.create({ key: `arrowsDiagonal${ config.expanded ? 'In' : 'Out' }`, size: 17 })) }, manualGen(mode) { // Prefix/Suffix modes const modeKey = `${mode}Enabled` let autoGenToggled = false // to extend this notif duration settings.save(modeKey, !config[modeKey]) if (config[modeKey]) // Manual-Gen toggled on, disable all Auto-Gen ['get', 'summarize'].forEach(mode => { if (settings.typeIsEnabled(`auto${log.toTitleCase(mode)}${ mode == 'get' ? 'Disabled' : '' }`)) { toggle.autoGen(mode) ; autoGenToggled = true } }) feedback.notify(`${settings.controls[modeKey].label} ${menus.toolbar.state.words[+config[modeKey]]}`, null, autoGenToggled ? 2.75 : null) // +1s duration if conflicting mode notif shown) if (modals.settings.get()) { // update visual state of Settings toggle const modeToggle = document.querySelector(`[id*=${modeKey}] input`) if (modeToggle.checked != config[modeKey]) modals.settings.toggle.switch(modeToggle) } }, minimized(state = '') { const toMinimize = state == 'on' || !state && !config.minimized settings.save('minimized', toMinimize) const chevronBtn = app.div.querySelector('[id$=chevron-btn]') if (chevronBtn) { // update icon chevronBtn.textContent = '' chevronBtn.append(icons.create({ key: `chevron${ config.minimized ? 'Up' : 'Down' }`, size: 22, style: 'position: relative ; top: -1px' })) chevronBtn.onclick = () => { if (app.div.querySelector('[id$=font-size-slider-track]')?.classList.contains('active')) fontSizeSlider.toggle('off') toggle.minimized() } } update.appBottomPos() // toggle visual minimization tooltip.toggle('off') // hide lingering tooltip }, proxyMode() { settings.save('proxyAPIenabled', !config.proxyAPIenabled) feedback.notify(`${app.msgs.menuLabel_proxyAPImode} ${menus.toolbar.state.words[+config.proxyAPIenabled]}`) menus.toolbar.refresh() if (modals.settings.get()) { // update visual states of Settings toggles const proxyToggle = document.querySelector('[id*=proxy] input'), preferredAPIentry = document.querySelector('[id*=preferredAPI]'), streamingToggle = document.querySelector('[id*=streaming] input') if (proxyToggle.checked != config.proxyAPIenabled) // Proxy state out-of-sync (from using toolbar menu) modals.settings.toggle.switch(proxyToggle) preferredAPIentry.classList.toggle('active', config.proxyAPIenabled) preferredAPIentry.style.pointerEvents = config.proxyAPIenabled ? '' : 'none' if (streamingToggle.checked && !config.proxyAPIenabled // Streaming checked but OpenAI mode || // ...or Streaming unchecked but enabled in Proxy mode !streamingToggle.checked && config.proxyAPIenabled && !config.streamingDisabled) modals.settings.toggle.switch(streamingToggle) } const apiBeacon = app.div.querySelector(`#${app.slug} .api-btn`) if (apiBeacon) apiBeacon.style.pointerEvents = config.proxyAPIenabled ? '' : 'none' if (app.div.querySelector(`.${app.slug}-alert`)) // re-send query if user alerted get.reply({ msgs: app.msgChain, src: get.reply.src }) }, relatedQueries() { settings.save('rqDisabled', !config.rqDisabled) update.rqVisibility() if (!config.rqDisabled && !app.div.querySelector(`.${app.slug}-related-queries`)) // get related queries for 1st time get.related(app.msgChain[app.msgChain.length - 1]?.content || searchQuery) .then(queries => show.related(queries)) .catch(err => { log.error(err.message) ; api.tryNew(get.related) }) update.replyPreMaxHeight() feedback.notify(`${app.msgs.menuLabel_relatedQueries} ${menus.toolbar.state.words[+!config.rqDisabled]}`) }, sidebar(mode, state = '') { const configKeyName = mode + 'Sidebar', prevStickyState = config.stickySidebar // for hiding notif if no change from Pin menu 'Sidebar' click let anchorModeDisabled = false // to extend this notif duration // Save new state + disable incompatible Anchor mode if (state == 'on' || !state && !config[configKeyName]) { // toggle on if (mode == 'sticky' && config.anchored) { toggle.anchorMode() ; anchorModeDisabled = true } settings.save(configKeyName, true) } else settings.save(configKeyName, false) // Apply new state to UI app.div.classList.toggle(mode, config[configKeyName]) update.replyPreMaxHeight() ; update.bylineVisibility() if (mode == 'wider') // toggle icons everywhere document.querySelectorAll(`#${app.slug} svg.widescreenTall, #${app.slug} svg.widescreenWide`) .forEach(icon => icon.replaceWith( icons.create({ key: `widescreen${ config.widerSidebar ? 'Wide' : 'Tall' }`}))) if (modals.settings.get()) { // update visual state of Settings toggles const sidebarToggle = document.querySelector(`[id*=${mode}] input`) if (sidebarToggle.checked ^ config[`${mode}Sidebar`]) modals.settings.toggle.switch(sidebarToggle) } // Notify of mode change if (mode == 'sticky' && prevStickyState == config.stickySidebar) return feedback.notify(`${ app.msgs[`menuLabel_${ mode }Sidebar`] || log.toTitleCase(mode) + ' Sidebar' } ${ menus.toolbar.state.words[+config[configKeyName]]}`, null, anchorModeDisabled ? 2.75 : null) // +1s duration if conflicting mode notif shown }, streaming() { if (!env.scriptManager.supportsStreaming) { // alert userscript manager unsupported, suggest TM/SC const scLink = ( env.browser.isFF ? 'https://addons.mozilla.org/firefox/addon/scriptcat/' : env.browser.isEdge ? 'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh' : 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf' ) modals.alert( `${settings.controls.streamingDisabled.label} ${app.msgs.alert_unavailable}`, `${settings.controls.streamingDisabled.label} ${app.msgs.alert_isOnlyAvailFor}` + ` Tampermonkey ${ app.msgs.about_and}` + ` ScriptCat.` + ` (${app.msgs.alert_userscriptMgrNoStream}.)` ) } else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode let msg = `${settings.controls.streamingDisabled.label} ` + `${app.msgs.alert_isCurrentlyOnlyAvailBy} ` + `${app.msgs.alert_switchingOn} ${app.msgs.mode_proxy}. ` + `(${app.msgs.alert_openAIsupportSoon}!)` const switchPhrase = app.msgs.alert_switchingOn msg = msg.replace(switchPhrase, `${switchPhrase}`) const alert = modals.alert(`${app.msgs.mode_streaming} ${app.msgs.alert_unavailable}`, msg) alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() } } else { // functional toggle settings.save('streamingDisabled', !config.streamingDisabled) feedback.notify(`${settings.controls.streamingDisabled.label} ${ menus.toolbar.state.words[+!config.streamingDisabled]}`) } } } // Define GET functions window.get = { json(url) { return new Promise((resolve, reject) => { let retryCnt = 0; (function getData(url) { xhr({ method: 'GET', url: url, onload: resp => { if (resp.status == 404 && retryCnt < 1) { // try other format retryCnt++ ; getData(url.endsWith('.json') ? url + '5' : url.slice(0, -1)) } else handleResp(resp, resolve, reject) }, onerror: err => reject(new Error(`LOAD ERROR: ${err.message}`)) })})(url) function handleResp(resp, resolve, reject) { if (resp.status >= 300) { // status error const errType = resp.status >= 300 && resp.status < 400 ? 'REDIRECT' : resp.status >= 400 && resp.status < 500 ? 'CLIENT' : 'SERVER' return reject(new Error(`${errType} ERROR: ${resp.status}`)) } try { resolve(JSON5.parse(resp.responseText)) } catch (err) { reject(new Error(`PARSE ERROR: ${err.message}`)) } } }) }, async related(query) { // Init API attempt props get.related.status = 'waiting' get.related.triedAPIs = get.related.triedAPIs || [] 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( [session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))]) // Try diff API after 7s of no response const iniAPI = get.related.api get.related.query = query // expose to api.tryNew() in case modded clearTimeout(get.related.timeout) ; get.related.timeout = setTimeout(() => { if (get.related.status != 'done' // still no queries received && get.related.api == iniAPI // not already trying diff API from err && get.related.triedAPIs.length != Object.keys(apis).length // untried APIs remain ) api.tryNew(get.related, 'timeout') }, 7000) // Augment query const reqAPI = get.related.api let rqPrompt = prompts.create('relatedQueries', { prevQuery: query, mods: 'all' }) rqPrompt = prompts.augment(rqPrompt, { api: reqAPI }) // Get related queries return new Promise(resolve => { const reqMethod = apis[reqAPI].method const reqData = api.createReqData(reqAPI, [{ role: 'user', content: rqPrompt }]) const xhrConfig = { headers: api.createHeaders(reqAPI), method: reqMethod, responseType: 'text', onerror: err => { log.error(err) ; api.tryNew(get.related) }, onload: resp => api.process.text(resp, { caller: get.related, callerAPI: reqAPI }).then(resolve), url: apis[reqAPI].endpoints?.completions || apis[reqAPI].endpoint } if (reqMethod == 'POST') xhrConfig.data = JSON.stringify(reqData) else if (reqMethod == 'GET') xhrConfig.url += `?q=${reqData}` xhr(xhrConfig) }) }, async reply({ msgs, src = null }) { get.reply.src = src ; show.reply.updatedAPIinHeader = false // Show loading status const rqDiv = app.div.querySelector(`.${app.slug}-related-queries`), loadingSpinner = icons.create({ key: 'arrowsCyclic', size: 15 }) let loadingElem loadingSpinner.style.cssText = 'position: relative ; top: 1px ; margin-right: 6px' if (app.div.querySelector('.reply-pre')) { // reply exists, show where chatbar was if (!/regen|summarize/i.test(src)) rqDiv?.remove() // clear RQs to re-get later app.div.querySelector('footer').textContent = '' // clear footer loadingElem = app.div.querySelector('section') loadingElem.style.margin = `13px 0 ${ rqDiv?.isConnected ? ( env.browser.isFF ? -19 : -10 ) : -55 }px` loadingElem.textContent = app.alerts.waitingResponse loadingSpinner.style.animation = 'rotate 1s infinite cubic-bezier(0, 1.05, 0.79, 0.44)' // faster ver } else { // replace app div w/ alert feedback.appAlert('waitingResponse') loadingElem = app.div.querySelector(`.${app.slug}-alert`) loadingSpinner.style.animation = 'rotate 2s infinite linear' // slower ver } loadingElem.classList.add('loading', 'no-user-select') loadingElem.prepend(loadingSpinner) // Init msgs msgs = structuredClone(msgs) // deep copy to not affect app.msgChain if (msgs.length > 3) msgs = msgs.slice(-3) // keep last 3 only msgs.forEach(msg => { // trim agent msgs if (msg.role == 'assistant' && msg.content.length > 250) msg.content = msg.content.substring(0, 250) + '...' }) // Init API attempt props get.reply.status = 'waiting' get.reply.triedAPIs = get.reply.triedAPIs || [] 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 return feedback.appAlert(`${ config.preferredAPI ? 'api' : 'proxy' }NotWorking`, `suggest${ config.preferredAPI ? 'DiffAPI' : 'OpenAI' }`) // Init OpenAI key if (!config.proxyAPIenabled) config.openAIkey = await Promise.race( [session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))]) // Try diff API after 7-14s of no response else { const iniAPI = get.reply.api ; clearTimeout(get.reply.timeout) get.reply.timeout = setTimeout(() => { if (config.proxyAPIenabled // only do in Proxy mode && get.reply.status != 'done' && !get.reply.sender // still no reply received && get.reply.api == iniAPI // not already trying diff API from err && get.reply.triedAPIs.length != Object.keys(apis).length -1 // untried APIs remain ) api.tryNew(get.reply, 'timeout') }, ( config.streamingDisabled ? 10 : 7 *( config.preferredAPI ? 2 : 1 )) *1000) } // Augment query const reqAPI = get.reply.api, lastUserMsg = msgs[msgs.length - 1] lastUserMsg.content = prompts.augment(lastUserMsg.content, { api: reqAPI, caller: get.reply }) // Get/show answer from AI const reqMethod = apis[reqAPI].method const reqData = api.createReqData(reqAPI, msgs) const xhrConfig = { headers: api.createHeaders(reqAPI), method: reqMethod, responseType: config.streamingDisabled || !config.proxyAPIenabled ? 'text' : 'stream', onerror: err => { log.error(err) if (!config.proxyAPIenabled) feedback.appAlert(!config.openAIkey ? 'login' : ['OpenAI', 'notWorking', 'suggestProxy']) else api.tryNew(get.reply) }, onload: resp => api.process.text(resp, { caller: get.reply, callerAPI: reqAPI }), onloadstart: resp => api.process.stream(resp, { caller: get.reply, callerAPI: reqAPI }), url: apis[reqAPI].endpoints?.completions || apis[reqAPI].endpoint } if (reqMethod == 'POST') xhrConfig.data = JSON.stringify(reqData) else if (reqMethod == 'GET') xhrConfig.url += `?q=${reqData}` xhr(xhrConfig) // Get/show Related Queries if enabled/missing/on 1st get.reply() attempt only if (!config.rqDisabled && !rqDiv && get.reply.attemptCnt == 1) get.related(app.msgChain[app.msgChain.length - 1].content) .then(queries => show.related(queries)) .catch(err => { log.error(err.message) ; api.tryNew(get.related) }) update.footerContent() } } // Define SHOW functions window.show = { async codeCornerBtns() { if (!app.div.querySelector('code')) return // Init general language data window.codeLangData ||= await get.json(`${app.urls.aiwebAssets}/data/code-languages.json`) .catch(err => log.error(err.message)) // Add buttons to every block app.div.querySelectorAll('code').forEach(block => { if (block.querySelector('[id$=copy-btn]')) return const codeBtnsDiv = dom.create.elem('div', { class: `code-header` }) // Create Copy button const copyBtn = buttons.reply.bubble.copy.cloneNode(true) copyBtn.style.cssText = '' // clear app header btn styles Object.entries(buttons.reply.bubble.copy.listeners).forEach( ([eventType, handler]) => copyBtn[eventType] = handler) // Create Download button const downloadBtn = dom.create.elem('btn', { id: `${app.slug}-download-btn` }) const downloadSVGs = { download: icons.create({ key: 'download' }), downloaded: icons.create({ key: 'checkmarkDouble' })} Object.entries(downloadSVGs).forEach(([svgType, svg]) => { svg.id = `${app.slug}-${svgType}-icon` ;['width', 'height'].forEach(attr => svg.setAttribute(attr, 15)) }) downloadBtn.append(downloadSVGs.download) downloadBtn.onclick = event => { // download code, update icon + tooltip status if (!downloadBtn.firstChild.matches('[id$=download-icon]')) return // since clicking on DL'd icon // Update cursor/icon/tooltip downloadBtn.style.cursor = 'default' // remove finger downloadBtn.firstChild.replaceWith(downloadSVGs.downloaded.cloneNode(true)) // change to DL'd icon tooltip.update(event.currentTarget) // to 'Code downloaded!' setTimeout(() => { // restore icon/cursor/tooltip after a bit downloadBtn.firstChild.replaceWith(downloadSVGs.download.cloneNode(true)) downloadBtn.style.cursor = 'pointer' if (downloadBtn.matches(':hover')) // restore tooltip downloadBtn.dispatchEvent(new Event('mouseenter')) }, 10000) // Init block's language data const codeBlock = downloadBtn.closest('code'), blockLang = { hljsSlug: /language-(\w+)/.exec(codeBlock.className)?.[1] } if (blockLang.hljsSlug && window.codeLangData) for (const [langName, langEntry] of Object.entries(window.codeLangData)) if (langEntry.hljsSlug == blockLang.hljsSlug) { [blockLang.name, blockLang.fileExtension] = [langName, langEntry.fileExtension] break } // Download code const code = codeBlock.textContent.replace(/^>> /, '').trim() + '\n' const dlLink = dom.create.anchor(URL.createObjectURL(new Blob([code], { type: 'text/plain' }))) const now = new Date(), formattedDate = [ // YYYY-MM-DD now.getFullYear(), String(now.getMonth() +1).padStart(2, '0'), String(now.getDate()).padStart(2, '0') ].join('-') dlLink.download /* filename */ = `${app.slug}_${blockLang.name.toLowerCase() || 'code'}_${ formattedDate}_${Date.now().toString(36)}${ blockLang.fileExtension ? '.' + blockLang.fileExtension : '' }` document.body.append(dlLink) ; dlLink.click() ; dlLink.remove() // download code URL.revokeObjectURL(dlLink.href) // prevent memory leaks } downloadBtn.onmouseenter = downloadBtn.onmouseleave = tooltip.toggle // Assemble elems codeBtnsDiv.append(copyBtn, downloadBtn) ; block.prepend(codeBtnsDiv) }) }, related(queries) { log.caller = 'show.related()' if (get.reply.status == 'waiting') // recurse until get.reply() finishes showing answer return setTimeout(() => show.related(queries), 500, queries) // Re-get.related() if current reply is question to suggest answers const currentReply = app.div.querySelector(`#${app.slug} .reply-pre`)?.textContent.trim() if (!/shuffle|summarize/i.test(get.reply.src) && !get.related.replyIsQuestion && /[??]/.test(currentReply)) { log.debug('Re-getting related queries to answer reply question...') get.related.replyIsQuestion = true get.related(currentReply).then(queries => show.related(queries)) .catch(err => { log.error(err.message) ; api.tryNew(get.related) }) } // Show the queries else if (queries && !app.div.querySelector(`.${app.slug}-related-queries`)) { // Create/classify/append parent div const rqsDiv = dom.create.elem('div', { class: `${app.slug}-related-queries anchored-hidden` }) app.div.append(rqsDiv) // Fill each child div, add attributes + icon + listener queries.forEach((query, idx) => { const rqDiv = dom.create.elem('div', { title: app.msgs.tooltip_sendRelatedQuery, tabindex: 0, class: `${app.slug}-related-query fade-in no-user-select no-mobile-tap-outline` }) rqDiv.textContent = query ; rqDiv.prepend(icons.create({ key: 'arrowDownRight' })) rqsDiv.append(rqDiv) setTimeout(() => { // add fade + listeners rqDiv.classList.add('active') rqDiv.onclick = rqDiv.onkeydown = event => { 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 const chatbar = app.div.querySelector('textarea') ; if (!chatbar) return const relatedQuery = event.target.textContent ; chatbar.value = relatedQuery if (/\[[^[\]]+\]/.test(relatedQuery)) { // highlight 1st bracleted placeholder chatbar.focus() ui.addListeners.replySection.chatbarAutoSizer() // since query not auto-sent chatbar.setSelectionRange(relatedQuery.indexOf('['), relatedQuery.indexOf(']') +1) } else // send placeholder-free related query chatbar.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })) } } }, (idx+1) *50) }) update.replyPreMaxHeight() ; get.related.replyIsQuestion = null } }, reply({ content, standby = false, apiUsed = null }) { show.reply.shareURL = null // reset to regen using longer app.msgChain tooltip.toggle('off') // hide lingering tooltip if cursor was on corner button const regenSVGwrapper = app.div.querySelector('[id$=regen-btn]')?.firstChild if (regenSVGwrapper?.style?.animation) { // remove animation, restore cursor/tooltip regenSVGwrapper.style.animation = regenSVGwrapper.style.cursor = '' const regenBtn = regenSVGwrapper.closest('btn') if (regenBtn.matches(':hover')) // restore tooltip regenBtn.dispatchEvent(new Event('mouseenter')) } // Build answer interface up to reply section if missing if (!app.div.querySelector('.reply-pre')) { app.div.textContent = '' ; dom.addRisingParticles(app.div) // Create/append header div const appHeaderDiv = dom.create.elem('div', { class: 'app-header', style: 'margin: -8px 0' }) app.div.append(appHeaderDiv) // Create/append title const appHeaderLogo = logos.bravegpt.create() ; appHeaderLogo.width = 112 const appTitleAnchor = dom.create.anchor(app.urls.app, appHeaderLogo) appTitleAnchor.classList.add(`${app.slug}-name`, 'no-user-select') appHeaderDiv.append(appTitleAnchor) // Create/append header buttons div const headerBtnsDiv = dom.create.elem('div', { id: `${app.slug}-header-btns`, class: 'no-mobile-tap-outline' }) appHeaderDiv.append(headerBtnsDiv) // Create/append Chevron button if (!env.browser.isMobile) { var chevronBtn = dom.create.elem('btn', { id: `${app.slug}-chevron-btn`, class: `${app.slug}-header-btn anchored-only`, style: 'margin: 0.5px 1px 0 11px' }) chevronBtn.append(icons.create({ key: `chevron${ config.minimized ? 'Up' : 'Down' }`, size: 22, style: 'position: relative ; top: -1px' })) headerBtnsDiv.append(chevronBtn) } // Create/append About button const aboutBtn = dom.create.elem('btn', { id: `${app.slug}-about-btn`, class: `${app.slug}-header-btn`, style: 'margin-top: 1px' }) aboutBtn.append(icons.create({ key: 'questionMarkCircle' })) ; headerBtnsDiv.append(aboutBtn) // Create/append Settings button const settingsBtn = dom.create.elem('btn', { id: `${app.slug}-settings-btn`, class: `${app.slug}-header-btn`, style: 'margin: 2.5px 10.5px 0 3px' }) settingsBtn.append(icons.create({ key: 'sliders', size: 17 })) ; headerBtnsDiv.append(settingsBtn) // Create/append Font Size button if (!standby) { var fontSizeBtn = dom.create.elem('btn', { id: `${app.slug}-font-size-btn`, class: `${app.slug}-header-btn app-hover-only`, style: 'margin: 1px 10px 0 1px' }) fontSizeBtn.append(icons.create({ key: 'fontSize' })) ; headerBtnsDiv.append(fontSizeBtn) } // Create/append Pin button if (!env.browser.isMobile) { var pinBtn = dom.create.elem('btn', { id: `${app.slug}-pin-btn`, class: `${app.slug}-header-btn app-hover-only`, style: 'margin: 1.5px 9px 0 0' }) pinBtn.append(icons.create({ key: 'pin', size: 16.5 })) headerBtnsDiv.append(pinBtn) // Create/append Wider Sidebar button var wsbBtn = dom.create.elem('btn', { id: `${app.slug}-wsb-btn`, class: `${app.slug}-header-btn app-hover-only anchored-hidden`, style: 'margin: 2px 12px 0 0' }) wsbBtn.append(icons.create({ key: `widescreen${ config.widerSidebar ? 'Wide' : 'Tall' }`})) headerBtnsDiv.append(wsbBtn) // Create/append Expand/Shrink button var arrowsBtn = dom.create.elem('btn', { id: `${app.slug}-arrows-btn`, class: `${app.slug}-header-btn app-hover-only anchored-only`, style: 'margin: 2.5px 13.5px 0 0' }) arrowsBtn.append(icons.create({ key: `arrowsDiagonal${ config.expanded ? 'In' : 'Out' }`, size: 17 })) headerBtnsDiv.append(arrowsBtn) } // Add app header button listeners ui.addListeners.btns.appHeader() // Create/append 'by KudoAI' const bylineSpan = dom.create.elem('span', { class: 'byline no-user-select' }) bylineSpan.textContent = 'by ' bylineSpan.append(dom.create.anchor(app.urls.publisher, 'KudoAI')) appHeaderDiv.querySelector(`.${app.slug}-name`).insertAdjacentElement('afterend', bylineSpan) update.bylineVisibility() // Show standby state if prefix/suffix mode on if (standby) { const standbyBtnsDiv = dom.create.elem('div', { class: `${app.slug}-standby-btns`, style: 'will-change: transform' }) ;['query', 'summarize'].forEach(btnType => { const btn = { node: dom.create.elem('button', { class: `${app.slug}-standby-btn no-mobile-tap-outline` }), icon: icons.create({ key: btnType == 'query' ? 'send' : 'summarize' }), textSpan: dom.create.elem('span') } btn.textSpan.textContent = btnType == 'query' ? `${app.msgs.btnLabel_sendSearchQueryTo} ${app.name}` : app.msgs.tooltip_summarizeResults btn.node.onclick = show.reply[`${btnType}BtnClickHandler`] = () => { show.reply.userInteracted = true ; show.reply.chatbarFocused = false app.msgChain.push({ role: 'user', content: btnType == 'summarize' ? prompts.create('summarizeResults') : new URL(location.href).searchParams.get('q') }) get.reply({ msgs: app.msgChain, src: btnType }) } btn.node.append(btn.icon, btn.textSpan) ; standbyBtnsDiv.append(btn.node) }) app.div.append(standbyBtnsDiv) // Otherwise create/append answer bubble section } else replyBubble.insert() } // Build reply section if missing if (!app.div.querySelector(`#${app.slug}-chatbar`)) { // Init/clear user reply section content/classes/style const replySection = app.div.querySelector('section') || dom.create.elem('section') if (replySection.className.includes('loading')) replySection.textContent = replySection.className = replySection.style = '' // Create/append section elems const replyForm = dom.create.elem('form') const continueChatDiv = dom.create.elem('div') const chatTextarea = dom.create.elem('textarea', { id: `${app.slug}-chatbar`, rows: 1, placeholder: `${app.msgs[standby ? 'placeholder_askSomethingElse' : 'tooltip_sendReply']}...` }) continueChatDiv.append(chatTextarea) replyForm.append(continueChatDiv) ; replySection.append(replyForm) app.div.querySelector('.reply-bubble, [class*=standby-btns]').after(replySection) // Create/append chatbar buttons const rOffset = 12, spreadFactor = 7.5 ;['send', 'shuffle', 'summarize'].forEach((btnType, idx) => { if (btnType == 'summarize' && app.div.querySelector('[class*=standby-btn]')) return // since big Summarize button exists const btn = dom.create.elem('button', { id: `${app.slug}-${btnType}-btn`, class: `${app.slug}-chatbar-btn no-mobile-tap-outline` }) btn.style.right = `${ rOffset + idx * spreadFactor }px` if (btnType == 'shuffle') btn.style.right = '17px' btn.append(icons.create({ key: btnType, size: btnType == 'send' ? 14 : 18 })) continueChatDiv.append(btn) }) // Init/fill/append footer app.footer = app.div.querySelector('footer') || dom.create.elem('footer') app.footer.append(app.footerContent) if (!app.div.querySelector('footer')) app.div.append(app.footer) // Add listeners ui.addListeners.replySection() // Scroll to top on mobile if user interacted if (env.browser.isMobile && show.reply.userInteracted) { document.body.scrollTop = 0 // Safari document.documentElement.scrollTop = 0 // Chromium/FF/IE } } saveAppDiv() // to fight Brave mutations // Render/show answer if query sent if (!standby) { // Show API used in bubble header if (!show.reply.updatedAPIinHeader) { show.reply.updatedAPIinHeader = true const preHeaderLabel = app.div.querySelector('.reply-header-txt'), apiBeacon = dom.create.elem('span', { class: 'api-btn', style: 'cursor: pointer' }) apiBeacon.textContent = '⦿' apiBeacon.onmouseenter = apiBeacon.onmouseleave = apiBeacon.onclick = menus.hover.toggle apiBeacon.style.pointerEvents = config.proxyAPIenabled ? '' : 'none' preHeaderLabel.replaceChildren( apiBeacon, ` API ${app.msgs.componentLabel_used}: `, dom.create.elem('b')) setTimeout(() => type(apiUsed, preHeaderLabel.lastChild, { speed: 1.5 }), 150) function type(text, targetElem, { speed = 1 } = {}) { targetElem.textContent = '' ; let i = 0; (function typeNextChar() { if (i < text.length) { targetElem.textContent += text[i] ; i++ ; setTimeout(typeNextChar, 50 / speed) } })() } } // Render MD, highlight code const replyPre = app.div.querySelector('.reply-pre') try { // to render markdown replyPre.innerHTML = marked.parse(content) } catch (err) { log.error(err.message) } hljs.highlightAll() // highlight code replyPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';
') }) update.replyPrefix() // prepend '>> ' if dark scheme w/ bg animations to emulate terminal // Typeset math ;[replyPre, ...replyPre.querySelectorAll('*')].forEach(elem => renderMathInElement(elem, { delimiters: app.katexDelimiters, throwOnError: false })) if (config.stickySidebar) update.replyPreMaxHeight() saveAppDiv() // to fight Brave mutations // Auto-scroll if active if (config.autoScroll && !env.browser.isMobile && config.proxyAPIenabled && !config.streamingDisabled) { if (config.stickySidebar || config.anchored) replyPre.scrollTop = replyPre.scrollHeight scrollBy({ top: app.div.querySelector('footer').getBoundingClientRect().bottom - innerHeight + 13 }) } } // Focus chatbar conditionally if (!show.reply.chatbarFocused // do only once && !env.browser.isMobile // exclude mobile devices to not auto-popup OSD keyboard && ((!config.autoFocusChatbarDisabled && ( config.anchored // include Anchored mode if AF enabled // ...or un-Anchored if fully above fold || ( app.div.offsetHeight < innerHeight - app.div.getBoundingClientRect().top ))) // ...or Anchored if AF disabled & user interacted || (config.autoFocusChatbarDisabled && config.anchored && show.reply.userInteracted)) ) { app.div.querySelector(`#${app.slug}-chatbar`).focus() ; show.reply.chatbarFocused = true } // Restore minimized/restored state if anchored if (config.anchored) update.appBottomPos() show.reply.userInteracted = false } } // Define COMPONENTS window.fontSizeSlider = { // requires dom.js + fadeInDelay: 5, // ms hWheelDistance: 10, // px createAppend() { // requires dom.js + // Create/ID/classify slider elems fontSizeSlider.cursorOverlay = dom.create.elem('div', { class: 'cursor-overlay' }) const slider = dom.create.elem('div', { id: `${app.slug}-font-size-slider-track`, class: 'fade-in-less', style: 'display: none' }) const sliderThumb = dom.create.elem('div', { title: Math.floor(config.fontSize *10) /10 + 'px', id: `${app.slug}-font-size-slider-thumb` }) const sliderTip = dom.create.elem('div', { id: `${app.slug}-font-size-slider-tip` }) // Assemble/insert elems slider.append(sliderThumb, sliderTip) app.div.insertBefore(slider, app.div.querySelector(`.${app.slug}-tooltip,` // desktop + '.reply-bubble')) // mobile // Init thumb pos setTimeout(() => { const iniLeft = (config.fontSize - config.minFontSize) / (config.maxFontSize - config.minFontSize) * (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 => { if (event.button != 0) return // prevent non-left-click drag 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?.isConnected) fontSizeSlider.cursorOverlay.remove() }) // Add event listener for wheel-scrolling thumb if (!env.browser.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 => { if (event.button != 0) return // prevent non-left-click drag event.preventDefault() // prevent text selection 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 replyPre = app.div.querySelector('.reply-pre'), fontSizePercent = newLeft / sliderWidth, fontSize = config.minFontSize + fontSizePercent * (config.maxFontSize - config.minFontSize) replyPre.style.fontSize = fontSize + 'px' replyPre.style.lineHeight = fontSize * config.lineHeightRatio + 'px' settings.save('fontSize', fontSize) sliderThumb.title = Math.floor(config.fontSize *10) /10 + 'px' } return slider }, toggle(state = '') { // requires app const slider = document.getElementById(`${app.slug}-font-size-slider-track`) || fontSizeSlider.createAppend() const replyTip = app.div.querySelector('.reply-tip') const sliderTip = document.getElementById(`${app.slug}-font-size-slider-tip`) // Show slider if (state == 'on' || (!state && slider.style.display == 'none')) { // Position slider tip const btnSpan = document.getElementById(`${app.slug}-font-size-btn`), rects = { appDiv: app.div.getBoundingClientRect(), btnSpan: btnSpan.getBoundingClientRect() } sliderTip.style.right = `${ rects.appDiv.right - ( rects.btnSpan.left + rects.btnSpan.right )/2 -32 }px` // Show slider, hide reply tip slider.style.display = sliderTip.style.display = '' ; if (replyTip) replyTip.style.display = 'none' setTimeout(() => slider.classList.add('active'), fontSizeSlider.fadeInDelay) // Hide slider } else if (state == 'off' || (!state && slider.style.display != 'none')) { slider.classList.remove('active') ; if (replyTip) replyTip.style.display = '' sliderTip.style.display = slider.style.display = 'none' } } } const logos = { // requires dom.js + + GM_getResourceText() bravegpt: { create() { // requires dom.js + app const bravegptlogo = dom.create.elem('img', { id: `${app.slug}-logo`, class: 'no-mobile-tap-outline' }) logos.bravegpt.update(bravegptlogo) return bravegptlogo }, update(...targetLogos) { // requires + GM_getResourceText() targetLogos = targetLogos.flat() // flatten array args nested by spread operator if (!targetLogos.length) targetLogos = document.querySelectorAll(`#${app.slug}-logo`) targetLogos.forEach(logo => logo.src = GM_getResourceText(`bgpt${ env.ui.app.scheme == 'dark' ? 'DS' : 'LS' }logo`)) } } } window.modals = { stack: [], // of types of undismissed modals class: `${app.slug}-modal`, about() { // Show modal const labelStyles = 'text-transform: uppercase ; font-size: 16px ; font-weight: bold ;' + `color: ${ env.ui.app.scheme == 'dark' ? 'white' : '#494141' }` const aboutModal = modals.alert( `${app.symbol} ${app.msgs.appName}`, // title `🧠 ${app.msgs.about_author}: ` + `${app.author[0].name} ${app.msgs.about_and}` + ` ${app.msgs.about_contributors}\n` + `🏷️ ${app.msgs.about_version}: ` + `${app.version}\n` + `📜 ${app.msgs.about_openSourceCode}: ` + `` + app.urls.github + '\n' + `🚀 ${app.msgs.about_latestChanges}: ` + `` + `${app.urls.github}/commits\n` + `⚡ ${app.msgs.about_poweredBy}: ` + `chatgpt.js` + ` v${app.chatgptjsVer}`, [ // buttons function checkForUpdates() { userscript.updateCheck() }, function getSupport(){}, function rateUs() { modals.open('feedback') }, function moreAIextensions(){} ], '', 655 // modal width ) // Add logo const aboutHeaderLogo = logos.bravegpt.create() ; aboutHeaderLogo.width = 375 aboutHeaderLogo.style.cssText = `max-width: 98% ; margin: 1px ${ env.browser.isMobile ? 'auto' : '16%' } 0` aboutModal.firstChild.nextSibling.before(aboutHeaderLogo) // after close btn // Center text aboutModal.querySelector('h2').remove() // remove empty title h2 aboutModal.querySelector('p').style.cssText = ( 'overflow-wrap: anywhere ; line-height: 1.55 ;' + `margin: ${ env.browser.isPhone ? '15px 0 -21px' : '13px 0 -18px 11px' }` ) // Hack buttons aboutModal.querySelectorAll('button').forEach(btn => { btn.style.cssText = 'height: 53px ; min-width: 136px' // Replace link buttons w/ clones that don't dismiss modal if (/support|extensions/i.test(btn.textContent)) { btn.replaceWith(btn = btn.cloneNode(true)) btn.onclick = () => modals.safeWinOpen(app.urls[ btn.textContent.includes(app.msgs.btnLabel_getSupport) ? 'support' : 'relatedExtensions' ]) } // Prepend emoji + localize labels if (/updates/i.test(btn.textContent)) btn.textContent = `🚀 ${app.msgs.btnLabel_checkForUpdates}` else if (/support/i.test(btn.textContent)) btn.textContent = `🧠 ${app.msgs.btnLabel_getSupport}` else if (/rate/i.test(btn.textContent)) btn.textContent = `⭐ ${app.msgs.btnLabel_rateUs}` else if (/extensions/i.test(btn.textContent)) btn.textContent = `🤖 ${app.msgs.btnLabel_moreAIextensions}` // Hide Dismiss button else btn.style.display = 'none' }) return aboutModal }, alert(title = '', msg = '', btns = '', checkbox = '', width = '') { // generic one from chatgpt.alert() const alertID = chatgpt.alert(title, msg, btns, checkbox, width), alert = document.getElementById(alertID).firstChild this.init(alert) // add classes/listeners/hack bg return alert }, api() { // requires lib/feedback.js + // Show modal const modalBtns = [app.msgs.menuLabel_random, ...Object.keys(apis).filter(api => api != 'OpenAI')] .map(api => { // to btn callback/label function onclick() { settings.save('preferredAPI', api == app.msgs.menuLabel_random ? false : api) if (modals.settings.get()) { // update status of Preferred API entry const preferredAPIstatus = document.querySelector('[id*=preferredAPI] > span') if (preferredAPIstatus.textContent != api) preferredAPIstatus.textContent = api } feedback.notify(`${app.msgs.menuLabel_preferred} API ${app.msgs.menuLabel_saved.toLowerCase()}`, `${ config.anchored ? 'top' : 'bottom' }-right`) if (app.div.querySelector(`.${app.slug}-alert`) && config.proxyAPIenabled) get.reply({ msgs: app.msgChain, src: get.reply.src }) // re-send query if user alerted } Object.defineProperty(onclick, 'name', { value: api.toLowerCase() }) return onclick }) const apiModal = modals.alert(`${app.msgs.menuLabel_preferred} API:`, '', modalBtns, '', 503) // Re-style elems apiModal.querySelector('h2').style.justifySelf = 'center' // center title const btnsDiv = apiModal.querySelector('.modal-buttons') btnsDiv.style.cssText = ` /* y-pad, gridify */ margin: 18px 0px 14px !important ; display: grid ; grid-template-columns: repeat(3, 1fr) ; gap: 10px` btnsDiv.querySelectorAll('button').forEach((btn, idx) => { if (idx == 0) btn.style.display = 'none' // hide Dismiss button else btn.classList.toggle('primary-modal-btn', // emphasize preferred API config.preferredAPI.toLowerCase() == btn.textContent.toLowerCase() || btn.textContent == app.msgs.menuLabel_random && !config.preferredAPI) }) return apiModal }, feedback() { // Init buttons let btns = [ function productHunt(){}, function g2(){}, function alternativeto(){} ] if (modals.stack[0] != 'about') btns.push(function github(){}) // Show modal const feedbackModal = modals.alert(`${app.msgs.alert_choosePlatform}:`, '', btns, '', 456) // Center CTA feedbackModal.querySelector('h2').style.justifySelf = 'center' // Re-style button cluster const btnsDiv = feedbackModal.querySelector('.modal-buttons') btnsDiv.style.cssText += 'display: flex ; flex-wrap: wrap ; justify-content: center ;' + 'margin: 18px 0 6px !important' // close gap between title/btns // Hack buttons btns = btnsDiv.querySelectorAll('button') btns.forEach((btn, idx) => { if (idx == 0) btn.style.display = 'none' // hide Dismiss button if (idx == btns.length -1) btn.classList.remove('primary-modal-btn') // de-emphasize last link btn.style.marginTop = btn.style.marginBottom = '5px' // v-pad btns // Replace buttons w/ clones that don't dismiss modal btn.replaceWith(btn = btn.cloneNode(true)) btn.onclick = () => modals.safeWinOpen( btn.textContent == 'Product Hunt' ? app.urls.review.productHunt : btn.textContent == 'G2' ? app.urls.review.g2 : btn.textContent == 'Alternativeto' ? app.urls.review.alternativeTo : app.urls.discuss ) }) return feedbackModal }, handlers: { dismiss: { // to dismiss native modals click(event) { const clickedElem = event.target if (clickedElem == event.currentTarget || clickedElem.closest('[class*=-close-btn]')) modals.hide((clickedElem.closest('[class*=-modal-bg]') || clickedElem).firstChild) }, key(event) { if (event.key.startsWith('Esc') || event.keyCode == 27) modals.hide(document.querySelector('[class$=-modal]')) } }, drag: { mousedown(event) { // find modal, update styles, attach listeners, init XY offsets if (event.button != 0) return // prevent non-left-click drag if (!/auto|default/.test(getComputedStyle(event.target).cursor)) return // prevent drag on interactive elems modals.draggingModal = event.currentTarget event.preventDefault() // prevent sub-elems like icons being draggable Object.assign(modals.draggingModal.style, { // update styles transform: 'scale(1.05)', willChange: 'transform', transition: '0.1s', '-webkit-transition': '0.1s', '-moz-transition': '0.1s', '-o-transition': '0.1s', '-ms-transition': '0.1s' }) document.body.style.cursor = 'grabbing' // update cursor ;[...modals.draggingModal.children] // prevent hover FX if drag lags behind cursor .forEach(child => child.style.pointerEvents = 'none') ;['mousemove', 'mouseup'].forEach(eventType => // add listeners document.addEventListener(eventType, modals.handlers.drag[eventType])) const draggingModalRect = modals.draggingModal.getBoundingClientRect() modals.handlers.drag.offsetX = event.clientX - draggingModalRect.left +21 modals.handlers.drag.offsetY = event.clientY - draggingModalRect.top +12 }, mousemove(event) { // drag modal if (modals.draggingModal) { const newX = event.clientX - modals.handlers.drag.offsetX, newY = event.clientY - modals.handlers.drag.offsetY Object.assign(modals.draggingModal.style, { left: `${newX}px`, top: `${newY}px` }) } }, mouseup() { // restore styles/pointer events, remove listeners, reset modals.draggingModal Object.assign(modals.draggingModal.style, { // restore styles cursor: 'inherit', transform: 'scale(1)', willChange: 'auto', transition: 'inherit', '-webkit-transition': 'inherit', '-moz-transition': 'inherit', '-o-transition': 'inherit', '-ms-transition': 'inherit' }) document.body.style.cursor = '' // restore cursor ;[...modals.draggingModal.children] // restore pointer events .forEach(child => child.style.pointerEvents = '') ;['mousemove', 'mouseup'].forEach(eventType => // remove listeners document.removeEventListener(eventType, modals.handlers.drag[eventType])) modals.draggingModal = null } } }, hide(modal) { const modalContainer = modal?.parentNode ; if (!modalContainer) return modalContainer.style.animation = 'modal-zoom-fade-out 0.165s ease-out' modalContainer.onanimationend = () => modalContainer.remove() }, init(modal) { // requires lib/dom.js if (!this.styles) this.stylize() // to init/append stylesheet // Add classes modal.classList.add('no-user-select', this.class) ; modal.parentNode.classList.add(`${this.class}-bg`) // Add listeners modal.onwheel = modal.ontouchmove = event => event.preventDefault() // disable wheel/swipe scrolling modal.onmousedown = this.handlers.drag.mousedown // enable click-dragging if (!modal.parentNode.className.includes('chatgpt-modal')) { // enable click-dismissing native modals const dismissElems = [modal.parentNode, modal.querySelector('[class*=-close-btn]')] dismissElems.forEach(elem => elem.onclick = this.handlers.dismiss.click) } // Hack BG dom.addRisingParticles(modal) setTimeout(() => { // dim bg modal.parentNode.style.backgroundColor = `rgba(67,70,72,${ env.ui.app.scheme == 'dark' ? 0.62 : 0.33 })` modal.parentNode.classList.add('animated') }, 100) // delay for transition fx }, observeRemoval(modal, modalType, modalSubType) { // to maintain stack for proper nav const modalBG = modal.parentNode new MutationObserver(([mutation], obs) => { mutation.removedNodes.forEach(removedNode => { if (removedNode == modalBG) { if (modals.stack[0].includes(modalSubType || modalType)) { // new modal not launched so nav back modals.stack.shift() // remove this modal type from stack 1st const prevModalType = modals.stack[0] if (prevModalType) { // open it modals.stack.shift() // remove type from stack since re-added on open modals.open(prevModalType) } } obs.disconnect() }}) }).observe(modalBG.parentNode, { childList: true, subtree: true }) }, open(modalType, modalSubType) { // custom ones const modal = modalSubType ? modals[modalType][modalSubType]() : (modals[modalType].show || modals[modalType])() if (!modal) return // since no div returned if (settings.controls[modalType]?.type != 'prompt') { // add to stack this.stack.unshift(modalSubType ? `${modalType}_${modalSubType}` : modalType) log.debug(`Modal stack: ${JSON.stringify(modals.stack)}`) } this.init(modal) // add classes/listeners/hack bg this.observeRemoval(modal, modalType, modalSubType) // to maintain stack for proper nav if (!modals.handlers.dismiss.key.added) { // add key listener to dismiss modals document.addEventListener('keydown', modals.handlers.dismiss.key) modals.handlers.dismiss.key.added = true } }, replyLang() { // requires while (true) { let replyLang = prompt(`${app.msgs.prompt_updateReplyLang}:`, config.replyLang) if (replyLang == null) break // user cancelled so do nothing else if (!/\d/.test(replyLang)) { replyLang = ( // auto-case for menu/alert aesthetics replyLang.length < 4 || replyLang.includes('-') ? replyLang.toUpperCase() : log.toTitleCase(replyLang) ) settings.save('replyLang', replyLang || env.browser.language) modals.alert(`${app.msgs.alert_langUpdated}!`, // title `${app.name} ${ // msg app.msgs.alert_willReplyIn} ${ replyLang || app.msgs.alert_yourSysLang }.`, '', '', 447) // modal width if (modals.settings.get()) // update settings menu status label document.querySelector('#replyLang-settings-entry span').textContent = replyLang break } } }, safeWinOpen(url) { open(url, '_blank', 'noopener') }, // to prevent backdoor vulnerabilities scheme() { // Show modal const schemeModal = modals.alert(`${ app.name } ${( app.msgs.menuLabel_colorScheme ).toLowerCase() }:`, '', // title [ function auto(){}, function light(){}, function dark(){} ], // buttons '', 503 // px width ) // Center title/button cluster schemeModal.querySelector('h2').style.justifySelf = 'center' schemeModal.querySelector('.modal-buttons') .style.cssText = 'justify-content: center ; margin: 18px 0 14px !important' // Hack buttons const schemeEmojis = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'} schemeModal.querySelectorAll('button').forEach(btn => { const btnScheme = btn.textContent.toLowerCase() // Emphasize active scheme btn.classList.toggle('primary-modal-btn', config.scheme == btn.textContent.toLowerCase() || (btn.textContent == 'Auto' && !config.scheme)) // Prepend emoji + localize labels if (Object.prototype.hasOwnProperty.call(schemeEmojis, btnScheme)) btn.textContent = `${schemeEmojis[btnScheme]} ${ // emoji app.msgs['scheme_' + btnScheme] || app.msgs['menuLabel_' + btnScheme] || btnScheme.toUpperCase() }` else btn.style.display = 'none' // hide Dismiss button // Clone button to replace listener to not dismiss modal on click btn.replaceWith(btn = btn.cloneNode(true)) btn.onclick = () => { const newScheme = btnScheme == 'auto' ? ui.getScheme() : btnScheme settings.save('scheme', btnScheme == 'auto' ? false : newScheme) schemeModal.querySelectorAll('button').forEach(btn => btn.classList.remove('primary-modal-btn')) // clear prev emphasized active scheme btn.classList.add('primary-modal-btn') // emphasize newly active scheme btn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis setTimeout(() => { btn.style.pointerEvents = 'auto' }, // re-enable hover fx 100) // ...after 100ms to flicker emphasis update.scheme(newScheme) ; schemeNotify(btnScheme) } }) function schemeNotify(scheme) { // Show notification feedback.notify(`${app.msgs.menuLabel_colorScheme}: ` + ( scheme == 'light' ? app.msgs.scheme_light || 'Light' : scheme == 'dark' ? app.msgs.scheme_dark || 'Dark' : app.msgs.menuLabel_auto ).toUpperCase() ) // Append scheme icon const notifs = document.querySelectorAll('.chatgpt-notif'), notif = notifs[notifs.length -1] notif.append(icons.create({ key: scheme == 'light' ? 'sun' : scheme == 'dark' ? 'moon' : 'arrowsCyclic', style: 'width: 23px ; height: 23px ; position: relative ; top: 3px ; margin-left: 6px' })) } return schemeModal }, settings: { createAppend() { // Init master elems const settingsContainer = dom.create.elem('div'), settingsModal = dom.create.elem('div', { id: `${app.slug}-settings` }) settingsContainer.append(settingsModal) // Init settings keys const settingsKeys = Object.keys(settings.controls).filter(key => !(env.browser.isMobile && settings.controls[key].mobile == false)) // Init logo const settingsIcon = icons.app.bravegpt.create() settingsIcon.style.cssText = `width: ${ env.browser.isPortrait ? 63 : 67 }px ; margin-bottom: 10px ;` + `position: relative ; top: -29px ; right: ${ env.browser.isPortrait ? -5 : 7 }px ;` + 'filter: drop-shadow(5px 5px 15px rgba(0,0,0,0.3))' // Init title const settingsTitleDiv = dom.create.elem('div', { id: `${app.slug}-settings-title` }), settingsTitleIcon = icons.create({ key: 'sliders' }), settingsTitleH4 = dom.create.elem('h4') settingsTitleIcon.style.cssText += 'width: 21px ; height: 21px ;' + 'position: relative ; right: 8px ; top: 2.5px' settingsTitleH4.textContent = app.msgs.menuLabel_settings settingsTitleH4.prepend(settingsTitleIcon) ; settingsTitleDiv.append(settingsTitleH4) // Init settings lists const settingsLists = [], middleGap = 30 // px const settingsListContainer = dom.create.elem('div') const settingsListCnt = ( env.browser.isMobile && ( env.browser.isPortrait || settingsKeys.length < 8 )) ? 1 : 2 const settingEntryCap = Math.floor(settingsKeys.length /2) for (let i = 0 ; i < settingsListCnt ; i++) settingsLists.push(dom.create.elem('ul')) settingsListContainer.style.width = '95%' // pad vs. parent if (settingsListCnt > 1) { // style multi-list landscape mode settingsListContainer.style.cssText += ( // make/pad flexbox, add middle gap `display: flex ; padding: 11px 0 13px ; gap: ${ middleGap /2 }px` ) settingsLists[0].style.cssText = ( // add vertical separator `padding-right: ${ middleGap /2 }px` ) } // Create/append setting icons/labels/toggles settingsKeys.forEach((key, idx) => { const setting = settings.controls[key] // Create/append item/label elems const settingEntry = dom.create.elem('li', { id: `${key}-settings-entry`, title: setting.helptip || '' }) const settingLabel = dom.create.elem('label') ; settingLabel.textContent = setting.label settingEntry.append(settingLabel); (settingsLists[env.browser.isPortrait ? 0 : +(idx >= settingEntryCap)]).append(settingEntry) // Create/prepend icons const settingIcon = icons.create({ key: setting.icon }) settingIcon.style.cssText = 'position: relative ;' + ( /proxy/i.test(key) ? 'top: 3px ; left: -0.5px ; margin-right: 9px' : /preferred/i.test(key) ? 'top: 3.5px ; margin-right: 7.5px' : /streaming/i.test(key) ? 'top: 3px ; left: 0.5px ; margin-right: 9px' : /auto(?:get|focus)/i.test(key) ? 'top: 4.5px ; margin-right: 7px' : /summarize/i.test(key) ? 'top: 3.5px ; left: -5px ; margin-right: 3px ; height: 17.5px' : /autoscroll/i.test(key) ? 'top: 3.5px ; left: -1.5px ; margin-right: 6px' : /^rq/.test(key) ? 'top: 2.5px ; left: 0.5px ; margin-right: 9px ; transform: scaleY(-1)' : /prefix/i.test(key) ? 'top: 2.5px ; left: 0.5px ; margin-right: 9px' : /suffix/i.test(key) ? 'top: 4px ; left: -1.5px ; margin-right: 7px' : /sidebar/i.test(key) ? 'top: 4px ; left: -1.5px ; margin-right: 7.5px' : /anchor/i.test(key) ? 'top: 3px ; left: -2.5px ; margin-right: 5.5px' : /animation/i.test(key) ? 'top: 3px ; left: -1.5px ; margin-right: 6.5px' : /replylang/i.test(key) ? 'top: 3px ; left: -1.5px ; margin-right: 9px' : /scheme/i.test(key) ? 'top: 2.5px ; left: -1.5px ; margin-right: 8px' : /debug/i.test(key) ? 'top: 3.5px ; left: -1.5px ; margin-right: 8px' : /about/i.test(key) ? 'top: 3px ; left: -3px ; margin-right: 5.5px' : '' ) settingEntry.prepend(settingIcon) if (key.includes('Animation')) // customize sparkle icon elem fill settingIcon[`${ key.startsWith('fg') ? 'last' : 'first' }Child`].style.fill = 'none' // Create/append toggles/listeners if (setting.type == 'toggle') { // Init toggle input const settingToggle = dom.create.elem('input', { type: 'checkbox', disabled: true, style: 'display: none' }) settingToggle.checked = config[key] ^ key.includes('Disabled') // init based on config/name && !(key == 'streamingDisabled' && !config.proxyAPIenabled) // uncheck Streaming in OAI mode // Create/classify switch const switchSpan = dom.create.elem('span', { class: 'track' }), knobSpan = dom.create.elem('span', { class: 'knob' }) // Append elems switchSpan.append(knobSpan) ; settingEntry.append(settingToggle, switchSpan) // Update visual state w/ animation setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), 155) // Add click listener settingEntry.onclick = () => { if (!(key == 'streamingDisabled' // visually switch toggle if not Streaminng... && ( // ...in unsupported env... !env.scriptManager.supportsStreaming || !config.proxyAPIenabled ) )) modals.settings.toggle.switch(settingToggle) // Call specialized toggle funcs const autoGenMatch = /get|summarize/i.exec(key), manualGenMatch = /(?:suf|pre)fix/i.exec(key) if (key.includes('proxy')) toggle.proxyMode() else if (key.includes('streaming')) toggle.streaming() else if (key.includes('rq')) toggle.relatedQueries() else if (autoGenMatch) toggle.autoGen(autoGenMatch[0].toLowerCase()) else if (manualGenMatch) toggle.manualGen(manualGenMatch[0].toLowerCase()) else if (key.includes('Sidebar')) toggle.sidebar(key.replace('Sidebar', '')) else if (key.includes('anchor')) toggle.anchorMode() else if (key.includes('bgAnimation')) toggle.animations('bg') else if (key.includes('fgAnimation')) toggle.animations('fg') // ...or generically toggle/notify else { settings.save(key, !config[key]) // update config feedback.notify(`${settings.controls[key].label} ${ menus.toolbar.state.words[+(key.includes('Disabled') ^ config[key])]}`) } } // Add .active + config status + listeners to pop-up settings } else { settingEntry.classList.add('active') const configStatusSpan = dom.create.elem('span', { style: `float: right ; font-size: 11px ; margin-top: 3px ;${ key != 'about' ? 'text-transform: uppercase !important' : '' }` }) ;({ about: () => { const innerDiv = dom.create.elem('div'), xGap = '     ' modals.settings.aboutContent = { short: `v${GM_info.script.version}`, long: `${app.msgs.about_version}: v${ GM_info.script.version + xGap }${ app.msgs.about_poweredBy} chatgpt.js${xGap}` } for (let i = 0; i < 7; i++) modals.settings.aboutContent.long += modals.settings.aboutContent.long // make long af innerDiv.innerHTML = modals.settings.aboutContent[ config.fgAnimationsDisabled ? 'short' : 'long'] innerDiv.style.float = config.fgAnimationsDisabled ? 'right' : '' configStatusSpan.append(innerDiv) ; settingEntry.onclick = () => modals.open('about') }, preferredAPI: () => { configStatusSpan.textContent = config.preferredAPI || app.msgs.menuLabel_random settingEntry.onclick = () => modals.open('api') settingEntry.classList.toggle('active', config.proxyAPIenabled) settingEntry.style.pointerEvents = config.proxyAPIenabled ? '' : 'none' }, replyLang: () => { configStatusSpan.textContent = config.replyLang settingEntry.onclick = () => modals.open('replyLang') }, scheme: () => { modals.settings.updateSchemeStatus(configStatusSpan) settingEntry.onclick = () => modals.open('scheme') } })[key]?.() settingEntry.append(configStatusSpan) } }) settingsListContainer.append(...settingsLists) // Create close button const closeBtn = dom.create.elem('div', { title: app.msgs.tooltip_close, class: `${app.slug}-modal-close-btn no-mobile-tap-outline` }) closeBtn.append(icons.create({ key: 'x' })) // Assemble/append elems settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsListContainer) document.body.append(settingsContainer) return settingsContainer }, get() { return document.getElementById(`${app.slug}-settings`) }, show() { const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend() settingsContainer.style.display = '' // show modal if (env.browser.isMobile) { // scale 93% to viewport sides const settingsModal = settingsContainer.querySelector(`#${app.slug}-settings`), scaleRatio = 0.93 * innerWidth / settingsModal.offsetWidth settingsModal.style.transform = `scale(${scaleRatio})` } return settingsContainer.firstChild }, toggle: { switch(settingToggle) { settingToggle.checked = !settingToggle.checked modals.settings.toggle.updateStyles(settingToggle) }, updateStyles(settingToggle) { // for .toggle.show() + staggered switch animations in .createAppend() const settingLi = settingToggle.parentNode, switchSpan = settingLi.querySelector('span'), knobSpan = switchSpan.querySelector('span') requestAnimationFrame(() => { 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)' settingLi.classList.toggle('active', settingToggle.checked) // dim/brighten entry }) // to trigger 1st transition fx } }, updateSchemeStatus(schemeStatusSpan = null) { schemeStatusSpan ||= document.querySelector('#scheme-settings-entry span') if (schemeStatusSpan) { schemeStatusSpan.textContent = '' schemeStatusSpan.append( // status txt + icon document.createTextNode(app.msgs[/dark|light/.test(config.scheme) ? `scheme_${config.scheme}` : 'menuLabel_auto']), icons.create({ size: 12, key: config.scheme == 'dark' ? 'moon' : config.scheme == 'light' ? 'sun' : 'arrowsCyclic' }) ) } } }, shareChat(shareURL) { // Show modal const shareChatModal = modals.alert( `${log.toTitleCase(app.msgs.btnLabel_convo)} ${app.msgs.tooltip_page} ${ // title app.msgs.alert_generated.toLowerCase()}!`, `${shareURL}`, // link msg [ // buttons function copyUrl() { navigator.clipboard.writeText(shareURL) .then(() => feedback.notify(app.msgs.notif_copiedToClipboard)) }, function visitPage() { modals.safeWinOpen(shareURL) }, function downloadChat() { xhr({ method: 'GET', url: shareURL, onload: resp => { const html = resp.responseText, dlLink = dom.create.anchor( URL.createObjectURL(new Blob([html], { type: 'text/html' }))) dlLink.download /* filename */ = html.match(/([^<]+)<\/title>/i)[1] // page title .replace(/\s*[—|/]+\s*/g, ' ') // convert symbols to space for hyphen-casing .replace(/\.{2,}/g, '') // strip ellipsis .toLowerCase().trim().replace(/\s+/g, '-') // hyphen-case + '.html' document.body.append(dlLink) ; dlLink.click() ; dlLink.remove() // download HTML URL.revokeObjectURL(dlLink.href) // prevent memory leaks }, onerror: err => log.error('Failed to download chat:', err) }) } ] ) // Prefix icon to title const modalTitle = shareChatModal.querySelector('h2'), titleIcon = icons.create({ key: 'speechBalloons' }) titleIcon.style.cssText = 'height: 28px ; width: 28px ; position: relative ; top: 7px ; right: 8px ;' + `fill: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }` modalTitle.prepend(titleIcon) // Hide Dismiss button, localize other labels const modalBtns = shareChatModal.querySelectorAll('button') modalBtns[0].style.display = 'none' // hide Dismiss button if (!env.browser.language.startsWith('en')) // localize button labels modalBtns.forEach(btn => { if (/copy/i.test(btn.textContent)) btn.textContent = `${app.msgs.tooltip_copy} URL` else if (/visit/i.test(btn.textContent)) btn.textContent = app.msgs.btnLabel_visitPage else if (/download/i.test(btn.textContent)) btn.textContent = `${app.msgs.btnLabel_download} ${log.toTitleCase(app.msgs.btnLabel_convo)}` }) // Style elements shareChatModal.style.wordBreak = 'break-all' // since URL really long shareChatModal.querySelector('h2').style.justifySelf = 'center' shareChatModal.querySelector('p').style.cssText = 'text-align: center ; margin: 10px 0 -24px' shareChatModal.querySelector('.modal-buttons').style.cssText = 'justify-content: center' return shareChatModal }, stylize() { if (!this.styles) document.head.append(this.styles = dom.create.elem('style')) this.styles.textContent = ( // Vars `:root { --modal-btn-zoom: scale(1.055) ; --modal-btn-transition: transform 0.15s ease ; --settings-li-transition: transform 0.1s ease ; /* for Settings entry hover-zoom */ --fg-transition: opacity 0.65s cubic-bezier(0.165,0.84,0.44,1), /* fade-in */ transform 0.55s cubic-bezier(0.165,0.84,0.44,1) !important ; /* move-in */ --bg-transition: background-color 0.25s ease !important } /* dim */` // Main modal styles + '@keyframes modal-zoom-fade-out {' + '0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }' + '100% { opacity: 0 ; transform: scale(1.35) }}' + '.chatgpt-modal > div { background-color: white !important ; color: #202124 }' + '.chatgpt-modal p { margin: 14px 0 -20px 4px ; font-size: 18px }' // pos/size modal msg + `.chatgpt-modal a { color: #${ env.ui.app.scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }` + '.modal-buttons {' + `margin: 38px 0 1px ${ env.browser.isMobile ? 0 : -7 }px !important ; width: 100% }` + '.chatgpt-modal button {' // this.alert() buttons + `min-width: 123px ; padding: ${ env.browser.isMobile ? '5px' : '4px 8px' } !important ;` + 'cursor: pointer ; border-radius: 0 !important ; height: 39px ;' + 'border: 1px solid ' + ( env.ui.app.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 }' + ( env.ui.app.scheme == 'dark' ? // darkmode chatgpt.alert() styles ( '.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {' + 'color: white !important }' + '.primary-modal-btn { background: hsl(186 100% 69%) !important ; color: black !important }' + '.chatgpt-modal a { color: #00cfff !important }' + '.chatgpt-modal button:hover {' + 'background-color: #00cfff !important ; color: black !important }' ) : '' ) + `.${modals.class} { display: grid ; place-items: center }` // for centered icon/logo + '[class*=modal-close-btn] {' + 'position: absolute !important ; float: right ; top: 14px !important ; right: 16px !important ;' + 'cursor: pointer ; width: 33px ; height: 33px ; border-radius: 20px }' + `[class*=modal-close-btn] path {${ env.ui.app.scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}` + ( env.ui.app.scheme == 'dark' ? // invert dark mode hover paths '[class*=modal-close-btn]:hover path { stroke: black ; fill: black }' : '' ) + '[class*=modal-close-btn]:hover { background-color: #f2f2f2 }' // hover underlay + '[class*=modal-close-btn] svg { margin: 11.5px }' // center SVG for hover underlay + '[class*=-modal] h2 {' + 'font-size: 26px ; line-height: 32px ; padding: 0 ; margin: 4px 0 -1px !important ;' + `${ env.browser.isMobile ? 'text-align: center' // center on mobile : 'justify-self: start' }}` // left-align on desktop + '[class*=-modal] p { justify-self: start ; font-size: 20px }' + '[class*=-modal] button { font-size: 14px !important ; background: none }' + '[class*=-modal-bg] {' + 'pointer-events: auto ;' // override any disabling from site modals + 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 ;' // align + `transition: var(--bg-transition) ; /* dim */ -webkit-transition: var(--bg-transition) ; -moz-transition: var(--bg-transition) ; -o-transition: var(--bg-transition) ; -ms-transition: var(--bg-transition) }` + '[class*=-modal-bg].animated > div {' + 'z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }' + '[class$=-modal] {' // native modals + chatgpt.alert()s + 'position: absolute ;' // to be click-draggable + 'opacity: 0 ;' // to fade-in + `background-image: linear-gradient(180deg, ${ env.ui.app.scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) ;` + `border: 1px solid ${ env.ui.app.scheme == 'dark' ? 'white' : '#b5b5b5' } !important ;` + `color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' };` + 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from + `transition: var(--fg-transition) ; /* fade-in + move-in */ -webkit-transition: var(--fg-transition) ; -moz-transition: var(--fg-transition) ; -o-transition: var(--fg-transition) ; -ms-transition: var(--fg-transition) } ${ env.browser.isMobile ? '' : `[class$=-modal] button:hover { transform: var(--modal-btn-zoom) }`} ${ config.fgAnimationsDisabled ? '' : `[class$=-modal] button { ${ env.browser.isMobile ? '' : 'will-change: transform ;' } transition: var(--modal-btn-transition) ; -webkit-transition: var(--modal-btn-transition) ; -moz-transition: var(--modal-btn-transition) ; -o-transition: var(--modal-btn-transition) ; -ms-transition: var(--modal-btn-transition) }`}` // Settings modal + `#${app.slug}-settings { font-family: var(--brand-font) ; min-width: ${ env.browser.isPortrait ? 288 : 758 }px ; max-width: 75vw ; margin: 12px 23px ; word-wrap: break-word ; border-radius: 15px ; ${ env.ui.app.scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }; --shadow: 0 30px 60px rgba(0,0,0,0.12) ; box-shadow: var(--shadow) ; -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow) }` + `#${app.slug}-settings-title {` + 'font-weight: bold ; line-height: 19px ; text-align: center ;' + `margin: 0 ${ env.browser.isMobile ? -31 : -6 }px -3px 0 }` + `#${app.slug}-settings-title h4 {` + `font-size: ${ env.browser.isPortrait ? 26 : 30 }px ; font-weight: bold ;` + 'margin: -31px 17px 7px 0 }' + `#${app.slug}-settings ul {` + 'list-style: none ; padding: 0 ; margin: 0 ;' // hide bullets, override Brave ul margins + `width: ${ env.browser.isPortrait ? 100 : 50 }% }` // set width based on column cnt + ( env.browser.isPhone ? '' : ( `#${app.slug}-settings ul:first-of-type {` // color desktop middle separator + `border-right: 1px dotted ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }}` )) + `#${app.slug}-settings li {` + `color: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };` + `fill: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };` + `stroke: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };` + 'height: 37px ; padding: 7px 10px ; font-size: 14.5px ;' + `border-bottom: 1px dotted ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' };` // add separators + 'border-radius: 3px ;' // slightly round highlight strip + `${ config.fgAnimationsDisabled || env.browser.isMobile ? '' : `transition: var(--settings-li-transition) ; -webkit-transition: var(--settings-li-transition) ; -moz-transition: var(--settings-li-transition) ; -o-transition: var(--settings-li-transition) ; -ms-transition: var(--settings-li-transition)` }}` + `#${app.slug}-settings li.active {` + `color: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' };` // for text + `fill: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' };` // for icons + `stroke: ${ env.ui.app.scheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' }}` // for icons + `#${app.slug}-settings li label { padding-right: 20px }` // right-pad labels so toggles don't hug + `#${app.slug}-settings li:last-of-type { border-bottom: none }` // remove last bottom-border + `#${app.slug}-settings li, #${app.slug}-settings li label { cursor: pointer }` // add finger on hover + `#${app.slug}-settings li:hover {` + 'background: rgba(100,149,237,0.88) ; color: white ; fill: white ; stroke: white ;' + `${ env.browser.isMobile ? '' : 'transform: scale(1.15)' }}` + `#${app.slug}-settings li > input { float: right } /* pos toggles */ #${app.slug}-settings li > .track { position: relative ; left: -1px ; bottom: -5.5px ; float: right ; background-color: #ccc ; width: 26px ; height: 13px ; border-radius: 28px ; ${ config.fgAnimationsDisabled ? '' : `transition: 0.4s ; -webkit-transition: 0.4s ; -moz-transition: 0.4s ; -o-transition: 0.4s ; -ms-transition: 0.4s` }} #${app.slug}-settings li .knob { position: absolute ; left: 1px ; bottom: 1px ; content: "" ; background-color: white ; width: 11px ; height: 11px ; border-radius: 28px ; ${ config.fgAnimationsDisabled ? '' : `transition: 0.2s ; -webkit-transition: 0.2s ; -moz-transition: 0.2s ; -o-transition: 0.2s ; -ms-transition: 0.2s` }}` + '#scheme-settings-entry > span { margin: 3px -2px 0 }' // align Scheme status + '#scheme-settings-entry > span > svg {' // v-align/left-pad Scheme status icon + 'position: relative ; top: 2px ; margin-left: 4px }' + ( config.fgAnimationsDisabled ? '' // spin cycle arrows icon when scheme is Auto : ( '#scheme-settings-entry svg[class*=arrowsCyclic],' + '.chatgpt-notif svg[class*=arrowsCyclic] { animation: rotate 5s linear infinite }' )) + `#about-settings-entry span { color: ${ env.ui.app.scheme == 'dark' ? '#28ee28' : 'green' }}` + '#about-settings-entry > span {' // outer About status span + `width: ${ env.browser.isPortrait ? '15vw' : '95px' }; height: 20px ; overflow: hidden ;` + `${ config.fgAnimationsDisabled ? '' : ( // fade edges 'mask-image: linear-gradient(' + 'to right, transparent, black 20%, black 89%, transparent) ;' + '-webkit-mask-image: linear-gradient(' + 'to right, transparent, black 20%, black 89%, transparent)' )}}` + '#about-settings-entry > span > div {' + `text-wrap: nowrap ; ${ config.fgAnimationsDisabled ? '' : 'animation: ticker linear 75s infinite' }}` + '@keyframes ticker { 0% { transform: translateX(100%) } 100% { transform: translateX(-2000%) }}' + `.about-em { color: ${ env.ui.app.scheme == 'dark' ? 'white' : 'green' } !important }` ) }, update: { width: 489, available() { // Show modal const updateAvailModal = modals.alert(`🚀 ${app.msgs.alert_updateAvail}!`, // title `${app.msgs.alert_newerVer} ${app.name} ` // msg + `(v${app.latestVer}) ${app.msgs.alert_isAvail}! ` + '<a target="_blank" rel="noopener" style="font-size: 0.93rem" href="' + `${app.urls.github}/commits/main/greasemonkey/${app.slug}.user.js` + `">${app.msgs.link_viewChanges}</a>`, function update() { // button modals.safeWinOpen(`${app.urls.update.gm}?t=${Date.now()}`) }, '', modals.update.width ) // Localize button labels if needed if (!env.browser.language.startsWith('en')) { const updateBtns = updateAvailModal.querySelectorAll('button') updateBtns[1].textContent = app.msgs.btnLabel_update updateBtns[0].textContent = app.msgs.btnLabel_dismiss } return updateAvailModal }, unavailable() { return modals.alert(`${app.msgs.alert_upToDate}!`, // title `${app.name} (v${app.version}) ${app.msgs.alert_isUpToDate}!`, // msg '', '', modals.update.width ) } } } // Run MAIN routine menus.toolbar.register() // Init UI props env.ui = { app: { scheme: config.scheme || ui.getScheme() }, site: { scheme: ui.getScheme() }} // Create/ID/classify/listenerize/stylize APP container app.div = dom.create.elem('div', { id: app.slug, class: 'fade-in snippet' }) themes.apply(config.theme) ; ui.addListeners.appDiv() ;['anchored', 'expanded', 'sticky', 'wider'].forEach(mode => (config[mode] || config[`${mode}Sidebar`]) && app.div.classList.add(mode)) update.appStyle() ;['rpg', 'rpw'].forEach(cssType => // rising particles document.head.append(dom.create.style(GM_getResourceText(`${cssType}CSS`)))) // APPEND to Brave app.parentDivSelector = env.browser.isMobile ? '#results' : '.sidebar' app.parentDiv = await new Promise(resolve => { const appDivParent = document.querySelector(app.parentDivSelector) if (appDivParent) resolve(appDivParent) else new MutationObserver((_, obs) => { const appDivParent = document.querySelector(app.parentDivSelector) if (appDivParent) { obs.disconnect() ; resolve(appDivParent) } }).observe(document.body, { childList: true, subtree: true }) }) setTimeout(() => { app.parentDiv.prepend(app.div) ; ui.visibilizeOverflow() setTimeout(() => app.div.classList.add('active'), 100) // fade in }, env.scriptManager.name == 'Violentmonkey' ? 1000 : 0) // delay in VM to avoid mutation bug https://github.com/KudoAI/bravegpt/issues/123 // Init footer CTA to share feedback const braveClassList = 'feedback svelte-8js1iq' app.footerContent = dom.create.anchor('#', app.msgs.link_shareFeedback, { target: '_self', class: braveClassList }) app.footerContent.onclick = () => modals.open('feedback') // REFERRALIZE links to support author setTimeout(() => document.querySelectorAll('a[href^="https://www.amazon."]').forEach(anchor => { const url = new URL(anchor.href) ; url.searchParams.set('tag', 'kudo-ai-20') anchor.href = url.toString() }), 1500) // AUTO-GEN reply or show STANDBY mode app.msgChain = [] ; const searchQuery = new URL(location.href).searchParams.get('q') if (!config.autoGetDisabled || config.autoSummarize // Auto-Gen on || (config.prefixEnabled || config.suffixEnabled) // or Manual-Gen on && [config.prefixEnabled && location.href.includes('q=%2F'), // prefix required/present config.suffixEnabled // suffix required/present && /q=.*?(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(location.href) ].filter(Boolean).length == (config.prefixEnabled + config.suffixEnabled) // validate both Manual-Gen modes ) { // auto-gen reply app.msgChain.push({ time: Date.now(), role: 'user', content: config.autoSummarize ? prompts.create('summarizeResults') : searchQuery }) get.reply({ msgs: app.msgChain, src: 'query' }) } else { // show Standby mode show.reply({ standby: true }) if (!config.rqDisabled) get.related(searchQuery) .then(queries => show.related(queries)) .catch(err => { log.error(err.message) ; api.tryNew(get.related) }) } saveAppDiv() // to fight Brave Svelte mutations // Monitor SCHEME PREF changes to update app scheme if auto-scheme mode new MutationObserver(handleSchemePrefChange).observe( // class changes from Brave Search theme settings document.documentElement, { attributes: true, attributeFilter: ['class'] }) window.matchMedia('(prefers-color-scheme: dark)').addEventListener( // for browser/system scheme pref changes 'change', () => requestAnimationFrame(handleSchemePrefChange)) function handleSchemePrefChange() { if (config.scheme) return // since light/dark hard-set const displayedScheme = ui.getScheme() if (env.ui.app.scheme != displayedScheme) update.scheme(displayedScheme) } // Observe DOM for need to re-insert app div new MutationObserver((_, obs) => { if (!document.getElementById(app.slug)) { restoreAppDiv() ; obs.disconnect() } }).observe(document.body, { subtree: true, childList: true }) function saveAppDiv() { if (restoreAppDiv.restored) return ; saveAppDiv.html = app.div.innerHTML } function restoreAppDiv() { log.caller = 'restoreAppDiv()' log.debug(`Restoring ${app.name} from mutation...`) app.div = dom.create.elem('div', { id: app.slug, class: 'fade-in active snippet' }) ; ui.addListeners.appDiv() ;['anchored', 'expanded', 'sticky', 'wider'].forEach(mode => (config[mode] || config[`${mode}Sidebar`]) && app.div.classList.add(mode)) app.div.innerHTML = saveAppDiv.html if (app.div.querySelector(`.${app.slug}-header-btn`)) ui.addListeners.btns.appHeader() app.div.querySelectorAll(`.${app.slug}-standby-btn`).forEach((btn, idx) => btn.onclick = show.reply[`${['query', 'summarize'][idx]}BtnClickHandler`]) show.codeCornerBtns() if (app.div.querySelector(`.${app.slug}-chatbar-btn`)) ui.addListeners.replySection() document.querySelector(app.parentDivSelector).prepend(app.div) ; ui.visibilizeOverflow() restoreAppDiv.restored = true } })()