// ==UserScript==
// @name DuckDuckGPT 🤖
// @description Adds AI answers to DuckDuckGo (powered by GPT-4o!)
// @description:af Voeg AI-antwoorde by DuckDuckGo (aangedryf deur GPT-4o!)
// @description:am የ DuckDuckGo ውስጥ AI መልቀቅን አድርግ፣ (GPT-4o በመሣሪያዎቹ ውስጥ!)
// @description:ar يضيف إجابات AI إلى DuckDuckGo (مدعوم بواسطة GPT-4o!)
// @description:as DuckDuckGo-লৈ AI উত্তৰ যোগ দিয়ে (GPT-4o দ্বাৰা পাওৱা হৈছে!)
// @description:az DuckDuckGo-ya AI cavablarını əlavə edir (GPT-4o tərəfindən dəstəklənir!)
// @description:be Дадае ІА адказы на DuckDuckGo (падтрымліваецца GPT-4o!)
// @description:bg Добавя ИИ отговори в DuckDuckGo (поддържан от GPT-4o!)
// @description:bn DuckDuckGo-ত AI উত্তর যোগ করে (GPT-4o দ্বারা প্রচালিত!)
// @description:bs Dodaje AI odgovore na DuckDuckGo (pokreće GPT-4o!)
// @description:ca Afegeix respostes d'IA a DuckDuckGo (impulsat per GPT-4o!)
// @description:ceb Nagdugang ug mga tubag AI ngadto sa DuckDuckGo (gipadagan sa GPT-4o!)
// @description:co Aggiunge risposte AI a DuckDuckGo (supportate da GPT-4o!)
// @description:cs Přidává AI odpovědi do DuckDuckGo (poháněno GPT-4o!)
// @description:cy Ychwanegu atebion AI i DuckDuckGo (a yrrir gan GPT-4o!)
// @description:da Tilføjer AI-svar til DuckDuckGo (drevet af GPT-4o!)
// @description:de Fügt AI-Antworten zu DuckDuckGo hinzu (betrieben von GPT-4o!)
// @description:el Προσθέτει απαντήσεις AI στο DuckDuckGo (τροφοδοτούμενο από GPT-4o!)
// @description:en Adds AI answers to DuckDuckGo (powered by GPT-4o!)
// @description:eo Aldonas AI-respondojn al DuckDuckGo (ebligita de GPT-4o!)
// @description:es Añade respuestas de IA a DuckDuckGo (impulsado por GPT-4o!)
// @description:et Lisab AI-vastused DuckDuckGo'le (juhitud GPT-4o-ga!)
// @description:eu Gehitu IA erantzunak DuckDuckGo-n (GPT-4o-k bultzatuta!)
// @description:fa پاسخهای هوشمصنوعی به DuckDuckGo اضافه میشود (توسط GPT-4o پشتیبانی میشود!)
// @description:fi Lisää tekoälyvastauksia DuckDuckGo:hun (ohjattu GPT-4o:lla!)
// @description:fil Nagdaragdag ng mga sagot ng AI sa DuckDuckGo (pinapagana ng GPT-4o!)
// @description:fo Bætir AI svar við DuckDuckGo (drifin af GPT-4o!)
// @description:fr Ajoute des réponses IA à DuckDuckGo (propulsé par GPT-4o!)
// @description:fr-CA Ajoute des réponses IA à DuckDuckGo (propulsé par GPT-4o!)
// @description:fy Foeget AI-antwurden ta oan DuckDuckGo (dreaun troch GPT-4o!)
// @description:ga Cuirtear freagraí AI le DuckDuckGo (dírítear ag GPT-4o!)
// @description:gd Cur freagairtichean AI ris an DuckDuckGo (air a thug seachad le GPT-4o!)
// @description:gl Engade respostas de IA a DuckDuckGo (impulsado por GPT-4o!)
// @description:gu DuckDuckGo માટે AI જવાબો ઉમેરે છે (GPT-4o દ્વારા પોવરેડ!)
// @description:ha Ƙaddara takardun AI zu DuckDuckGo (da aka fi GPT-4o!)
// @description:haw Hoʻohui aku i nā hoʻopiʻi AI iā DuckDuckGo (hoʻohui ʻia e GPT-4o!)
// @description:he מוסיף תשובות AI ל-DuckDuckGo (מופעל על ידי GPT-4o!)
// @description:hi DuckDuckGo में AI उत्तर जोड़ता है (GPT-4o द्वारा संचालित!)
// @description:hmn Ntxig AI nruab nruab rau DuckDuckGo (pab cuam GPT-4o!)
// @description:hr Dodaje AI odgovore na DuckDuckGo (pokreće GPT-4o!)
// @description:ht Ajoute repons AI nan DuckDuckGo (pòte pa GPT-4o!)
// @description:hu AI válaszokat ad hozzá a DuckDuckGo-hoz (GPT-4o által hajtva!)
// @description:hy Ավելացնում է AI պատասխաններ DuckDuckGo-ում (աջակցված է GPT-4o-ով!)
// @description:ia Adde responas AI a DuckDuckGo (propulsate per GPT-4o!)
// @description:id Menambahkan jawaban AI ke DuckDuckGo (didukung oleh GPT-4o!)
// @description:ig Tinye ihe ndekọ AI n'ụzọ ọgụgụ DuckDuckGo (n'efu na GPT-4o!)
// @description:ii DuckDuckGo ᐸᔦᒪᔪᐃᓃᑦ AI ᓇᑕᐅᒪᐃᑦᓯ (GPT-4o ᓂᑕᔪᑦᓯᐏᑦᑕᒥᔭ!)
// @description:is Bætir AI svar við DuckDuckGo (keyrir á GPT-4o!)
// @description:it Aggiunge risposte AI a DuckDuckGo (alimentato da GPT-4o!)
// @description:iu DuckDuckGo ᑲᑎᒪᔪᖅᑐᖅᑐᐃᓐᓇᓂᒃ AI ᑎᑎᕋᖃᕐᓯᒪᓂᖏᓐ (GPT-4o ᑐᑭᒧᑦᑖᑦ!)
// @description:ja DuckDuckGo に AI 回答を追加します (GPT-4o で動作!)
// @description:jv Nambéhi pirangga AI nganti DuckDuckGo (diduweni déning GPT-4o!)
// @description:ka ამატებს AI პასუხებს DuckDuckGo-ს (იმართება GPT-4o!)
// @description:kk DuckDuckGo-ға AI жауаптарын қосады (GPT-4o арқылы жұмыс істейді!)
// @description:kl DuckDuckGo-mi AI-t Kalaallit Nunaanni iluani (GPT-4o! -nip ilaanni!)
// @description:km បន្ថែមចម្លើយ AI ទៅ DuckDuckGo (ដំណើរការដោយ GPT-4o!)
// @description:kn DuckDuckGo ಗೆ AI ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (GPT-4o ನಿಂದ ನಡೆಸಲ್ಪಡುತ್ತಿದೆ!)
// @description:ko DuckDuckGo에 AI 답변을 추가합니다(GPT-4o 제공!)
// @description:ku Bersivên AI-ê li DuckDuckGo zêde dike (ji hêla GPT-4o ve hatî hêzdar kirin!)
// @description:ky DuckDuckGo'го AI жоопторун кошот (GPT-4o тарабынан иштейт!)
// @description:la Addit AI responsa DuckDuckGo (powered per GPT-4o!)
// @description:lb Füügt AI Äntwerten op DuckDuckGo (ugedriwwen duerch GPT-4o!)
// @description:lg Yambula emisomo ey'ensobi ku DuckDuckGo (enkuuma GPT-4o!)
// @description:ln Ebakisi biyano ya AI na DuckDuckGo (ezali na nguya ya GPT-4o!)
// @description:lo ເພີ່ມຄໍາຕອບ AI ໃຫ້ກັບ DuckDuckGo (ຂັບເຄື່ອນໂດຍ GPT-4o!)
// @description:lt Prideda AI atsakymus į „DuckDuckGo“ (maitina GPT-4o!)
// @description:lv Pievieno AI atbildes DuckDuckGo (darbina GPT-4o!)
// @description:mg Manampy valiny AI amin'ny DuckDuckGo (nampiasain'ny GPT-4o!)
// @description:mi Ka taapirihia nga whakautu AI ki a DuckDuckGo (whakamahia e GPT-4o!)
// @description:mk Додава одговори со вештачка интелигенција на DuckDuckGo (напојувано од GPT-4o!)
// @description:ml DuckDuckGo-യിലേക്ക് AI ഉത്തരങ്ങൾ ചേർക്കുന്നു (GPT-4o നൽകുന്നതാണ്!)
// @description:mn DuckDuckGo-д AI хариултуудыг нэмдэг (GPT-4o-оор ажилладаг!)
// @description:mr DuckDuckGo ला AI उत्तरे जोडते (GPT-4o द्वारे समर्थित!)
// @description:ms Menambahkan jawapan AI pada DuckDuckGo (dikuasakan oleh GPT-4o!)
// @description:mt Iżżid it-tweġibiet AI għal DuckDuckGo (mħaddma minn GPT-4o!)
// @description:my DuckDuckGo (GPT-4o ဖြင့် စွမ်းဆောင်ထားသည့်) တွင် AI အဖြေများကို ပေါင်းထည့်သည်
// @description:na Aeta AI teroma i DuckDuckGo (ira GPT-4o reke akea!)
// @description:nb Legger til AI-svar på DuckDuckGo (drevet av GPT-4o!)
// @description:nd Iyatholakala amaswelelo e-AI kuDuckDuckGo (kuyatholakala ngokulawula uGPT-4o!)
// @description:ne DuckDuckGo मा AI जवाफहरू थप्छ (GPT-4o द्वारा संचालित!)
// @description:ng Ondjova mbelelo dha AI moDuckDuckGo (uumbuli nguGPT-4o!)
// @description:nl Voegt AI-antwoorden toe aan DuckDuckGo (mogelijk gemaakt door GPT-4o!)
// @description:nn Legg til AI-svar på DuckDuckGo (drevet av GPT-4o!)
// @description:no Legger til AI-svar til DuckDuckGo (drevet av GPT-4o!)
// @description:nso Ya go etela ditshenyegi tsa AI mo DuckDuckGo (e dirwang ke GPT-4o!)
// @description:ny Imawonjezera mayankho a AI ku DuckDuckGo (yoyendetsedwa ndi GPT-4o!)
// @description:oc Ajusta de respòstas d'IA a DuckDuckGo (amb GPT-4o!)
// @description:om Deebii AI DuckDuckGo (GPT-4o'n kan hojjetu!) irratti dabalata.
// @description:or DuckDuckGo କୁ AI ଉତ୍ତର ଯୋଗ କରେ (GPT-4o ଦ୍ୱାରା ଚାଳିତ!)
// @description:pa DuckDuckGo (GPT-4o ਦੁਆਰਾ ਸੰਚਾਲਿਤ!) ਵਿੱਚ AI ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ
// @description:pl Dodaje odpowiedzi AI do DuckDuckGo (obsługiwane przez GPT-4o!)
// @description:ps DuckDuckGo ته د AI ځوابونه اضافه کوي (د GPT-4o لخوا پرمخ وړل کیږي!)
// @description:pt Adiciona respostas de IA ao DuckDuckGo (desenvolvido por GPT-4o!)
// @description:pt-BR Adiciona respostas de IA ao DuckDuckGo (desenvolvido por GPT-4o!)
// @description:qu DuckDuckGo (GPT-4o nisqawan kallpachasqa!) nisqaman AI kutichiykunata yapan.
// @description:rm Agiuntescha respostas d'IA a DuckDuckGo (propulsà da GPT-4o!)
// @description:rn Abafasha inyandiko z'IA ku DuckDuckGo (yashyizweho na GPT-4o!)
// @description:ro Adaugă răspunsuri AI la DuckDuckGo (alimentat de GPT-4o!)
// @description:ru Добавляет ответы ИИ в DuckDuckGo (на базе GPT-4o!)
// @description:rw Ongeraho ibisubizo bya AI kuri DuckDuckGo (ikoreshwa na GPT-4o!)
// @description:sa DuckDuckGo (GPT-4o द्वारा संचालितम्!) इत्यत्र AI उत्तराणि योजयति ।
// @description:sat DuckDuckGo ar AI jawab khon ojantok (GPT-4o! sebadha manju)
// @description:sc Agiungit rispostas de IA a DuckDuckGo (motorizadu da GPT-4o!)
// @description:sd شامل ڪري ٿو AI جوابن کي DuckDuckGo (GPT-4o پاران طاقتور!)
// @description:se Lávdegáhtii AI vástid DuckDuckGo (GPT-4o! vuosttas!)
// @description:sg Nâ tî-kûzâ mái vêdáara AI mbi DuckDuckGo (ngâ GPT-4o!)
// @description:si DuckDuckGo වෙත AI පිළිතුරු එක් කරයි (GPT-4o මගින් බලගන්වයි!)
// @description:sk Pridáva odpovede AI do DuckDuckGo (poháňané GPT-4o!)
// @description:sl Dodaja odgovore AI v DuckDuckGo (poganja GPT-4o!)
// @description:sm Faʻaopoopo tali AI ile DuckDuckGo (faʻamalosia e GPT-4o!)
// @description:sn Inowedzera mhinduro dzeAI kuDuckDuckGo (inofambiswa neGPT-4o!)
// @description:so Waxay ku dartay jawaabaha AI DuckDuckGo (waxaa ku shaqeeya GPT-4o!)
// @description:sq Shton përgjigjet e AI në DuckDuckGo (mundësuar nga GPT-4o!)
// @description:sr Додаје АИ одговоре у DuckDuckGo (покреће ГПТ-4о!)
// @description:ss Iphendvulela izindlela zezilungiselelo ku-DuckDuckGo (izenzakalo nge-GPT-4o!)
// @description:st E kopanetse diqoqo tsa AI ka DuckDuckGo (ka sebelisoa ke GPT-4o!)
// @description:su Nambahkeun jawaban AI kana DuckDuckGo (dikuatkeun ku GPT-4o!)
// @description:sv Lägger till AI-svar till DuckDuckGo (driven av GPT-4o!)
// @description:sw Inaongeza majibu ya AI kwa DuckDuckGo (inaendeshwa na GPT-4o!)
// @description:ta DuckDuckGo க்கு AI பதில்களைச் சேர்க்கிறது (GPT-4o மூலம் இயக்கப்படுகிறது!)
// @description:te DuckDuckGoకి AI సమాధానాలను జోడిస్తుంది (GPT-4o ద్వారా ఆధారితం!)
// @description:tg Ба DuckDuckGo ҷавобҳои AI илова мекунад (аз ҷониби GPT-4o!)
// @description:th เพิ่มคำตอบ AI ให้กับ DuckDuckGo (ขับเคลื่อนโดย GPT-4o!)
// @description:ti ናብ DuckDuckGo (ብGPT-4o ዝሰርሕ!) ናይ AI መልስታት ይውስኸሉ።
// @description:tk DuckDuckGo-a AI jogaplaryny goşýar (GPT-4o bilen işleýär!)
// @description:tl Nagdadagdag ng mga sagot ng AI sa DuckDuckGo (pinapatakbo ng GPT-4o!)
// @description:tn O amogela dipotso tsa AI mo DuckDuckGo (e a nang le GPT-4o!)
// @description:to Tambisa mabizo a AI ku DuckDuckGo (mukutenga na GPT-4o!)
// @description:tr DuckDuckGo'ya yapay zeka yanıtları ekler (GPT-4o tarafından desteklenmektedir!)
// @description:ts Ku engetela tinhlamulo ta AI eka DuckDuckGo (leyi fambiwaka hi GPT-4o!)
// @description:tt DuckDuckGo'ка AI җаваплары өсти (GPT-4o белән эшләнгән!)
// @description:tw Ɔde AI mmuae ka DuckDuckGo (a GPT-4o na ɛma ahoɔden!) ho.
// @description:ug DuckDuckGo ۋەبسېتكە AI جاۋابلار قوشۇدۇ (GPT-4o تەكشۈرگۈچى بىلەن!)
// @description:uk Додає відповіді штучного інтелекту в DuckDuckGo (на базі GPT-4o!)
// @description:ur DuckDuckGo میں AI جوابات شامل کرتا ہے (GPT-4o کے ذریعے تقویت یافتہ!)
// @description:uz DuckDuckGo-ga AI javoblarini qo'shadi (GPT-4o tomonidan quvvatlanadi!)
// @description:vi Thêm câu trả lời AI vào DuckDuckGo (được cung cấp bởi GPT-4o!)
// @description:xh Yongeza iimpendulo ze-AI kwi-DuckDuckGo (ixhaswe yi-GPT-4o!)
// @description:yi לייגט אַי ענטפֿערס צו DuckDuckGo (Powered דורך GPT-4o!)
// @description:yo Ṣe afikun awọn idahun AI si DuckDuckGo (agbara nipasẹ GPT-4o!)
// @description:zh 为 DuckDuckGo 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-CN 为 DuckDuckGo 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-HK 為 DuckDuckGo 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zh-SG 为 DuckDuckGo 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-TW 為 DuckDuckGo 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zu Yengeza izimpendulo ze-AI ku-DuckDuckGo (inikwa amandla yi-GPT-4o!)
// @author KudoAI
// @namespace https://kudoai.com
// @version 2014.9.11.3
// @license MIT
// @icon https://media.ddgpt.com/images/icons/duckduckgpt/icon48.png?af89302
// @icon64 https://media.ddgpt.com/images/icons/duckduckgpt/icon64.png?af89302
// @antifeature ads A very tiny text ad displays below DuckDuckGPT chatbar. This motivates me to spend otherwise unpaid time upgrading script w/ new features & APIs.
// @antifeature referral-link
// @compatible chrome except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible firefox
// @compatible edge except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible opera after allowing userscript manager access to search page results in opera://extensions
// @compatible brave except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible vivaldi except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible waterfox
// @compatible librewolf
// @compatible ghost
// @compatible qq
// @compatible whale
// @compatible kiwi
// @compatible mask
// @compatible orion
// @match *://duckduckgo.com/?*
// @include https://auth0.openai.com
// @connect binjie.fun
// @connect chatgpt.com
// @connect gptforlove.com
// @connect greasyfork.org
// @connect jsdelivr.net
// @connect mixerbox.com
// @connect openai.com
// @connect promplate.dev
// @connect sogou.com
// @require https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@3.2.1/dist/chatgpt.min.js#sha256-o4SiKP/O2pQ6Uizl38AAkZHPkW+u4sk0yct7DcSgsFM=
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require https://cdn.jsdelivr.net/npm/generate-ip@2.4.2/dist/generate-ip.min.js#sha256-PRvQIDVWK/a+aAqEFVQv7RePbRe/tX6tWQVM80rAe2M=
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha256-g3pvpbDHNrUrveKythkPMF2j/J7UFoHbUyFQcFe1yEY=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @resource ddgptIcon https://cdn.jsdelivr.net/gh/KudoAI/duckduckgpt@9cb2fec/media/images/icons/duckduckgpt/icon64.png.b64#sha256-k7hl9PAq+HAKG2vS9wlKmu3EEvdE3k2Z2KR/SRkk6D4=
// @resource ddgptLSlogo https://cdn.jsdelivr.net/gh/KudoAI/duckduckgpt@edc8ee5/media/images/logos/duckduckgpt/lightmode/logo697x122.png.b64#sha256-7O4AxPinoZ6h36KHuJVa4vwfTEOYTwT+lKiDbf/jjkg=
// @resource ddgptDSlogo https://cdn.jsdelivr.net/gh/KudoAI/duckduckgpt@edc8ee5/media/images/logos/duckduckgpt/darkmode/logo697x122.png.b64#sha256-lSd4M3RPT4+SjjBk8PKGFoyM9p3rZHgxt0NgoKqQkiM=
// @resource hljsCSS https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css#sha256-v0N76BFFkH0dCB8bUr4cHSVN8A/zCaOopMuSmJWV/5w=
// @resource bsbgCSS https://cdn.jsdelivr.net/gh/KudoAI/duckduckgpt@54f7f2c/styles/css/black-rising-stars.min.css#sha256-bXbVZUD7ciKqK0wU/BLQzh08JwkoNExHHqXITugd/3o=
// @resource wsbgCSS https://cdn.jsdelivr.net/gh/KudoAI/duckduckgpt@54f7f2c/styles/css/white-rising-stars.min.css#sha256-ya9newifevSPO1Q4AzMf42yAF6TE+iZHrDbVj0HyuEM=
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_cookie
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getResourceText
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @noframes
// @homepageURL https://www.duckduckgpt.com
// @supportURL https://support.duckduckgpt.com
// @contributionURL https://github.com/sponsors/KudoAI
// ==/UserScript==
// Dependencies:
// ✓ chatgpt.js (https://chatgpt.js.org) © 2023–2024 KudoAI & contributors under the MIT license
// ✓ generate-ip (https://generate-ip.org) © 2024 Adam Lui & contributors under the MIT license
// ✓ highlight.js (https://highlightjs.org) © 2006 Ivan Sagalaev under the BSD 3-Clause license
// ✓ KaTeX (https://katex.org) © 2013–2020 Khan Academy & other contributors under the MIT license
// ✓ Marked (https://marked.js.org) © 2018+ MarkedJS © 2011–2018 Christopher Jeffrey under the MIT license
// Documentation: https://docs.ddgpt.com
(async () => {
// Init APP INFO
const app = {
name: 'DuckDuckGPT', symbol: '🐤', configKeyPrefix: 'duckDuckGPT',
urls: {
app: 'https://www.duckduckgpt.com', publisher: 'https://www.kudoai.com', support: 'https://support.ddgpt.com',
chatgptJS: 'https://chatgpt.js.org', relatedApps: 'https://github.com/adamlui/chatgpt-apps',
alternativeTo: 'https://alternativeto.net/software/duckduckgpt',
futurepedia: 'https://www.futurepedia.io/tool/duckduckgpt',
gitHub: 'https://github.com/KudoAI/duckduckgpt',
greasyFork: 'https://greasyfork.org/scripts/459849-duckduckgpt',
productHunt: 'https://www.producthunt.com/products/duckduckgpt' },
latestAssetCommitHash: '0b230e3' // for cached messages.json
}
app.urls.assetHost = app.urls.gitHub.replace('github.com', 'cdn.jsdelivr.net/gh') + `@${app.latestAssetCommitHash}/`
app.urls.update = app.urls.greasyFork.replace('https://', 'https://update.')
.replace(/(\d+)-?([a-zA-Z-]*)$/, (_, id, name) => `${id}/${ !name ? 'script' : name }.meta.js`)
// Init BROWSER flags
const browser = {} ; ['Chrome', 'Firefox', 'Edge', 'Brave', 'Mobile'].forEach(platform =>
browser['is' + platform] = chatgpt.browser['is' + platform]())
browser.isPortrait = browser.isMobile && (window.innerWidth < window.innerHeight)
// Init DEBUG mode
const config = {} ; loadSetting('debugMode')
// Define LOG functions/props
const log = {
styles: {
prefix: {
base: `color: white ; padding: 2px 3px 2px 5px ; border-radius: 2px ; ${ browser.isFirefox ? 'font-size: 13px ;' : '' }`,
info: 'background: linear-gradient(344deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 39%, rgba(30,29,43,0.6026611328125) 93%)',
working: 'background: linear-gradient(342deg, rgba(255,128,0,1) 0%, rgba(255,128,0,0.9612045501794468) 57%, rgba(255,128,0,0.7539216370141807) 93%)' ,
success: 'background: linear-gradient(344deg, rgba(0,107,41,1) 0%, rgba(3,147,58,1) 39%, rgba(24,126,42,0.7735294801514356) 93%)',
warning: 'background: linear-gradient(344deg, rgba(255,0,0,1) 0%, rgba(232,41,41,0.9079832616640406) 57%, rgba(222,49,49,0.6530813008797269) 93%)',
caller: 'color: blue'
},
msg: { working: 'color: #ff8000', warning: 'color: red' }
},
regEx: { greenChars: /\b(?:true|\d+)\b|success\W?/i, redChars: /\bfalse\b|error\W?/i, purpChars: /[ '"]\w+['"]?: /i },
prettifyObj(obj) { return JSON.stringify(obj)
.replace(/([{,](?=")|(?:"):)/g, '$1 ') // append spaces to { and "
.replace(/((?<!})})/g, ' $1') // prepend spaces to }
.replace(/"/g, '\'') // replace " w/ '
},
toTitleCase(str) { return str.charAt(0).toUpperCase() + str.slice(1) }
} ; ['info', 'error', 'debug'].forEach(logType =>
log[logType] = function() {
if (logType == 'debug' && !config.debugMode) return
const args = Array.from(arguments).map(arg => typeof arg == 'object' ? JSON.stringify(arg) : arg),
msgType = args.some(arg => /\.{3}$/.test(arg)) ? 'working'
: args.some(arg => /\bsuccess\b|!$/i.test(arg)) ? 'success'
: args.some(arg => /\b(?:error|fail)\b/i.test(arg)) || logType == 'error' ? 'warning' : 'info',
prefixStyle = log.styles.prefix.base + log.styles.prefix[msgType],
baseMsgStyle = log.styles.msg[msgType], msgStyles = []
// Combine args into finalMsg, color chars
let finalMsg = logType == 'error' && args.length == 1 ? 'ERROR: ' : ''
args.forEach((arg, idx) => {
finalMsg += idx == 1 ? ': ' : idx > 1 ? ' ' : '' // separate multi-args
finalMsg += arg?.toString().replace(new RegExp(Object.values(log.regEx).map(value => value.source).join('|'), 'ig'), match => {
if (log.regEx.greenChars.test(match)) { msgStyles.push('color: green', baseMsgStyle) ; return `%c${match}%c` }
else if (log.regEx.redChars.test(match)) { msgStyles.push('color: red', baseMsgStyle) ; return `%c${match}%c` }
else if (log.regEx.purpChars.test(match)) { msgStyles.push('color: #dd29f4', baseMsgStyle) ; return `%c${match}%c` }
})
})
console[logType == 'error' ? logType : 'info'](
`${app.symbol} %c${app.name}%c ${ log.caller ? `${log.caller} » ` : '' }%c${finalMsg}`,
prefixStyle, log.styles.prefix.caller, baseMsgStyle, ...msgStyles
)
}
)
// Init COMPATIBILITY flags
log.debug('Initializing compatibility flags...')
const streamingSupported = {
browser: !(getUserscriptManager() == 'Tampermonkey' && (browser.isChrome || browser.isEdge || browser.isBrave)),
userscriptManager: /Tampermonkey|ScriptCat/.test(getUserscriptManager()) }
log.debug(`Success! streamingSupported = ${log.prettifyObj(streamingSupported)}`)
// Init CONFIG
log.debug('Initializing config...')
Object.assign(config, { minFontSize: 11, maxFontSize: 24, lineHeightRatio: 1.28 })
config.userLanguage = chatgpt.getUserLanguage()
config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
loadSetting('anchored', 'autoGet', 'autoFocusChatbarDisabled', 'autoScroll', 'bgAnimationsDisabled', 'expanded',
'fgAnimationsDisabled', 'fontSize', 'minimized', 'notFirstRun', 'prefixEnabled', 'proxyAPIenabled',
'replyLanguage', 'rqDisabled', 'scheme', 'stickySidebar', 'streamingDisabled', 'suffixEnabled', 'widerSidebar')
if (!config.replyLanguage) saveSetting('replyLanguage', config.userLanguage) // init reply language if unset
if (!config.fontSize) saveSetting('fontSize', 14) // init reply font size if unset
if (!streamingSupported.browser || !streamingSupported.userscriptManager) saveSetting('streamingDisabled', true) // disable Streaming in unspported env
if (!config.notFirstRun && browser.isMobile) saveSetting('autoGet', true) // reverse default auto-get disabled if mobile
saveSetting('notFirstRun', true)
log.debug(`Success! config = ${log.prettifyObj(config)}`)
// Init UI VARS
log.debug('Initializing UI variables...')
let scheme = config.scheme || ( chatgpt.isDarkMode() ? 'dark' : 'light' )
const isCentered = isCenteredMode()
log.debug(`Success! scheme = '${scheme}', isCentered = ${isCentered}`)
// Init XHR fetcher
log.debug('Initializing XHR fetcher...')
const xhr = getUserscriptManager() == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest
log.debug(`Success! xhr = ${ getUserscriptManager() == 'OrangeMonkey' ? 'GM_xmlhttpRequest' : 'GM.xmlHttpRequest' }`)
// Init API props
log.debug('Initializing API properties...')
const apis = {
'AIchatOS': {
endpoint: 'https://api.binjie.fun/api/generateStream',
expectedOrigin: {
url: 'https://chat18.aichatos8.com',
headers: { 'Accept': 'application/json, text/plain, */*', 'Priority': 'u=0', 'Sec-Fetch-Site': 'cross-site' }},
method: 'POST', streamable: true, accumulatesText: false, failFlags: ['很抱歉地', '系统公告'],
userID: '#/chat/' + Date.now() },
'Free Chat': {
endpoint: 'https://demo-railway.promplate.dev/single/chat_messages',
expectedOrigin: {
url: 'https://e10.frechat.xyz',
headers: { 'Accept': '*/*', 'Priority': 'u=0', 'Sec-Fetch-Site': 'cross-site' }},
method: 'PUT', streamable: true, accumulatesText: false,
failFlags: [
'invalid_request_error', 'literal_error', 'me@promplate.dev', '^Not Found$',
'Sorry, your account balance is insufficient', 'This service has been suspended', 'your free credit'],
availModels: [
'deepseek-ai/deepseek-llm-67b-chat', 'gemma2-9b-it', 'THUDM/glm-4-9b-chat', 'gpt-4o-mini-2024-07-18',
'llama3-70b-8192', 'mixtral-8x7b-32768', 'nous-hermes-2-mixtral-8x7b-dpo', 'Qwen/Qwen2-57B-A14B-Instruct',
'01-ai/Yi-1.5-34B-Chat-16K' ]},
'GPTforLove': {
endpoint: 'https://api11.gptforlove.com/chat-process',
expectedOrigin: {
url: 'https://ai27.gptforlove.com',
headers: { 'Accept': 'application/json, text/plain, */*', 'Priority': 'u=0', 'Sec-Fetch-Site': 'same-site' }},
method: 'POST', streamable: true, accumulatesText: true, failFlags: ['[\'"]?status[\'"]?:\\s*[\'"]Fail[\'"]'] },
'MixerBox AI': {
endpoint: 'https://chatai.mixerbox.com/api/chat/stream',
expectedOrigin: {
url: 'https://chatai.mixerbox.com',
headers: { 'Accept': '*/*', 'Alt-Used': 'chatai.mixerbox.com', 'Sec-Fetch-Site': 'same-origin' }},
method: 'POST', streamable: true, accumulatesText: false },
'OpenAI': {
endpoints: {
auth: 'https://auth0.openai.com',
completions: 'https://api.openai.com/v1/chat/completions',
session: 'https://chatgpt.com/api/auth/session' },
expectedOrigin: {
url: 'https://chatgpt.com',
headers: { 'Accept': '*/*', 'Priority': 'u=4', 'Sec-Fetch-Site': 'same-site' }},
method: 'POST', streamable: true }
}
log.debug(`Success! apis = ${log.prettifyObj(apis)}`)
// Init INPUT EVENTS
log.debug('Initializing input events...')
const inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
inputEvents[action] = ( window.PointerEvent ? 'pointer' : browser.isMobile ? 'touch' : 'mouse' ) + action)
log.debug(`Success! inputEvents = ${log.prettifyObj(inputEvents)}`)
// Init localized MESSAGES
log.debug('Initializing localized messages...')
let msgs = {}
if (!config.userLanguage.startsWith('en')) msgs = await new Promise(resolve => {
const msgHostDir = app.urls.assetHost + 'greasemonkey/_locales/',
msgLocaleDir = ( config.userLanguage ? config.userLanguage.replace('-', '_') : 'en' ) + '/'
let msgHref = msgHostDir + msgLocaleDir + 'messages.json', msgXHRtries = 0
xhr({ method: 'GET', url: msgHref, onload: onLoad })
function onLoad(resp) {
try { // to return localized messages.json
const msgs = JSON.parse(resp.responseText), flatMsgs = {}
for (const key in msgs) // remove need to ref nested keys
if (typeof msgs[key] == 'object' && 'message' in msgs[key])
flatMsgs[key] = msgs[key].message
resolve(flatMsgs)
} catch (err) { // if bad response
msgXHRtries++ ; if (msgXHRtries == 3) return resolve({}) // try up to 3X (original/region-stripped/EN) only
msgHref = config.userLanguage.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
msgHref.replace(/([^_]+_[^_]+)_[^/]*(\/.*)/, '$1$2') // ...strip region before retrying
: ( msgHostDir + 'en/messages.json' ) // else use default English messages
xhr({ method: 'GET', url: msgHref, onload: onLoad })
}
}
})
log.debug(Object.keys(msgs).length > 0 ? `Success! msgs = ${log.prettifyObj(msgs)}` : 'Skipped due to English locale')
// Init SETTINGS props
log.debug('Initializing settings properties...')
const settingsProps = {
proxyAPIenabled: { type: 'toggle', icon: 'sunglasses',
label: msgs.menuLabel_proxyAPImode || 'Proxy API Mode',
helptip: msgs.helptip_proxyAPImode || 'Uses a Proxy API for no-login access to AI' },
streamingDisabled: { type: 'toggle', icon: 'signalStream',
label: msgs.mode_streaming || 'Streaming Mode',
helptip: msgs.helptip_streamingMode || 'Receive replies in a continuous text stream' },
autoGet: { type: 'toggle', icon: 'speechBalloonLasso',
label: msgs.menuLabel_autoGetAnswers || 'Auto-Get Answers',
helptip: msgs.helptip_autoGetAnswers || 'Auto-send queries to DuckDuckGPT when using search engine' },
autoFocusChatbarDisabled: { type: 'toggle', mobile: false, icon: 'caretsInward',
label: msgs.menuLabel_autoFocusChatbar || 'Auto-Focus Chatbar',
helptip: msgs.helptip_autoFocusChatbar || 'Auto-focus chatbar whenever it appears' },
autoScroll: { type: 'toggle', mobile: false, icon: 'arrowsDown',
label: `${ msgs.mode_autoScroll || 'Auto-Scroll' } (${ msgs.menuLabel_whenStreaming || 'when streaming' })`,
helptip: msgs.helptip_autoScroll || 'Auto-scroll responses as they generate in Streaming Mode' },
rqDisabled: { type: 'toggle', icon: 'speechBalloons',
label: `${ msgs.menuLabel_show || 'Show' } ${ msgs.menuLabel_relatedQueries || 'Related Queries' }`,
helptip: msgs.helptip_showRelatedQueries || 'Show related queries below chatbar' },
prefixEnabled: { type: 'toggle', icon: 'slash',
label: `${ msgs.menuLabel_require || 'Require' } "/" ${ msgs.menuLabel_beforeQuery || 'before query' }`,
helptip: msgs.helptip_prefixMode || 'Require "/" before queries for answers to show' },
suffixEnabled: { type: 'toggle', icon: 'questionMark',
label: `${ msgs.menuLabel_require || 'Require' } "?" ${ msgs.menuLabel_afterQuery || 'after query' }`,
helptip: msgs.helptip_suffixMode || 'Require "?" after queries for answers to show' },
widerSidebar: { type: 'toggle', mobile: false, centered: false, icon: 'widescreen',
label: msgs.menuLabel_widerSidebar || 'Wider Sidebar',
helptip: msgs.helptip_widerSidebar || 'Horizontally expand search page sidebar' },
stickySidebar: { type: 'toggle', mobile: false, centered: false, icon: 'webCorner',
label: msgs.menuLabel_stickySidebar || 'Sticky Sidebar',
helptip: msgs.helptip_stickySidebar || 'Makes DuckDuckGPT visible in sidebar even as you scroll' },
anchored: { type: 'toggle', mobile: false, centered: false, icon: 'anchor',
label: msgs.mode_anchor || 'Anchor Mode',
helptip: msgs.helptip_anchorMode || 'Anchor DuckDuckGPT to bottom of window' },
bgAnimationsDisabled: { type: 'toggle', icon: 'sparkles',
label: `${ msgs.menuLabel_background || 'Background' } ${ msgs.menuLabel_animations || 'Animations' }`,
helptip: msgs.helptip_bgAnimations || 'Show animated backgrounds in UI components' },
fgAnimationsDisabled: { type: 'toggle', icon: 'sparkles',
label: `${ msgs.menuLabel_foreground || 'Foreground' } ${ msgs.menuLabel_animations || 'Animations' }`,
helptip: msgs.helptip_fgAnimations || 'Show foreground animations in UI components' },
replyLanguage: { type: 'prompt', icon: 'languageChars',
label: msgs.menuLabel_replyLanguage || 'Reply Language',
helptip: msgs.helptip_replyLanguage || 'Language for DuckDuckGPT to reply in' },
scheme: { type: 'modal', icon: 'scheme',
label: msgs.menuLabel_colorScheme || 'Color Scheme',
helptip: msgs.helptip_colorScheme || 'Scheme to display DuckDuckGPT UI components in' },
debugMode: { type: 'toggle', icon: 'bug',
label: msgs.mode_debug || 'Debug Mode',
helptip: msgs.helptip_debugMode || 'Show detailed logging in browser console' },
about: { type: 'modal', icon: 'questionMarkCircle',
label: `${ msgs.menuLabel_about || 'About' } ${app.name}...` }
}
log.debug(`Success! settingsProps = ${log.prettifyObj(settingsProps)}`)
// Init MENU objs
log.debug('Initializing menu objects...')
const menuIDs = [] // to store registered cmds for removal while preserving order
const menuState = {
symbol: ['❌', '✔️'], separator: getUserscriptManager() == 'Tampermonkey' ? ' — ' : ': ',
word: [(msgs.state_off || 'Off').toUpperCase(), (msgs.state_on || 'On').toUpperCase()]
}
log.debug(`Success! menuState = ${log.prettifyObj(menuState)}`)
// Define SCRIPT functions
function loadSetting(...keys) { keys.forEach(key => config[key] = GM_getValue(app.configKeyPrefix + '_' + key, false)) }
function saveSetting(key, value) { GM_setValue(app.configKeyPrefix + '_' + key, value) ; config[key] = value }
function safeWindowOpen(url) { window.open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
function getUserscriptManager() { try { return GM_info.scriptHandler } catch (err) { return 'other' }}
// Define MENU functions
function registerMenu() {
// Add command to toggle proxy API mode
const pmLabel = menuState.symbol[+config.proxyAPIenabled] + ' '
+ settingsProps.proxyAPIenabled.label + ' '
+ menuState.separator + menuState.word[+config.proxyAPIenabled]
menuIDs.push(GM_registerMenuCommand(pmLabel, toggle.proxyMode))
// Add command to launch About modal
const aboutLabel = `💡 ${settingsProps.about.label}`
menuIDs.push(GM_registerMenuCommand(aboutLabel, modals.about.show))
// Add command to launch Settings modal
const settingsLabel = `⚙️ ${ msgs.menuLabel_settings || 'Settings' }`
menuIDs.push(GM_registerMenuCommand(settingsLabel, modals.settings.show))
}
function promptReplyLang() {
log.caller = 'promptReplyLang()'
while (true) {
let replyLanguage = prompt(
( msgs.prompt_updateReplyLang || 'Update reply language' ) + ':', config.replyLanguage)
if (replyLanguage == null) break // user cancelled so do nothing
else if (!/\d/.test(replyLanguage)) {
replyLanguage = ( // auto-case for menu/alert aesthetics
[2, 3].includes(replyLanguage.length) || replyLanguage.includes('-') ? replyLanguage.toUpperCase()
: replyLanguage.charAt(0).toUpperCase() + replyLanguage.slice(1).toLowerCase() )
log.debug('Saving reply language...')
saveSetting('replyLanguage', replyLanguage || config.userLanguage)
log.debug(`Success! config.replyLanguage = ${config.replyLanguage}`)
const langUpdatedAlertID = siteAlert(( msgs.alert_langUpdated || 'Language updated' ) + '!', // title
`${ app.name } ${ msgs.alert_willReplyIn || 'will reply in' } `
+ ( replyLanguage || msgs.alert_yourSysLang || 'your system language' ) + '.',
'', '', 330) // confirmation width
const langUpdatedAlert = document.getElementById(langUpdatedAlertID).firstChild
modals.init(langUpdatedAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
if (modals.settings.get()) // update settings menu status label
document.querySelector('#replyLanguage-menu-entry span').textContent = replyLanguage
break
}}}
function refreshMenu() {
log.caller = `refreshMenu()`
log.debug('Refreshing toolbar menu...')
if (getUserscriptManager() == 'OrangeMonkey') { log.debug('OrangeMonkey userscript manager unsupported.') ; return }
for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu()
log.debug('Success! Menu refreshed')
}
function updateCheck() {
log.caller = 'updateCheck()'
const currentVer = GM_info.script.version
log.debug(`currentVer = ${currentVer}`)
// Fetch latest meta
log.debug('Fetching latest userscript meta...')
xhr({
method: 'GET', url: app.urls.update + '?t=' + Date.now(),
headers: { 'Cache-Control': 'no-cache' },
onload: resp => {
log.debug('Success! Response received')
const updateAlertWidth = 409
// Compare versions
log.debug('Comparing versions...')
const latestVer = /@version +(.*)/.exec(resp.responseText)[1]
for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
latestSubVer = parseInt(latestVer.split('.')[i], 10) || 0
if (currentSubVer > latestSubVer) break // out of comparison since not outdated
else if (latestSubVer > currentSubVer) { // if outdated
// Alert to update
log.debug(`Update v${latestVer} found!`)
const updateModalID = siteAlert(`🚀 ${ msgs.alert_updateAvail || 'Update available' }!`, // title
`${ msgs.alert_newerVer || 'An update to' } ${ app.name } `
+ `(v${latestVer}) ${ msgs.alert_isAvail || 'is available' }! `
+ '<a target="_blank" rel="noopener" style="font-size: 1.1rem" '
+ 'href="' + app.urls.gitHub + '/commits/main/greasemonkey/'
+ app.urls.update.replace(/.*\/(.*)meta\.js/, '$1user.js') + '"'
+ `>${ msgs.link_viewChanges || 'View changes' }</a>`,
function update() { // button
safeWindowOpen(app.urls.update.replace('meta.js', 'user.js') + '?t=' + Date.now())
}, '', updateAlertWidth
)
const updateModal = document.getElementById(updateModalID).firstChild
// Localize button labels if needed
if (!config.userLanguage.startsWith('en')) {
log.debug('Localizing button labels in non-English alert...')
const updateAlert = document.querySelector(`[id="${ updateModalID }"]`),
updateBtns = updateAlert.querySelectorAll('button')
updateBtns[1].textContent = msgs.btnLabel_update || 'Update'
updateBtns[0].textContent = msgs.btnLabel_dismiss || 'Dismiss'
}
modals.init(updateModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
return
}}
// Alert to no update found, nav back
log.debug('No update found.')
const noUpdateModalID = siteAlert(( msgs.alert_upToDate || 'Up-to-date' ) + '!', // title
`${ app.name } (v${ currentVer }) ${ msgs.alert_isUpToDate || 'is up-to-date' }!`, // msg
'', '', updateAlertWidth)
const noUpdateModal = document.getElementById(noUpdateModalID).firstChild
modals.init(noUpdateModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
modals.about.show()
}})}
// Define FEEDBACK functions
function notify(msg, position = '', notifDuration = '', shadow = 'shadow') {
// Strip state word to append styled one later
const foundState = menuState.word.find(word => msg.includes(word))
if (foundState) msg = msg.replace(foundState, '')
// Show notification
chatgpt.notify(msg, position, notifDuration, shadow)
const notif = document.querySelector('.chatgpt-notif:last-child')
// Prepend app icon
const notifIcon = icons.ddgpt.create()
notifIcon.style.cssText = 'width: 31px ; position: relative ; top: 5.8px ; margin-right: 8px'
notif.prepend(notifIcon)
// Append notif type icon
const iconStyles = 'width: 28px ; height: 28px ; position: relative ; top: 3.5px ; margin-left: 11px ;',
mode = Object.keys(settingsProps).find(key => settingsProps[key].label.includes(msg.trim()))
if (mode && !/(?:pre|suf)fix/.test(mode)) {
const modeIcon = icons[settingsProps[mode].icon].create()
modeIcon.style.cssText = iconStyles
+ ( /autoget|focus|scroll/i.test(mode) ? 'top: -3px' : '' ) // raise some icons
+ ( /animation|debug/i.test(mode) ? 'width: 25px ; height: 25px' : '' ) // shrink some icon
notif.append(modeIcon)
}
// Append styled state word
if (foundState) {
const styledState = document.createElement('span')
styledState.style.cssText = `font-weight: bold ; color: ${
foundState == menuState.word[0] ? '#ef4848 ; text-shadow: rgba(255, 169, 225, 0.44) 2px 1px 5px'
: '#5cef48 ; text-shadow: rgba(255, 250, 169, 0.38) 2px 1px 5px' }`
styledState.append(foundState) ; notif.insertBefore(styledState, notif.children[2])
}
}
function siteAlert(title = '', msg = '', btns = '', checkbox = '', width = '') {
return chatgpt.alert(title, msg, btns, checkbox, width )}
function appAlert(...alerts) {
log.caller = 'appAlert()'
alerts = alerts.flat() // flatten array args nested by spread operator
while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear appDiv content
const alertP = document.createElement('p')
alertP.id = 'ddgpt-alert' ; alertP.className = 'no-user-select'
alerts.forEach((alert, idx) => { // process each alert for display
let msg = appAlerts[alert] || alert // use string verbatim if not found in appAlerts
if (idx > 0) msg = ' ' + msg // left-pad 2nd+ alerts
if (msg.includes(appAlerts.login)) session.deleteOpenAIcookies()
if (msg.includes(appAlerts.waitingResponse)) alertP.classList.add('loading')
// Add login link to login msgs
if (msg.includes('@')) {
log.debug('Adding login link...')
msg += '<a class="alert-link" target="_blank" rel="noopener" href="https://chatgpt.com">chatgpt.com</a>,'
+ ` ${ msgs.alert_thenRefreshPage || 'then refresh this page' }.`
+ ` (${ msgs.alert_ifIssuePersists || 'If issue persists' },`
+ ` ${( msgs.alert_try || 'Try' ).toLowerCase() }`
+ ` ${ msgs.alert_switchingOn || 'switching on' }`
+ ` ${ msgs.mode_proxy || 'Proxy Mode' })`
}
// Hyperlink msgs.alert_switching<On|Off>
const foundState = ['On', 'Off'].find(state =>
msg.includes(msgs['alert_switching' + state]) || new RegExp(`\\b${state}\\b`, 'i').test(msg))
if (foundState) { // hyperlink switch phrase for click listener to toggle.proxyMode()
log.debug('Hyperlinking on/off state in alert...')
const switchPhrase = msgs['alert_switching' + foundState] || 'switching ' + foundState.toLowerCase()
msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
}
// Create/fill/append msg span
const msgSpan = document.createElement('span')
msgSpan.innerHTML = msg ; alertP.append(msgSpan)
// Activate toggle link if necessary
msgSpan.querySelector('[href="#"]')?.addEventListener('click', toggle.proxyMode)
})
appDiv.append(alertP)
}
// Define MODAL functions
const modals = {
clickHandler(event) { // to dismiss modals
log.caller = 'modals.clickHandler()'
if (event.target == event.currentTarget || event.target instanceof SVGPathElement) {
const targetModal = document.querySelector('[class$="-modal"]')
log.debug(`Dismiss element of div#${targetModal?.id} clicked`)
modals.hide(targetModal)
}
},
dragHandlers: {
mousedown(event) { // find modal, attach listeners, init XY offsets
if (event.button != 0) return // prevent non-left-click drag
if (getComputedStyle(event.target).cursor == 'pointer') return // prevent drag when clicking on interactive elems
log.caller = 'modals.dragHandlers.mousedown()'
modals.dragHandlers.draggableElem = event.currentTarget
log.debug(`Mouse down on div#${modals.dragHandlers.draggableElem?.id}`)
event.preventDefault(); // prevent sub-elems like icons being draggable
['mousemove', 'mouseup'].forEach(event => document.addEventListener(event, modals.dragHandlers[event]))
const draggableElemRect = modals.dragHandlers.draggableElem.getBoundingClientRect(),
targetModalIsSettings = event.currentTarget.closest('[id*="-settings"]')
modals.dragHandlers.offsetX = event.clientX - draggableElemRect.left + ( targetModalIsSettings ? 0 : 21 )
modals.dragHandlers.offsetY = event.clientY - draggableElemRect.top + ( targetModalIsSettings ? 0 : 12 )
},
mousemove(event) { // drag modal
if (modals.dragHandlers.draggableElem) {
const newX = event.clientX - modals.dragHandlers.offsetX,
newY = event.clientY - modals.dragHandlers.offsetY
modals.dragHandlers.draggableElem.style.left = `${newX}px`
modals.dragHandlers.draggableElem.style.top = `${newY}px`
}
},
mouseup() { // remove listeners, reset modals.dragHandlerss.draggableElem
log.caller = 'modals.dragHandlers.mouseup()'
log.debug(`Mouse up on div#${modals.dragHandlers.draggableElem?.id}`);
['mousemove', 'mouseup'].forEach(event => document.removeEventListener(event, modals.dragHandlers[event]))
modals.dragHandlers.draggableElem = null
}
},
hide(modal) {
log.caller = 'modals.hide()'
log.debug(`Dismissing div#${modal?.id}...`)
const modalContainer = modal?.parentNode
if (!modalContainer) return
modalContainer.style.animation = 'modal-zoom-fade-out .135s ease-out'
setTimeout(() => { modalContainer.remove() ; log.debug(`Success! div#${modal?.id} dismissed`)
}, 105) // delay for fade-out
},
init(modal) {
// Add classes
modal.classList.add('ddgpt-modal')
modal.parentNode.classList.add('ddgpt-modal-bg', 'no-user-select')
// Add listeners
modal.onwheel = modal.ontouchmove = event => event.preventDefault() // disable wheel/swipe scrolling
modal.onmousedown = modals.dragHandlers.mousedown
fillStarryBG(modal) // add stars
setTimeout(() => { // dim bg
modal.parentNode.style.backgroundColor = `rgba(67, 70, 72, ${ scheme === 'dark' ? 0.62 : 0.33 })`
modal.parentNode.classList.add('animated')
}, 100) // delay for transition fx
// Glowup btns
if (scheme == 'dark' && !config.fgAnimationsDisabled) toggle.btnGlow()
},
keyHandler(event) { // to dismiss modals
log.caller = 'modals.keyHandler()'
if (['Escape', 'Esc'].includes(event.key) || event.keyCode == 27) {
log.debug('Escape pressed')
const modal = document.querySelector('[class$="-modal"]')
if (modal) modals.hide(modal)
}
},
about: {
show() {
log.caller = 'modals.about.show()'
// Hide Settings modal if visible
const settingsModal = modals.settings.get()
if (settingsModal) {
log.debug('Hiding visible Settings modal...')
modals.hide(settingsModal) ; log.caller = 'modals.about.show()'
}
log.debug('Showing About modal...')
// Create/init modal
const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
const aboutModalID = chatgpt.alert('',
'🏷️ ' + ( msgs.about_version || 'Version' ) + ': <span class="about-em">' + GM_info.script.version + '</span>\n'
+ '⚡ ' + ( msgs.about_poweredBy || 'Powered by' ) + ': '
+ `<a href="${app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`
+ ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
+ '📜 ' + ( msgs.about_sourceCode || 'Source code' )
+ `: <a href="${app.urls.gitHub}" target="_blank" rel="nopener">`
+ app.urls.gitHub + '</a>',
[ // buttons
function checkForUpdates() { updateCheck() },
function getSupport() { safeWindowOpen(app.urls.support) },
function leaveAReview() { modals.feedback.show({ sites: 'review' }) },
function moreChatGPTapps() { safeWindowOpen(app.urls.relatedApps) }
], '', 577) // modal width
const aboutModal = document.getElementById(aboutModalID).firstChild
// Add logo
const aboutHeaderLogo = logos.ddgpt.create() ; aboutHeaderLogo.width = 420
aboutHeaderLogo.style.cssText = `max-width: 98% ; margin: -1px ${ browser.isMobile ? 'auto' : '13.5%' } 1px`
aboutModal.insertBefore(aboutHeaderLogo, aboutModal.firstChild.nextSibling) // after close btn
// Center text
aboutModal.removeChild(aboutModal.querySelector('h2')) // remove empty title h2
aboutModal.querySelector('p').style.cssText = 'justify-self: center ; text-align: center ; overflow-wrap: anywhere ;'
+ `margin: ${ browser.isPortrait ? '9px 0 -16px' : '3px 0 -6px' }`
// Resize/format buttons to include emoji + localized label + hide Dismiss button
aboutModal.querySelectorAll('button').forEach(btn => {
btn.style.cssText = 'height: 52px ; min-width: 136px'
// Emojize/localize label
if (/updates/i.test(btn.textContent)) btn.textContent = (
'🚀 ' + ( msgs.btnLabel_updateCheck || 'Check for Updates' ))
else if (/support/i.test(btn.textContent)) btn.textContent = (
'🧠 ' + ( msgs.btnLabel_getSupport || 'Get Support' ))
else if (/review/i.test(btn.textContent)) btn.textContent = (
'⭐ ' + ( msgs.btnLabel_leaveReview || 'Leave Review' ))
else if (/apps/i.test(btn.textContent)) btn.textContent = (
'🤖 ' + ( msgs.btnLabel_moreApps || 'More ChatGPT Apps' ))
else btn.style.display = 'none' // hide Dismiss button
})
modals.init(aboutModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
log.debug('Success! About Modal shown')
}
},
feedback: {
show(options) {
log.caller = `modals.feedback.show(${ options ? `'${options}'` : '' })`
log.debug('Showing Feedback modal...')
// Init buttons
let btns = [
function greasyFork() { safeWindowOpen(app.urls.greasyFork + '/feedback#post-discussion') },
function productHunt() { safeWindowOpen(app.urls.productHunt + '/reviews/new') },
function futurepedia() { safeWindowOpen(app.urls.futurepedia + '#tool-reviews') },
function alternativeTo() { safeWindowOpen(app.urls.alternativeTo + '/about/') }
]
if (options.sites == 'feedback') btns.splice(1, 0,
function github() { safeWindowOpen(app.urls.gitHub + '/discussions/new/choose') })
// Create/init modal
const feedbackModalID = siteAlert(`${
msgs.alert_choosePlatform || 'Choose a platform' }:`, '', btns, '', 408)
const feedbackModal = document.getElementById(feedbackModalID).firstChild
// 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-top: -2px !important' // close gap between title/btns
// Format button labels + add v-padding
btns = btnsDiv.querySelectorAll('button')
btns.forEach((btn, idx) => {
if (idx == 0) btn.style.display = 'none' // hide Dismiss button
else if (btn.textContent == 'Github') btn.textContent = 'GitHub'
else if (btn.textContent == 'Alternative To') btn.textContent = 'AlternativeTo'
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
})
modals.init(feedbackModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
log.debug('Success! Feedback modal shown')
}
},
scheme: {
show() {
log.caller = 'modals.scheme.show()'
log.debug('Showing Scheme modal...')
// Create/init modal
const schemeModalID = siteAlert(`${
app.name } ${( msgs.menuLabel_colorScheme || 'Color Scheme' ).toLowerCase() }:`, '',
[ function auto() {}, function light() {}, function dark() {} ]) // buttons
const schemeModal = document.getElementById(schemeModalID).firstChild
// Center title/button cluster
schemeModal.querySelector('h2').style.justifySelf = 'center'
schemeModal.querySelector('.modal-buttons').style.cssText = 'justify-content: center ; margin-top: -2px !important'
// Re-format each button
const buttons = schemeModal.querySelectorAll('button'),
schemes = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'}
for (const btn of buttons) {
const btnScheme = btn.textContent.toLowerCase()
// Emphasize active scheme
btn.classList = (
config.scheme == btn.textContent.toLowerCase() || (btn.textContent == 'Auto' && !config.scheme)
? 'primary-modal-btn' : '' )
// Prepend emoji + localize labels
if (Object.prototype.hasOwnProperty.call(schemes, btnScheme))
btn.textContent = `${schemes[btnScheme]} ${ // emoji
msgs['scheme_' + btnScheme] || msgs['menuLabel_' + btnScheme] || btnScheme.toUpperCase() }`
else btn.style.display = 'none' // hide Dismiss button
// Clone button to replace listener to not dismiss modal on click
const newBtn = btn.cloneNode(true) ; btn.parentNode.replaceChild(newBtn, btn)
newBtn.onclick = event => {
event.stopPropagation() // disable chatgpt.js dismissAlert()
const newScheme = btnScheme == 'auto' ? ( chatgpt.isDarkMode() ? 'dark' : 'light' ) : btnScheme
saveSetting('scheme', btnScheme == 'auto' ? false : newScheme)
schemeModal.querySelectorAll('button').forEach(btn => btn.classList = '') // clear prev emphasized active scheme
newBtn.classList = 'primary-modal-btn' // emphasize newly active scheme
newBtn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis
setTimeout(() => { newBtn.style.pointerEvents = 'auto'; }, 100) // re-enable hover fx after 100ms to flicker emphasis
update.scheme(newScheme) ; schemeNotify(btnScheme)
}
}
modals.init(schemeModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
log.debug('Success! Scheme modal shown')
function schemeNotify(scheme) {
// Show notification
notify(` ${ msgs.menuLabel_colorScheme || 'Color Scheme' }: `
+ ( scheme == 'light' ? msgs.scheme_light || 'Light' :
scheme == 'dark' ? msgs.scheme_dark || 'Dark'
: msgs.menuLabel_auto || 'Auto' ).toUpperCase() )
const notifs = document.querySelectorAll('.chatgpt-notif'),
notif = notifs[notifs.length -1]
// Append scheme icon
const schemeIcon = icons[scheme == 'light' ? 'sun' : scheme == 'dark' ? 'moon' : 'arrowsCycle'].create()
schemeIcon.style.cssText = 'width: 23px ; height: 23px ; position: relative ; top: 3px ; margin-left: 6px'
notif.append(schemeIcon)
}
}
},
settings: {
createAppend() {
log.caller = 'modals.settings.createAppend()'
// Init master elems
const settingsContainer = document.createElement('div'),
settingsModal = document.createElement('div') ; settingsModal.id = 'ddgpt-settings'
settingsContainer.append(settingsModal)
modals.init(settingsModal) // add classes/stars, disable wheel-scrolling, dim bg
// Init settings keys
log.debug('Initializing settings keys...')
const settingsKeys = Object.keys(settingsProps).filter(key => !(browser.isMobile && settingsProps[key].mobile == false)
&& !(isCentered && settingsProps[key].centered == false))
log.debug(`Success! settingsKeys = ${log.prettifyObj(settingsKeys)}`)
// Init logo
const settingsIcon = icons.ddgpt.create()
settingsIcon.style.cssText = 'width: 65px ; position: relative ; top: -32px ; margin-bottom: 4px ;'
+ 'filter: drop-shadow(5px 5px 15px rgba(0, 0, 0, 0.3))'
// Init title
const settingsTitleDiv = document.createElement('div') ; settingsTitleDiv.id = 'ddgpt-settings-title'
const settingsTitleH4 = document.createElement('h4') ; settingsTitleH4.textContent = msgs.menuLabel_settings || 'Settings'
const settingsTitleIcon = icons.sliders.create()
settingsTitleIcon.style.cssText = 'width: 21px ; height: 21px ; margin-right: 3px ; position: relative ; top: 2.5px ; right: 3px'
settingsTitleH4.prepend(settingsTitleIcon) ; settingsTitleDiv.append(settingsTitleH4)
// Init settings lists
log.debug('Initializing settings lists...')
const settingsLists = [], middleGap = 30, // px
settingsListContainer = document.createElement('div'),
settingsListCnt = ( browser.isMobile && ( browser.isPortrait || settingsKeys.length < 8 )) ? 1 : 2,
settingItemCap = Math.floor(settingsKeys.length /2)
for (let i = 0 ; i < settingsListCnt ; i++) settingsLists.push(document.createElement('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 ; border-right: 1px dotted ${ scheme == 'dark' ? 'white' : 'black '}` )
}
log.debug(`Success! settingsListCnt = ${settingsListCnt}`)
// Create/append setting icons/labels/toggles
settingsKeys.forEach((key, idx) => {
const setting = settingsProps[key]
// Create/append item/label elems
const settingItem = document.createElement('li') ; settingItem.id = key + '-menu-entry'
settingItem.title = setting.helptip || '' // for hover assistance
const settingLabel = document.createElement('label') ; settingLabel.textContent = setting.label
settingItem.append(settingLabel) ; (settingsLists[browser.isPortrait ? 0 : +(idx >= settingItemCap)]).append(settingItem)
// Create/prepend icons
const settingIcon = icons[setting.icon].create(/bg|fg/.exec(key)?.[0] ?? '')
settingIcon.style.cssText = 'position: relative ;' + (
/proxy/i.test(key) ? 'top: 3px ; left: -0.5px ; margin-right: 9px'
: /streaming/i.test(key) ? 'top: 3px ; left: 0.5px ; margin-right: 9px'
: /auto(?:get|focus)/i.test(key) ? 'top: 4.5px ; margin-right: 7px'
: /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' : ''
)
settingItem.prepend(settingIcon)
// Create/append toggles/listeners
if (setting.type == 'toggle') {
// Init toggle input
const settingToggle = document.createElement('input'),
settingToggleAttrs = [['type', 'checkbox'], ['disabled', true]]
settingToggleAttrs.forEach(([attr, value]) => settingToggle.setAttribute(attr, value))
settingToggle.checked = config[key] ^ key.includes('Disabled') // init based on config/name
&& !(key == 'streamingDisabled' && !config.proxyAPIenabled) // uncheck Streaming in OpenAI mode
settingToggle.style.display = 'none' // hide checkbox
// Create/stylize switch
const switchSpan = document.createElement('span')
const switchStyles = {
position: 'relative', left: '-1px', bottom:'-5.5px', float: 'right',
backgroundColor: settingToggle.checked ? '#ccc' : '#AD68FF', // init opposite final color
width: '26px', height: '13px', '-webkit-transition': '.4s', transition: '0.4s', borderRadius: '28px'
}
Object.assign(switchSpan.style, switchStyles)
// Create/stylize knob
const knobSpan = document.createElement('span')
const knobWidth = 11
const knobStyles = {
position: 'absolute', left: '1px', bottom: '1px',
width: `${ knobWidth }px`, height: `${ knobWidth }px`, content: '""', borderRadius: '28px',
transform: settingToggle.checked ? // init opposite final pos
'translateX(0)' : 'translateX(14px) translateY(0)',
backgroundColor: 'white', '-webkit-transition': '0.2s', transition: '0.2s'
}
Object.assign(knobSpan.style, knobStyles)
// Append elems
switchSpan.append(knobSpan) ; settingItem.append(settingToggle, switchSpan)
// Update visual state w/ animation
setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), idx *25 -25)
// Add click listener
settingItem.onclick = () => {
if (!(key == 'streamingDisabled' // visually switch toggle if not Streaminng...
&& (!streamingSupported.browser || !streamingSupported.userscriptManager // ...in unsupported env...
|| !config.proxyAPIenabled) // ...or in OpenAI mode
)) modals.settings.toggle.switch(settingToggle)
// Call specialized toggle funcs
const manualGetMatch = /(?:suf|pre)fix/.exec(key)
if (key.includes('proxy')) toggle.proxyMode()
else if (key.includes('streaming')) toggle.streaming()
else if (/autoget/i.test(key)) toggle.autoGet()
else if (key.includes('rq')) toggle.relatedQueries()
else if (manualGetMatch) toggle.manualGet(manualGetMatch[0])
else if (key.includes('Sidebar')) toggle.sidebar(/(.*?)Sidebar$/.exec(key)[1])
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 {
log.caller = 'settings.createAppend()'
log.debug(`Toggling ${settingItem.textContent} ${ key.includes('Disabled') ^ config[key] ? 'OFF' : 'ON' }...`)
saveSetting(key, !config[key]) // update config
notify(`${settingsProps[key].label} ${menuState.word[+key.includes('Disabled') ^ +config[key]]}`)
log[key.includes('debug') ? 'info' : 'debug'](`Success! config.${key} = ${config[key]}`)
}
}
// Add .active + config status + listeners to pop-up settings
} else {
settingItem.classList.add('active')
const configStatusSpan = document.createElement('span')
configStatusSpan.style.cssText = 'float: right ; font-size: 11px ; margin-top: 3px ;'
+ ( !key.includes('about') ? 'text-transform: uppercase !important' : '' )
if (key.includes('replyLang')) {
configStatusSpan.textContent = config.replyLanguage
settingItem.onclick = promptReplyLang
} else if (key.includes('scheme')) {
modals.settings.updateSchemeStatus(configStatusSpan)
settingItem.onclick = modals.scheme.show
} else if (key.includes('about')) {
const innerDiv = document.createElement('div'),
textGap = '     '
modals.settings.aboutContent = {}
modals.settings.aboutContent.short = `v${ GM_info.script.version}`
modals.settings.aboutContent.long = (
`${ msgs.about_version || 'Version' }: <span class="about-em">v${ GM_info.script.version + textGap }</span>`
+ `${ msgs.about_poweredBy || 'Powered by' } <span class="about-em">chatgpt.js</span>${textGap}` )
for (let i = 0; i < 7; i++) modals.settings.aboutContent.long += modals.settings.aboutContent.long // make it long af
innerDiv.innerHTML = modals.settings.aboutContent[config.fgAnimationsDisabled ? 'short' : 'long']
innerDiv.style.float = config.fgAnimationsDisabled ? 'right' : ''
configStatusSpan.append(innerDiv) ; settingItem.onclick = modals.about.show
} settingItem.append(configStatusSpan)
}
})
settingsListContainer.append(...settingsLists)
// Create close button
const closeBtn = document.createElement('div')
closeBtn.classList.add('ddgpt-modal-close-btn', 'no-mobile-tap-outline')
closeBtn.title = msgs.tooltip_close || 'Close'
const closeSVG = icons.x.create() ; closeBtn.append(closeSVG)
// Assemble/append elems
settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsListContainer)
document.body.append(settingsContainer)
// Add listeners to dismiss modal
const dismissElems = [settingsContainer, closeBtn, closeSVG]
dismissElems.forEach(elem => elem.onclick = modals.clickHandler)
return settingsContainer
},
get() { return document.getElementById('ddgpt-settings') },
show() {
log.caller = 'modals.settings.show()'
log.debug('Showing Settings modal...')
const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
settingsContainer.style.display = '' // show modal
log.caller = 'modals.settings.show()'
if (browser.isMobile) { // scale 93% to viewport sides
log.debug('Scaling 93% to viewport sides...')
const settingsModal = settingsContainer.querySelector('#ddgpt-settings'),
scaleRatio = 0.93 * window.innerWidth / settingsModal.offsetWidth
settingsModal.style.transform = `scale(${scaleRatio})`
}
log.debug('Success! Settings modal shown')
},
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')
setTimeout(() => {
switchSpan.style.backgroundColor = settingToggle.checked ? '#ad68ff' : '#ccc'
switchSpan.style.boxShadow = settingToggle.checked ? '2px 1px 9px #d8a9ff' : 'none'
knobSpan.style.transform = settingToggle.checked ? 'translateX(14px) translateY(0)' : 'translateX(0)'
settingLi.classList[settingToggle.checked ? 'add' : 'remove']('active') // dim/brighten setting entry
}, 1) // min delay to trigger transition fx
}
},
updateSchemeStatus(schemeStatusSpan = null) {
schemeStatusSpan = schemeStatusSpan || document.querySelector('#scheme-menu-entry span')
if (schemeStatusSpan) {
while (schemeStatusSpan.firstChild) schemeStatusSpan.removeChild(schemeStatusSpan.firstChild) // clear old status
schemeStatusSpan.append(...( // status txt + icon
config.scheme == 'dark' ? [document.createTextNode(msgs.scheme_dark || 'Dark'), icons.moon.create()]
: config.scheme == 'light' ? [document.createTextNode(msgs.scheme_light || 'Light'), icons.sun.create()]
: [document.createTextNode(msgs.menuLabel_auto || 'Auto'), icons.arrowsCycle.create()] ))
schemeStatusSpan.style.cssText += `; margin-top: ${ !config.scheme ? 3 : 0 }px !important`
}
}
}
}
// Define MENU functions
const menus = {
fadeInDelay: 5, // ms
show(menu) {
menu.style.display = ''
setTimeout(() => menu.classList.add('active'), menus.fadeInDelay)
},
pin: {
clickHandler(event) {
const pinMenu = event.target.closest('#pin-menu'),
itemLabel = event.target.textContent,
prevOffsetTop = appDiv.offsetTop
// Switch mode
if ([msgs.menuLabel_top, 'Top'].includes(itemLabel)) toggle.sidebar('sticky')
else if ([msgs.menuLabel_sidebar, 'Sidebar'].includes(itemLabel)) {
toggle.sidebar('sticky', 'off') ; toggle.anchorMode('off') }
else if ([msgs.menuLabel_bottom, 'Bottom'].includes(itemLabel)) toggle.anchorMode()
// Close/update menu
if (appDiv.offsetTop != prevOffsetTop) pinMenu.remove() // since app moved
else menus.pin.update(pinMenu) // since menu stayed in place
},
createAppend() {
const pinMenu = document.createElement('div') ; pinMenu.id = 'pin-menu'
pinMenu.classList.add('ddgpt-menu', 'btn-tooltip', 'fade-in-less', 'no-user-select')
menus.pin.update(pinMenu) ; appDiv.append(pinMenu)
return pinMenu
},
update(pinMenu) {
while (pinMenu.firstChild) pinMenu.removeChild(pinMenu.firstChild) // clear content
// Init core elems
const pinMenuUL = document.querySelector('#pin-menu ul') || document.createElement('ul'),
pinMenuItems = []
const pinMenulabels = [
`${ msgs.tooltip_pinTo || 'Pin to' }...`, msgs.menuLabel_top || 'Top',
msgs.menuLabel_sidebar || 'Sidebar', msgs.menuLabel_bottom || 'Bottom' ]
const pinMenuIcons = [icons.webCorner.create(), icons.sidebar.create(), icons.anchor.create(), icons.checkmark.create()]
// Style icons
pinMenuIcons.forEach(icon => icon.style.cssText = (
'width: 12px ; height: 12px ; position: relative ; top: 1px ; right: 5px ; margin-left: 5px'))
pinMenuIcons[0].style.width = pinMenuIcons[0].style.height = '11px' // shrink corner web icon
pinMenuIcons[3].style.cssText = 'position: relative ; float: right ; margin-right: -16px ; top: 4px' // re-style checkmarks
// Fill menu UL
for (let i = 0 ; i < 4 ; i++) {
pinMenuItems.push(document.createElement('li'))
pinMenuItems[i].textContent = pinMenulabels[i]
pinMenuItems[i].className = 'ddgpt-menu-item'
if (i == 0) { // format header item
pinMenuItems[i].innerHTML = `<b>${pinMenulabels[i]}</b>`
pinMenuItems[i].classList.add('ddgpt-menu-header') // to not apply hover fx from appStyle
pinMenuItems[i].style.cssText = 'margin-bottom: 1px ; border-bottom: 1px dotted white'
} else if (i == 1) pinMenuItems[i].style.marginTop = '3px' // top-pad first non-header item
pinMenuItems[i].style.paddingRight = '24px' // make room for checkmark
pinMenuItems[i].prepend(i > 0 ? pinMenuIcons[i -1] : '') // prepend left icon
if (i == 1 && config.stickySidebar // 'Top' item + Sticky mode on
|| i == 2 && !config.stickySidebar && !config.anchored // 'Sidebar' item + no mode on
|| i == 3 && config.anchored) // 'Bottom' item + Anchor mode on
pinMenuItems[i].append(pinMenuIcons[pinMenuIcons.length -1]) // append right checkmark
pinMenuItems[i].onclick = menus.pin.clickHandler
pinMenuUL.append(pinMenuItems[i])
}
pinMenu.append(pinMenuUL)
// Add listeners to make visibility stick when mousing from pinSVG
pinMenu.onmouseover = menus.pin.toggle
pinMenu.onmouseout = pinMenu.remove // instead of toggle so re-mouseover doesn't show ghost
},
toggle(event) { // visibility
const pinMenu = document.getElementById('pin-menu') || menus.pin.createAppend()
if (!menus.pin.topPos)
menus.pin.topPos = ( event.clientY || event.touches?.[0]?.clientY ) < 195 ? 53 : -85
if (!menus.pin.rightPos)
menus.pin.rightPos = appDiv.getBoundingClientRect().right - event.clientX - pinMenu.offsetWidth/2
pinMenu.style.top = `${menus.pin.topPos}px` ; pinMenu.style.right = `${menus.pin.rightPos}px`
pinMenu.style.opacity = (
event.type == 'mouseover' ? 1 : event.type == 'mouseout' ? 0 : +!parseInt(pinMenu.style.opacity, 10) )
}
}
}
// Define ICON functions
const icons = {
anchor: {
create() {
const anchorSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
anchorSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 24 24']]
anchorSVGattrs.forEach(([attr, value]) => anchorSVG.setAttribute(attr, value))
anchorSVG.append(createSVGelem('path', { stroke: 'none',
d: 'M12,2 C13.6568542,2 15,3.34314575 15,5 C15,6.30588222 14.1656226,7.41688515 13.0009007,7.82897577 L13.0008722,19.9379974 C15.8984799,19.5763478 18.3147266,17.665053 19.3940412,15.0596838 L19.417,15 L17,15 C15.9853611,15 15.6358608,13.6848035 16.4495309,13.1641077 L16.5527864,13.1055728 L20.5527864,11.1055728 C21.2176875,10.7731223 22,11.256618 22,12 C22,17.5228475 17.5228475,22 12,22 C6.4771525,22 2,17.5228475 2,12 C2,11.2957433 2.70213089,10.8247365 3.34138467,11.0597803 L3.4472136,11.1055728 L7.4472136,13.1055728 C8.35473419,13.5593331 8.07916306,14.8919819 7.11853213,14.9938221 L7,15 L4.582,15 L4.60595876,15.0596838 C5.68539551,17.6653477 8.10206662,19.5767802 11.0001109,19.9381201 L11.0000889,7.82932572 C9.8348501,7.41751442 9,6.30625206 9,5 C9,3.34314575 10.3431458,2 12,2 Z M12,4 C11.4477153,4 11,4.44771525 11,5 C11,5.55228475 11.4477153,6 12,6 C12.5522847,6 13,5.55228475 13,5 C13,4.44771525 12.5522847,4 12,4 Z' }
))
return anchorSVG
}
},
arrowDownRight: {
create() {
const arrowSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowSVGattrs = [['viewBox', '0 0 24 24'], ['width', 18], ['height', 18], ['fill', 'currentColor'], ['style', 'transform: rotate(180deg)']]
arrowSVGattrs.forEach(([attr, value]) => arrowSVG.setAttribute(attr, value))
arrowSVG.append(createSVGelem('path', {
d: 'M16 10H6.83L9 7.83l1.41-1.41L9 5l-6 6 6 6 1.41-1.41L9 14.17 6.83 12H16c1.65 0 3 1.35 3 3v4h2v-4c0-2.76-2.24-5-5-5z' }
))
return arrowSVG
}
},
arrowsCycle: {
create() {
const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowsSVGattrs = [['id', 'arrows-cycle'], ['width', 13], ['height', 13], ['viewBox', '197 -924 573 891']]
arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
arrowsSVG.append(createSVGelem('path', { stroke: 'none', d: 'M204-318q-22-38-33-78t-11-82q0-134 93-228t227-94h7l-64-64 56-56 160 160-160 160-56-56 64-64h-7q-100 0-170 70.5T240-478q0 26 6 51t18 49l-60 60ZM481-40 321-200l160-160 56 56-64 64h7q100 0 170-70.5T720-482q0-26-6-51t-18-49l60-60q22 38 33 78t11 82q0 134-93 228t-227 94h-7l64 64-56 56Z' }))
return arrowsSVG
}
},
arrowsDiagonal: {
inwardSVGpath() { return createSVGelem('path', { stroke: 'none',
d: 'M5 1h2v6H1V5h2.59L0 1.41 1.41 0 5 3.59zm7.41 10H15V9H9v6h2v-2.59L14.59 16 16 14.59z'
})},
outwardSVGpath() { return createSVGelem('path', { stroke: 'none',
d: 'M8 6.59L6.59 8 3 4.41V7H1V1h6v2H4.41zM13 9v2.59L9.41 8 8 9.41 11.59 13H9v2h6V9z'
})},
create() {
const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowsSVGattrs = [['id', 'arrows-diagonal-icon'], ['width', 16], ['height', 16], ['viewBox', '0 0 16 16']]
arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
icons.arrowsDiagonal.update(arrowsSVG)
return arrowsSVG
},
update(...targetIcons) {
targetIcons = targetIcons.flat() // flatten array args nested by spread operator
if (targetIcons.length == 0) targetIcons = document.querySelectorAll('#arrows-diagonal-icon')
targetIcons.forEach(icon => {
icon.firstChild?.remove() // clear prev paths
icon.append(icons.arrowsDiagonal[config.expanded ? 'inwardSVGpath' : 'outwardSVGpath']())
})
}
},
arrowsDown: {
create() {
const arrowsDownSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowsDownSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 24 24']]
arrowsDownSVGattrs.forEach(([attr, value]) => arrowsDownSVG.setAttribute(attr, value))
arrowsDownSVG.append(
createSVGelem('path', { stroke: 'none', d: 'M18,13H6a1,1,0,0,1,0-2H18a1,1,0,0,1,0,2Z' }),
createSVGelem('path', { stroke: 'none', d: 'M14.71,18.29a1,1,0,0,1,0,1.42l-2,2a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42l.29.3V16a1,1,0,0,1,2,0v2.59l.29-.3A1,1,0,0,1,14.71,18.29ZM11.29,8.71a1,1,0,0,0,1.42,0l2-2a1,1,0,1,0-1.42-1.42l-.29.3V3a1,1,0,0,0-2,0V5.59l-.29-.3A1,1,0,0,0,9.29,6.71Z' })
)
return arrowsDownSVG
}
},
arrowsTwistedRight: {
create() {
const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowsSVGattrs = [['width', 21], ['height', 21], ['viewBox', '-1 -1 32 32']]
arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
arrowsSVG.append(createSVGelem('path', { stroke: '', d: 'M23.707,16.293L28.414,21l-4.707,4.707l-1.414-1.414L24.586,22H23c-2.345,0-4.496-1.702-6.702-3.753c0.498-0.458,0.984-0.92,1.46-1.374C19.624,18.6,21.393,20,23,20h1.586l-2.293-2.293L23.707,16.293zM23,11h1.586l-2.293,2.293l1.414,1.414L28.414,10l-4.707-4.707l-1.414,1.414L24.586,9H23c-2.787,0-5.299,2.397-7.957,4.936C12.434,16.425,9.736,19,7,19H4v2h3c3.537,0,6.529-2.856,9.424-5.618C18.784,13.129,21.015,11,23,11zM11.843,14.186c0.5-0.449,0.995-0.914,1.481-1.377C11.364,11.208,9.297,10,7,10H4v2h3C8.632,12,10.25,12.919,11.843,14.186z' }))
return arrowsSVG
}
},
arrowUp: {
create() {
const arrowUpSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
arrowUpSVGattrs = [['width', 16], ['height', 16], ['viewBox', '4 2 16 16'],
['stroke-width', '2'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round']]
arrowUpSVGattrs.forEach(([attr, value]) => arrowUpSVG.setAttribute(attr, value))
arrowUpSVG.append(createSVGelem('path', { stroke: '', fill: 'none', 'stroke-width': '2', linecap: 'round', 'stroke-linejoin': 'round',
d: 'M7 11L12 6L17 11M12 18V7' }))
return arrowUpSVG
}
},
bug: {
create() {
const bugSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
bugSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 0 16 16']]
bugSVGattrs.forEach(([attr, value]) => bugSVG.setAttribute(attr, value))
bugSVG.append(
createSVGelem('path', {
d: 'M7 0V1.60002C7.32311 1.53443 7.65753 1.5 8 1.5C8.34247 1.5 8.67689 1.53443 9 1.60002V0H11V2.49963C11.8265 3.12041 12.4543 3.99134 12.7711 5H3.2289C3.5457 3.99134 4.17354 3.12041 5 2.49963V0H7Z' }),
createSVGelem('path', {
d: 'M0 7V9H3V10.4957L0.225279 11.2885L0.774721 13.2115L3.23189 12.5095C3.87194 14.5331 5.76467 16 8 16C10.2353 16 12.1281 14.5331 12.7681 12.5095L15.2253 13.2115L15.7747 11.2885L13 10.4957V9H16V7H9V12H7V7H0Z' })
)
return bugSVG
}
},
caretsInward: {
create() {
const caretsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
caretsSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
caretsSVGattrs.forEach(([attr, value]) => caretsSVG.setAttribute(attr, value))
caretsSVG.append(createSVGelem('path', { stroke: '', d: 'M11.29,9.71a1,1,0,0,0,1.42,0l5-5a1,1,0,1,0-1.42-1.42L12,7.59,7.71,3.29A1,1,0,0,0,6.29,4.71Zm1.42,4.58a1,1,0,0,0-1.42,0l-5,5a1,1,0,0,0,1.42,1.42L12,16.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z' }))
return caretsSVG
}
},
checkmark: {
create() {
const checkmarkSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
checkmarkSVGattrs = [['id', 'checkmark-icon'], ['width', 10], ['height', 10], ['viewBox', '0 0 20 20']]
checkmarkSVGattrs.forEach(([attr, value]) => checkmarkSVG.setAttribute(attr, value))
checkmarkSVG.append(createSVGelem('path', { stroke: 'none', d: 'M0 11l2-2 5 5L18 3l2 2L7 18z' }))
return checkmarkSVG
}
},
checkmarkDouble: {
create() {
const checkmarksSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
checkmarksSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
checkmarksSVGattrs.forEach(([attr, value]) => checkmarksSVG.setAttribute(attr, value))
checkmarksSVG.append(
createSVGelem('path', { stroke: 'none', d: 'M23.228 8.01785C23.6186 7.62741 23.6187 6.99424 23.2283 6.60363L22.5213 5.89638C22.1309 5.50577 21.4977 5.50563 21.1071 5.89607L10.0862 16.9122C9.69563 17.3027 9.6955 17.9359 10.0859 18.3265L10.7929 19.0337C11.1833 19.4243 11.8165 19.4245 12.2071 19.034L23.228 8.01785Z' }),
createSVGelem('path', { stroke: 'none', d: 'M17.2285 8.01777C17.619 7.62724 17.619 6.99408 17.2285 6.60356L16.5214 5.89645C16.1309 5.50592 15.4977 5.50592 15.1072 5.89645L5.54542 15.4582L2.76773 12.6805C2.37721 12.29 1.74404 12.29 1.35352 12.6805L0.646409 13.3876C0.255884 13.7782 0.255885 14.4113 0.646409 14.8019L4.83831 18.9938C5.22883 19.3843 5.862 19.3843 6.25252 18.9938L17.2285 8.01777Z' })
)
return checkmarksSVG
}
},
chevronDown: {
create() {
const chevronSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
chevronSVGattrs = [['width', 20], ['height', 20], ['viewBox', '0 0 16 16']]
chevronSVGattrs.forEach(([attr, value]) => chevronSVG.setAttribute(attr, value))
chevronSVG.append(createSVGelem('path', { stroke: 'none', d: 'M1 5l7 4.61L15 5v2.39L8 12 1 7.39z' }))
return chevronSVG
}
},
chevronUp: {
create() {
const chevronSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
chevronSVGattrs = [['width', 20], ['height', 20], ['viewBox', '0 0 16 16']]
chevronSVGattrs.forEach(([attr, value]) => chevronSVG.setAttribute(attr, value))
chevronSVG.append(createSVGelem('path', { stroke: 'none', d: 'M15 11L8 6.39 1 11V8.61L8 4l7 4.61z' }))
return chevronSVG
}
},
copy: {
create() {
const copySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
copySVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 1024 1024']],
copySVGtitle = document.createElementNS('http://www.w3.org/2000/svg', 'title')
copySVGattrs.forEach(([attr, value]) => copySVG.setAttribute(attr, value))
copySVG.append(
copySVGtitle,
createSVGelem('path', { stroke: 'none', d: 'M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64h64z' }),
createSVGelem('path', { stroke: 'none', d: 'M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64H384zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64z' })
)
return copySVG
}
},
ddgpt: {
create() {
const ddgptIcon = document.createElement('img')
ddgptIcon.src = GM_getResourceText('ddgptIcon')
return ddgptIcon
}
},
fontSize: {
create() {
const fontSizeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
fontSizeSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
fontSizeSVGattrs.forEach(([attr, value]) => fontSizeSVG.setAttribute(attr, value))
fontSizeSVG.append(
createSVGelem('path', { stroke: 'none', d: 'M234.997 448.199h-55.373a6.734 6.734 0 0 1-6.556-5.194l-11.435-48.682a6.734 6.734 0 0 0-6.556-5.194H86.063a6.734 6.734 0 0 0-6.556 5.194l-11.435 48.682a6.734 6.734 0 0 1-6.556 5.194H7.74c-4.519 0-7.755-4.363-6.445-8.687l79.173-261.269a6.734 6.734 0 0 1 6.445-4.781h69.29c2.97 0 5.59 1.946 6.447 4.79l78.795 261.269c1.303 4.322-1.933 8.678-6.448 8.678zm-88.044-114.93l-19.983-84.371c-1.639-6.921-11.493-6.905-13.111.02l-19.705 84.371c-.987 4.224 2.22 8.266 6.558 8.266H140.4c4.346 0 7.555-4.056 6.553-8.286z' }),
createSVGelem('path', { stroke: 'none', d: 'M502.572 448.199h-77.475a9.423 9.423 0 0 1-9.173-7.268l-16-68.114a9.423 9.423 0 0 0-9.173-7.268H294.19a9.423 9.423 0 0 0-9.173 7.268l-16 68.114a9.423 9.423 0 0 1-9.173 7.268h-75.241c-6.322 0-10.851-6.104-9.017-12.155L286.362 70.491a9.422 9.422 0 0 1 9.017-6.69h96.947a9.422 9.422 0 0 1 9.021 6.702l110.245 365.554c1.825 6.047-2.703 12.142-9.02 12.142zM379.385 287.395l-27.959-118.047c-2.293-9.683-16.081-9.661-18.344.029l-27.57 118.047c-1.38 5.91 3.106 11.565 9.175 11.565h55.529c6.082-.001 10.571-5.676 9.169-11.594z' })
)
return fontSizeSVG
}
},
languageChars: {
create() {
const languageCharsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
languageCharsSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
languageCharsSVGattrs.forEach(([attr, value]) => languageCharsSVG.setAttribute(attr, value))
languageCharsSVG.append(createSVGelem('path', { stroke: 'none', d: 'm459-48 188-526h125L960-48H847l-35-100H603L568-48H459ZM130-169l-75-75 196-196q-42-45-78-101t-55-105h117q17 32 40.5 67.5T325-514q35-37 70-93t64-119H0v-106h290v-80h106v80h290v106H572q-23 74-70 152T399-438l82 85-39 111-118-121-194 194Zm508-79h139l-69-197-70 197Z' }) )
return languageCharsSVG
}
},
moon: {
create() {
const moonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
moonSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
moonSVGattrs.forEach(([attr, value]) => moonSVG.setAttribute(attr, value))
moonSVG.append(createSVGelem('path', { fill: 'none', stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
d: 'M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z' }))
return moonSVG
}
},
pin: {
create() {
const pinSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
pinSVGattrs = [['id', 'pin-icon'], ['width', 17], ['height', 17], ['viewBox', '0 0 16 16']]
pinSVGattrs.forEach(([attr, value]) => pinSVG.setAttribute(attr, value))
pinSVG.append(createSVGelem('path', { stroke: '', d: 'M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146z' }))
return pinSVG
}
},
questionMark: {
create() {
const questionMarkSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
questionMarkSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 -960 960 960']]
questionMarkSVGattrs.forEach(([attr, value]) => questionMarkSVG.setAttribute(attr, value))
questionMarkSVG.append(createSVGelem('path', { stroke: 'none', d: 'M428-383q0-71 16-111t63-74q47-35 58.5-55.5T577-683q0-35-25-57.5T488-763q-26 0-61 18t-50 70l-114-47q27-82 90.5-122.5T488-885q93 0 151.5 59.5T698-682q0 55-17 95t-70 83q-37 29-48.5 55T550-383H428Zm60 265q-41 0-69.5-28.5T390-216q0-41 28.5-69.5T488-314q41 0 69.5 28.5T586-216q0 41-28.5 69.5T488-118Z' }))
return questionMarkSVG
}
},
questionMarkCircle: {
create() {
const questionMarkSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
questionMarkSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 56.693 56.693']]
questionMarkSVGattrs.forEach(([attr, value]) => questionMarkSVG.setAttribute(attr, value))
questionMarkSVG.append(createSVGelem('path', { stroke: 'none',
d: 'M28.765,4.774c-13.562,0-24.594,11.031-24.594,24.594c0,13.561,11.031,24.594,24.594,24.594 c13.561,0,24.594-11.033,24.594-24.594C53.358,15.805,42.325,4.774,28.765,4.774z M31.765,42.913c0,0.699-0.302,1.334-0.896,1.885 c-0.587,0.545-1.373,0.82-2.337,0.82c-0.993,0-1.812-0.273-2.431-0.814c-0.634-0.551-0.954-1.188-0.954-1.891v-1.209 c0-0.703,0.322-1.34,0.954-1.891c0.619-0.539,1.438-0.812,2.431-0.812c0.964,0,1.75,0.277,2.337,0.82 c0.594,0.551,0.896,1.186,0.896,1.883V42.913z M38.427,24.799c-0.389,0.762-0.886,1.432-1.478,1.994 c-0.581,0.549-1.215,1.044-1.887,1.473c-0.643,0.408-1.248,0.852-1.798,1.315c-0.539,0.455-0.99,0.963-1.343,1.512 c-0.336,0.523-0.507,1.178-0.507,1.943v0.76c0,0.504-0.247,1.031-0.735,1.572c-0.494,0.545-1.155,0.838-1.961,0.871l-0.167,0.004 c-0.818,0-1.484-0.234-1.98-0.699c-0.532-0.496-0.801-1.055-0.801-1.658c0-1.41,0.196-2.611,0.584-3.572 c0.385-0.953,0.86-1.78,1.416-2.459c0.554-0.678,1.178-1.27,1.854-1.762c0.646-0.467,1.242-0.93,1.773-1.371 c0.513-0.428,0.954-0.885,1.312-1.354c0.328-0.435,0.489-0.962,0.489-1.608c0-1.066-0.289-1.83-0.887-2.334 c-0.604-0.512-1.442-0.771-2.487-0.771c-0.696,0-1.294,0.043-1.776,0.129c-0.471,0.083-0.905,0.223-1.294,0.417 c-0.384,0.19-0.745,0.456-1.075,0.786c-0.346,0.346-0.71,0.783-1.084,1.301c-0.336,0.473-0.835,0.83-1.48,1.062 c-0.662,0.239-1.397,0.175-2.164-0.192c-0.689-0.344-1.11-0.793-1.254-1.338c-0.135-0.5-0.135-1.025-0.002-1.557 c0.098-0.453,0.369-1.012,0.83-1.695c0.451-0.67,1.094-1.321,1.912-1.938c0.814-0.614,1.847-1.151,3.064-1.593 c1.227-0.443,2.695-0.668,4.367-0.668c1.648,0,3.078,0.249,4.248,0.742c1.176,0.496,2.137,1.157,2.854,1.967 c0.715,0.809,1.242,1.738,1.568,2.762c0.322,1.014,0.486,2.072,0.486,3.146C39.024,23.075,38.823,24.024,38.427,24.799z' }
))
return questionMarkSVG
}
},
scheme: {
create() {
const schemeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
schemeSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
schemeSVGattrs.forEach(([attr, value]) => schemeSVG.setAttribute(attr, value))
schemeSVG.append(createSVGelem('path', { stroke: 'none', d: 'M479.92-34q-91.56 0-173.4-35.02t-142.16-95.34q-60.32-60.32-95.34-142.24Q34-388.53 34-480.08q0-91.56 35.02-173.4t95.34-142.16q60.32-60.32 142.24-95.34Q388.53-926 480.08-926q91.56 0 173.4 35.02t142.16 95.34q60.32 60.32 95.34 142.24Q926-571.47 926-479.92q0 91.56-35.02 173.4t-95.34 142.16q-60.32 60.32-142.24 95.34Q571.47-34 479.92-34ZM530-174q113-19 186.5-102.78T790-480q0-116.71-73.5-201.35Q643-766 530-785v611Z' }))
return schemeSVG
}
},
sidebar: {
create() {
const sidebarSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sidebarSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -975 900 1000']]
sidebarSVGattrs.forEach(([attr, value]) => sidebarSVG.setAttribute(attr, value))
sidebarSVG.append(createSVGelem('path', { stroke: 'none', d: 'M800-160q33 0 56.5-23.5T880-240v-480q0-33-23.5-56.5T800-800H160q-33 0-56.5 23.5T80-720v480q0 33 23.5 56.5T160-160h640Zm-240-80H160v-480h400v480Zm80 0v-480H800v480H640Zm160 0v-480 480Zm-160 0h-80 80Zm0-480h-80 80Z' }))
return sidebarSVG
}
},
signalStream: {
create() {
const signalStreamSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
signalStreamSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 0 32 32']]
signalStreamSVGattrs.forEach(([attr, value]) => signalStreamSVG.setAttribute(attr, value))
signalStreamSVG.append(createSVGelem('path', { stroke: '', 'stroke-width': 0.5, d: 'M16 11.75c-2.347 0-4.25 1.903-4.25 4.25s1.903 4.25 4.25 4.25c2.347 0 4.25-1.903 4.25-4.25v0c-0.003-2.346-1.904-4.247-4.25-4.25h-0zM16 17.75c-0.966 0-1.75-0.784-1.75-1.75s0.784-1.75 1.75-1.75c0.966 0 1.75 0.784 1.75 1.75v0c-0.001 0.966-0.784 1.749-1.75 1.75h-0zM3.25 16c0.211-3.416 1.61-6.471 3.784-8.789l-0.007 0.008c0.223-0.226 0.361-0.536 0.361-0.879 0-0.69-0.56-1.25-1.25-1.25-0.344 0-0.655 0.139-0.881 0.363l0-0c-2.629 2.757-4.31 6.438-4.506 10.509l-0.001 0.038c0.198 4.109 1.879 7.79 4.514 10.553l-0.006-0.006c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-2.173-2.307-3.573-5.363-3.774-8.743l-0.002-0.038zM9.363 16c0.149-2.342 1.109-4.436 2.6-6.026l-0.005 0.005c0.224-0.226 0.363-0.537 0.363-0.88 0-0.69-0.56-1.25-1.25-1.25-0.345 0-0.657 0.139-0.883 0.365l0-0c-1.94 2.035-3.179 4.753-3.323 7.759l-0.001 0.028c0.145 3.032 1.384 5.75 3.329 7.79l-0.005-0.005c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-1.49-1.581-2.451-3.676-2.591-5.993l-0.001-0.027zM26.744 5.453c-0.226-0.227-0.54-0.368-0.886-0.368-0.691 0-1.251 0.56-1.251 1.251 0 0.345 0.139 0.657 0.365 0.883l-0-0c2.168 2.31 3.567 5.365 3.775 8.741l0.002 0.040c-0.21 3.417-1.609 6.471-3.784 8.789l0.007-0.008c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c2.628-2.757 4.308-6.439 4.504-10.509l0.001-0.038c-0.198-4.108-1.878-7.79-4.512-10.553l0.006 0.007zM21.811 8.214c-0.226-0.224-0.537-0.363-0.881-0.363-0.69 0-1.25 0.56-1.25 1.25 0 0.343 0.138 0.653 0.361 0.879l-0-0c1.486 1.585 2.447 3.678 2.594 5.992l0.001 0.028c-0.151 2.343-1.111 4.436-2.601 6.027l0.005-0.005c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c1.939-2.036 3.178-4.754 3.323-7.759l0.001-0.028c-0.145-3.033-1.385-5.751-3.331-7.791l0.005 0.005z' }))
return signalStreamSVG
}
},
slash: {
create() {
const slashSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
slashSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 0 15 15']]
slashSVGattrs.forEach(([attr, value]) => slashSVG.setAttribute(attr, value))
slashSVG.append(createSVGelem('path', { stroke: '', d: 'M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z' }))
return slashSVG
}
},
sliders: {
create() {
const slidersSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
slidersSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 25 25']]
slidersSVGattrs.forEach(([attr, value]) => slidersSVG.setAttribute(attr, value))
// Top track
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 2, y1: 5.5, x2: 12, y2: 5.5 }))
slidersSVG.append(createSVGelem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
cx: 18, cy: 5.5, r: 3 }))
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 21, y1: 5.5, x2: 23, y2: 5.5 }))
// Middle track
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 2, y1: 12.5, x2: 4, y2: 12.5 }))
slidersSVG.append(createSVGelem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
cx: 8, cy: 12.5, r: 3 }))
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 14, y1: 12.5, x2: 23, y2: 12.5 }))
// Bottom track
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 2, y1: 19.5, x2: 8, y2: 19.5 }))
slidersSVG.append(createSVGelem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
cx: 14.5, cy: 19.5, r: 3 }))
slidersSVG.append(createSVGelem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
x1: 17, y1: 19.5, x2: 23, y2: 19.5 }))
return slidersSVG
}
},
sparkles: {
create(style) { // style = ( 'fg' ? filled front sparkle : 'bg' ? filled rear sparkles )
const sparklesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sparklesSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 512 512']]
sparklesSVGattrs.forEach(([attr, value]) => sparklesSVG.setAttribute(attr, value))
sparklesSVG.append(createSVGelem('path', { // large front sparkle
fill: style == 'bg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
d: 'M259.92,262.91,216.4,149.77a9,9,0,0,0-16.8,0L156.08,262.91a9,9,0,0,1-5.17,5.17L37.77,311.6a9,9,0,0,0,0,16.8l113.14,43.52a9,9,0,0,1,5.17,5.17L199.6,490.23a9,9,0,0,0,16.8,0l43.52-113.14a9,9,0,0,1,5.17-5.17L378.23,328.4a9,9,0,0,0,0-16.8L265.09,268.08A9,9,0,0,1,259.92,262.91Z' }))
sparklesSVG.append(createSVGelem('polygon', { // small(est) rear-left sparkle
fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 24,
points: '108 68 88 16 68 68 16 88 68 108 88 160 108 108 160 88 108 68' }))
sparklesSVG.append(createSVGelem('polygon', { // small rear-right sparkle
fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
points: '426.67 117.33 400 48 373.33 117.33 304 144 373.33 170.67 400 240 426.67 170.67 496 144 426.67 117.33' }))
return sparklesSVG
}
},
speaker: {
create() {
const speakerSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
speakerSVGattrs = [['width', 22], ['height', 22], ['viewBox', '0 0 32 32']]
speakerSVGattrs.forEach(([attr, value]) => speakerSVG.setAttribute(attr, value))
speakerSVG.append(
createSVGelem('path', { stroke: '', 'stroke-width': '2px', fill: 'none',
d: 'M24.5,26c2.881,-2.652 4.5,-6.249 4.5,-10c0,-3.751 -1.619,-7.348 -4.5,-10' }),
createSVGelem('path', { stroke: '', 'stroke-width': '2px', fill: 'none',
d: 'M22,20.847c1.281,-1.306 2,-3.077 2,-4.924c0,-1.846 -0.719,-3.617 -2,-4.923' }),
createSVGelem('path', { stroke: 'none', fill: '',
d: 'M9.957,10.88c-0.605,0.625 -1.415,0.98 -2.262,0.991c-4.695,0.022 -4.695,0.322 -4.695,4.129c0,3.806 0,4.105 4.695,4.129c0.846,0.011 1.656,0.366 2.261,0.991c1.045,1.078 2.766,2.856 4.245,4.384c0.474,0.49 1.18,0.631 1.791,0.36c0.611,-0.272 1.008,-0.904 1.008,-1.604c0,-4.585 0,-11.936 0,-16.52c0,-0.7 -0.397,-1.332 -1.008,-1.604c-0.611,-0.271 -1.317,-0.13 -1.791,0.36c-1.479,1.528 -3.2,3.306 -4.244,4.384Z' })
)
return speakerSVG
}
},
speechBalloonLasso: {
create() {
const speechBalloonLassoSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
speechBalloonLassoSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
speechBalloonLassoSVGattrs.forEach(([attr, value]) => speechBalloonLassoSVG.setAttribute(attr, value))
speechBalloonLassoSVG.append(createSVGelem('path', { stroke: 'none', d: 'M323-41v-247h-10q-105 0-172.5-67T73-528q0-105 74-179t179-74h36l-44-44 69-69 162 162-162 162-69-69 44-44h-36q-64 0-109.5 45.5T171-528q0 64 45.5 109.5T326-373h95v96l96-96h117q64 0 109.5-45.5T789-528q0-64-45.5-109.5T634-683h10v-98h-10q105 0 179 74t74 179q0 105-74 179t-179 74h-77L323-41Z' }))
return speechBalloonLassoSVG
}
},
speechBalloons: {
create() {
const speechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
speechBalloonSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 -960 960 960']]
speechBalloonSVGattrs.forEach(([attr, value]) => speechBalloonSVG.setAttribute(attr, value))
speechBalloonSVG.append(createSVGelem('path', { stroke: 'none', d: 'M350-212q-32.55 0-55.27-22.73Q272-257.45 272-290v-64h492v-342h63.67q33.33 0 55.83 22.72Q906-650.55 906-618v576L736-212H350ZM54-256v-582.4q0-32.38 22.72-54.99Q99.45-916 132-916h482q32.55 0 55.28 22.72Q692-870.55 692-838v334q0 32.55-22.72 55.27Q646.55-426 614-426H224L54-256Zm540-268v-294H152v294h442Zm-442 0v-294 294Z' }))
return speechBalloonSVG
}
},
sun: {
create() {
const sunSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sunSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
sunSVGattrs.forEach(([attr, value]) => sunSVG.setAttribute(attr, value))
sunSVG.append(createSVGelem('path', { stroke: 'none', d: 'M440-760v-160h80v160h-80Zm266 110-55-55 112-115 56 57-113 113Zm54 210v-80h160v80H760ZM440-40v-160h80v160h-80ZM254-652 140-763l57-56 113 113-56 54Zm508 512L651-255l54-54 114 110-57 59ZM40-440v-80h160v80H40Zm157 300-56-57 112-112 29 27 29 28-114 114Zm283-100q-100 0-170-70t-70-170q0-100 70-170t170-70q100 0 170 70t70 170q0 100-70 170t-170 70Zm0-80q66 0 113-47t47-113q0-66-47-113t-113-47q-66 0-113 47t-47 113q0 66 47 113t113 47Zm0-160Z' }))
return sunSVG
}
},
sunglasses: {
create() {
const sunglassesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
sunglassesSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
sunglassesSVGattrs.forEach(([attr, value]) => sunglassesSVG.setAttribute(attr, value))
sunglassesSVG.append(createSVGelem('path', { stroke: 'none', d: 'M507.44,185.327c-4.029-5.124-10.185-8.112-16.704-8.112c0,0-48.021,0-156.827,0h-65.774H243.87h-65.774c-108.806,0-156.827,0-156.827,0c-6.519,0-12.675,2.988-16.714,8.112c-4.028,5.125-5.486,11.815-3.965,18.152c0,0,12.421,56.269,19.927,82.534c7.506,26.265,26.265,48.772,86.29,48.772s59.827,0,74.828,0c21.258,0,46.256-19.99,55.028-45.023c4.97-14.16,12.756-32.738,19.338-47.876c6.582,15.138,14.368,33.716,19.338,47.876c8.773,25.033,33.77,45.023,55.028,45.023c15.001,0,14.803,0,74.828,0s78.784-22.507,86.29-48.772c7.496-26.264,19.918-82.534,19.918-82.534C512.935,197.142,511.478,190.452,507.44,185.327z M90.339,278.734C45.314,263.732,40.318,198.7,40.318,198.7s22.507,0,55.028,0L90.339,278.734z M340.464,278.734c-45.015-15.001-50.022-80.034-50.022-80.034s22.508,0,55.029,0L340.464,278.734z' }))
return sunglassesSVG
}
},
webCorner: {
create() {
const webSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
webSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 32 32']]
webSVGattrs.forEach(([attr, value]) => webSVG.setAttribute(attr, value))
webSVG.append(createSVGelem('path', { stroke: 'none', d: 'M29.9,2.6c-0.1-0.2-0.3-0.4-0.5-0.5C29.3,2,29.1,2,29,2H3C2.4,2,2,2.4,2,3s0.4,1,1,1h2c5,0,9,4,9,9c0,1.9-0.6,3.8-1.8,5.4l-4.9,4.9c-0.4,0.4-0.4,1,0,1.4C7.5,24.9,7.7,25,8,25s0.5-0.1,0.7-0.3l4.9-4.9c1.6-1.2,3.4-1.8,5.4-1.8c5,0,9,4,9,9v2 c0,0.6,0.4,1,1,1s1-0.4,1-1V3C30,2.9,30,2.7,29.9,2.6zM26.6,4l-4.8,4.8c0-1.9-0.8-3.5-2-4.8H26.6z M11.3,4H15c2.7,0,4.8,2.2,4.8,4.8c0,1-0.3,2-0.9,2.9l-3,3C16,14.2,16,13.6,16,13C16,9.3,14.1,6,11.3,4z M19,16c-0.6,0-1.2,0-1.7,0.1l3-3c0.8-0.6,1.8-0.9,2.9-0.9c2.7,0,4.8,2.2,4.8,4.8v3.7C26,17.9,22.7,16,19,16z M23.2,10.2L28,5.4v6.8C26.8,11,25.1,10.2,23.2,10.2z' }))
return webSVG
}
},
widescreen: {
wideSVGpath() { return createSVGelem('path', {
stroke: '', fill: '', 'fill-rule': 'evenodd', d: 'm26,13 0,10 -16,0 0,-10 z m-14,2 12,0 0,6 -12,0 0,-6 z'
})},
tallSVGpath() { return createSVGelem('path', {
stroke: '', fill: '', 'fill-rule': 'evenodd', d: 'm28,11 0,14 -20,0 0,-14 z m-18,2 16,0 0,10 -16,0 0,-10 z'
})},
create() {
const widescreenSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
widescreenSVGattrs = [['id', 'widescreen-icon'], ['width', 18], ['height', 18], ['viewBox', '8 8 20 20']]
widescreenSVGattrs.forEach(([attr, value]) => widescreenSVG.setAttribute(attr, value))
icons.widescreen.update(widescreenSVG)
return widescreenSVG
},
update(...targetIcons) {
targetIcons = targetIcons.flat() // flatten array args nested by spread operator
if (targetIcons.length == 0)
targetIcons = document.querySelectorAll('#widescreen-icon:not(.chatgpt-notif *)')
targetIcons.forEach(icon => {
icon.firstChild?.remove() // clear prev paths
icon.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
})
}
},
x: {
create() {
const xSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
xSVGattrs = [['height', '10px'], ['viewBox', '0 0 14 14'], 'fill', 'none']
xSVGattrs.forEach(([attr, value]) => xSVG.setAttribute(attr, value))
xSVG.append(createSVGelem('path', { d: 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z' }))
return xSVG
}
}
}
// Define LOGO functions
const logos = {
ddgpt: {
create() {
const ddgptLogo = document.createElement('img')
ddgptLogo.id = 'ddgpt-logo' ; ddgptLogo.className = 'no-mobile-tap-outline'
logos.ddgpt.update(ddgptLogo)
return ddgptLogo
},
update(...targetLogos) {
targetLogos = targetLogos.flat() // flatten array args nested by spread operator
if (targetLogos.length == 0) targetLogos = document.querySelectorAll('#ddgpt-logo')
targetLogos.forEach(logo => logo.src = GM_getResourceText(`ddgpt${ scheme == 'dark' ? 'DS' : 'LS' }logo`))
}
}
}
// Define UPDATE functions
const update = {
appBottomPos() { appDiv.style.bottom = `${ config.minimized ? 61 - appDiv.offsetHeight : -7 }px` },
appStyle() {
appStyle.innerText = (
'@keyframes modal-zoom-fade-out { 0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }'
+ '100% { opacity: 0 ; transform: scale(1.35) }}'
+ '@keyframes btn-zoom-fade-out { 0% { opacity: 1 } 50% { opacity: 0.65 ; transform: scale(1.85) }'
+ '75% { opacity: 0.05 ; transform: scale(3.15) } 100% { opacity: 0 ; transform: scale(5.85) }}'
+ '.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 }'
+ '#ddgpt * { 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 }'
+ '#ddgpt { border-radius: 8px ; padding: 17px 26px 16px ; flex-basis: 0 ; z-index: 5555 ;'
+ 'flex-grow: 1 ; word-wrap: break-word ; white-space: pre-wrap ; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.06) ;'
+ `background-image: linear-gradient(180deg, ${
scheme == 'dark' ? '#99a8a6 -215px, black 185px'
: `${ config.bgAnimationsDisabled ? 'white' : '#b6ebff' } -193px, white 65px` }) ;`
+ ( !config.fgAnimationsDisabled ?
'transition: bottom 0.1s cubic-bezier(0, 0, 0.2, 1),' // smoothen Anchor vertical minimize/restore
+ 'width 0.167s cubic-bezier(0, 0, 0.2, 1),' // smoothen Anchor horizontal expand/shrink
+ 'opacity 0.5s ease, transform 0.5s ease ;' : '' ) // smoothen 1st app fade-in
+ `border: ${ scheme == 'dark' ? 'none' : '1px solid #dadce0' }}`
+ '#ddgpt:hover { box-shadow: 0 1px 6px rgba(0, 0, 0, 0.14) }'
+ '#ddgpt p { margin: 0 ; ' + ( scheme == 'dark' ? 'color: #ccc } ' : ' } ' )
+ `#ddgpt .alert-link { color: ${ scheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }}`
+ ( scheme == 'dark' ? '#ddgpt a { text-decoration: underline }' : '' ) // underline dark-mode links in alerts
+ '.app-name, .app-name:hover { font-size: 1.5rem ; font-weight: 700 ; text-decoration: none ;'
+ `color: ${ scheme == 'dark' ? 'white' : 'black' }}`
+ '.kudoai { margin-left: 6px ; color: #aaa } '
+ '.kudoai a, .kudoai a:visited { color: #aaa ; text-decoration: none !important } '
+ `.kudoai a:hover { color: ${ scheme == 'dark' ? 'white' : 'black' }}`
+ '#corner-btns { float: right ; margin-top: 2px }'
+ '.corner-btn { float: right ; cursor: pointer ; position: relative ; top: 4px ; transition: transform 0.15s ease ;'
+ `${ scheme == 'dark' ? 'fill: white ; stroke: white' : 'fill: #adadad ; stroke: #adadad' };` // color
+ 'transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out }' // for re-appearances from btn-zoom-fade-out ends
+ `.corner-btn:hover { ${ scheme == 'dark' ? 'fill: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' } ;`
+ `${ config.fgAnimationsDisabled || browser.isMobile ? '' : 'transform: scale(1.285)' }}`
+ `.corner-btn:active { ${ scheme == 'dark' ? 'fill: #999999 ; stroke: #999999' : 'fill: #638ed4 ; stroke: #638ed4' } }`
+ ( config.bgAnimationsDisabled ? '' : ( '#ddgpt-logo, .corner-btn svg, .standby-btn'
+ `{ filter: drop-shadow(${ scheme == 'dark' ? '#7171714d 10px' : '#aaaaaa21 7px' } 7px 3px) }` ))
+ '#ddgpt .loading { color: #b6b8ba ; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite }'
+ '#ddgpt.sidebar-free { margin-left: 60px ; height: fit-content }'
+ '#font-size-slider-track { width: 98% ; height: 7px ; margin: -6px auto -13px ; padding: 15px 0 ;'
+ 'background-color: #ccc ; box-sizing: content-box; background-clip: content-box ; -webkit-background-clip: content-box }'
+ '#font-size-slider-track::before {' // to add finger cursor to unpadded core only
+ 'content: "" ; position: absolute ; top: 10px ; left: 0 ; right: 0 ; height: calc(100% - 20px) ; cursor: pointer }'
+ '#font-size-slider-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 }'
+ '#font-size-slider-thumb { z-index: 2 ; width: 10px ; height: 25px ; border-radius: 30% ; position: relative ; top: -7.65px ;'
+ `transition: transform 0.05s ease ; background-color: ${ scheme == 'dark' ? 'white' : '#4a4a4a' } ;`
+ 'box-shadow: rgba(0, 0, 0, 0.21) 1px 1px 9px 0 ; cursor: ew-resize }'
+ ( config.fgAnimationsDisabled || browser.isMobile ? '' : '#font-size-slider-thumb:hover { transform: scale(1.125) }' )
+ '.standby-btn { width: 100% ; margin: 9px 0 9px ; padding: 11px 0 ; cursor: pointer ;'
+ 'border-radius: 4px ; border: 1px solid #888 ;'
+ 'transition: transform 0.15s ease !important }'
+ '.standby-btn:hover { border-radius: 4px ;'
+ `${ scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' };`
+ `${ config.fgAnimationsDisabled || browser.isMobile ? '' : 'transform: scaleX(1.015) scaleY(1.03)' }}`
+ '#ddgpt pre { background-color: inherit }' // override DDG's unattractive thicc light border
+ '#ddgpt > pre {'
+ `font-size: ${config.fontSize}px ; white-space: pre-wrap ; min-width: 0 ;`
+ `line-height: ${ config.fontSize * config.lineHeightRatio }px ; overscroll-behavior: contain ;`
+ 'margin: .99rem 0 7px 0 ; padding: 1.25em 1.25em 0 1.25em ; border-radius: 10px ; overflow: auto ;'
+ ( !config.fgAnimationsDisabled ? // smoothen Anchor mode vertical expand/shrink
'transition: max-height 0.167s cubic-bezier(0, 0, 0.2, 1) ;' : '' )
+ `${ scheme == 'dark' ? 'background: #2b3a40cf ; color: #f2f2f2 ; border: 1px solid white'
: 'background: #eaeaeacf ; color: #202124 ; border: none' }}`
+ '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
+ '#ddgpt section.loading { padding-left: 5px }' // left-pad loading status when sending replies
+ '#ddgpt + footer { margin: 2px 0 25px ; position: relative }'
+ `#ddgpt + footer * { color: ${ scheme == 'dark' ? '#ccc' : '#666' } !important }`
+ '.balloon-tip { content: "" ; position: relative ; border: 7px solid transparent ;'
+ 'float: left ; left: 9px ; margin: 34px -14px 0 0 ;' // positioning
+ 'border-bottom-style: solid ; border-bottom-width: 1.19rem ; border-top: 0 ; border-bottom-color: '
+ ( scheme == 'dark' ? '##0000' : '#eaeaeacf' ) + '}'
+ '#copy-btn { float: right ; cursor: pointer }'
+ `pre > #copy-btn > svg { margin: -5px -6px 0 0 ; height: 15px ; width: 15px ; ${ scheme == 'dark' ? 'fill: white' : '' }}`
+ 'code #copy-btn { position: relative ; top: -6px ; right: -9px }'
+ 'code #copy-btn > svg { height: 13px ; width: 13px ; fill: white }'
+ '#app-chatbar {'
+ `border: solid 1px ${ scheme == 'dark' ? '#aaa' : '#638ed4' } ; border-radius: 12px 13px 12px 0 ;`
+ 'font-size: 0.92rem ; height: 19px ; width: 82.6% ; max-height: 200px ; resize: none ; '
+ `position: relative ; z-index: 555 ; color: #${ scheme == 'dark' ? 'eee' : '222' } ;`
+ 'margin: 3px 0 15px 0 ; padding: 13px 57px 9px 10px ;'
+ `background: ${ scheme == 'dark' ? '#5151519e' : '#eeeeee9e' }}`
+ '.related-queries {'
+ 'display: flex ; flex-wrap: wrap ; width: 100% ; position: relative ; overflow: visible ;'
+ `${ browser.isFirefox ? 'top: -20px ; margin: -3px 0 -10px' : 'top: -25px ; margin: -7px 0 -15px' }}`
+ '.related-query {'
+ 'box-sizing: border-box ; width: fit-content ; max-width: 100% ;' // confine to .related-queries bounds
+ `margin: 4px 4px ${ scheme == 'dark' ? 7 : 2 }px 0 ; padding: 4px 10px 5px 10px ;`
+ `color: ${ scheme == 'dark' ? '#f2f2f2' : '#767676' } ;`
+ `background: ${ scheme == 'dark' ? '#595858d6' : '#fbfbfbb0' } ;`
+ `border: 1px solid ${ scheme == 'dark' ? '#777' : '#e1e1e1' } ; font-size: 0.88em ; cursor: pointer ;`
+ 'border-radius: 0 13px 12px 13px ; flex: 0 0 auto ;'
+ `box-shadow: 1px 3px ${ scheme == 'dark' ? '11px -8px lightgray' : '8px -6px rgba(169, 169, 169, 0.75)' };`
+ `${ config.fgAnimationsDisabled ? '' : 'transition: transform 0.1s ease !important' }}`
+ '.related-query:hover, .related-query:focus {'
+ ( config.fgAnimationsDisabled || browser.isMobile ? '' : 'transform: scale(1.055) !important ;' )
+ `background: ${ scheme == 'dark' ? '#a2a2a270': '#dae5ffa3 ; color: #000000a8 ; border-color: #a3c9ff' }}`
+ '.related-query svg { position: relative ; top: 4px ; margin-right: 6px ;' // related query icon
+ `color: ${ scheme == 'dark' ? '#aaa' : '#c1c1c1' }}`
+ '.fade-in { opacity: 0 ; transform: translateY(10px) }'
+ '.fade-in-less { opacity: 0 ; transition: opacity 0.2s ease }'
+ '.fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) }'
+ '.chatbar-btn { z-index: 560 ;'
+ 'border: none ; float: right ; position: relative ; background: none ; cursor: pointer ;'
+ `bottom: ${ browser.isFirefox ? 50 : 55 }px ;`
+ `${ scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }}`
+ '.chatbar-btn:hover {'
+ `${ scheme == 'dark' ? 'color: #white ; fill: #white ; stroke: #white' : 'color: #638ed4 ; fill: #638ed4 ; stroke: #638ed4' }}`
+ ( // rendered markdown styles
'#ddgpt > pre h1 { font-size: 24px } #ddgpt > pre h2 { font-size: 22px } #ddgpt > pre h3 { font-size: 20px }' // size headings
+ '#ddgpt > pre h1, #ddgpt > pre h2, #ddgpt > pre h3 { margin-bottom: -15px }' // reduce gap after headings
+ '#ddgpt > pre ol { margin: -30px 0 -20px }' // reduce v-padding
+ '#ddgpt > pre ol > li { margin: -10px 0 0 1.6em ; list-style: decimal }' // reduce v-padding, show number markers
+ '#ddgpt > pre ol > li::marker { font-size: 0.9em }' // shrink number markers
+ '#ddgpt > pre ul { margin: -28px 0 -21px }' // reduce v-padding
+ '#ddgpt > pre ul > li { margin: -10px 0 0 1.2em ; list-style: inside }' ) // reduce v-padding, show bullets
+ '.katex-html { display: none } ' // hide unrendered math
+ '.chatgpt-notif { fill: white ; stroke: white ; color: white ; padding: 7.5px 14px 6.5px 11.5px !important }'
+ '.notif-close-btn { display: none !important }' // hide notif close btn
+ '.chatgpt-modal > div { padding: 20px 25px 24px 25px !important ;' // increase alert padding
+ 'background-color: white !important ; color: black }'
+ '.chatgpt-modal p { margin: -8px 0 -14px 4px ; font-size: 1.55rem }' // pos/size modal msg
+ `.chatgpt-modal a { color: #${ scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
+ `.modal-buttons { margin: 24px -5px -3px ${ browser.isMobile ? -5 : -15 }px !important ; width: 100% }` // pos/size modal buttons
+ '.chatgpt-modal button {' // modal buttons
+ 'font-size: 1rem ; text-transform: uppercase ; min-width: 121px ;'
+ `padding: ${ browser.isMobile ? '7px' : '4px 10px' } !important ;`
+ 'cursor: pointer ; border-radius: 0 !important ; height: 39px ;'
+ 'border: 1px solid ' + ( scheme == 'dark' ? 'white' : 'black' ) + '!important ;'
+ `${ scheme == 'dark' ? 'background: none ; color: white' : '' }}`
+ '.primary-modal-btn { background: black !important ; color: white !important }'
+ '.chatgpt-modal button:hover { background-color: #9cdaff !important ; color: black !important }'
+ ( scheme == 'dark' ? // darkmode chatgpt.alert() styles
( '.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {'
+ 'background-color: black !important ; color: white }'
+ '.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 }' ) : '' )
+ '[class*="-modal-bg"] {'
+ 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
+ 'transition: background-color .15s ease ;' // speed to show bg dim
+ 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
+ '[class*="-modal-bg"].animated > div { z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }'
+ '[class$="-modal"] {' // native modals + chatgpt.alert()s
+ 'z-index: 13456 ; position: absolute ;' // to be click-draggable
+ 'opacity: 0 ;' // to fade-in
+ `background-image: linear-gradient(180deg, ${ scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) ;`
+ `border: 1px solid ${ scheme == 'dark' ? 'white' : '#b5b5b5' } !important ;`
+ `color: ${ scheme == 'dark' ? 'white' : 'black' } ;`
+ 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from
+ 'transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-ins
+ 'transform 0.55s cubic-bezier(.165,.84,.44,1) !important }' // for move-ins
+ ( config.fgAnimationsDisabled || browser.isMobile ? '' : (
'[class$="-modal"] button { transition: transform 0.15s ease }'
+ '[class$="-modal"] button:hover { transform: scale(1.055) }' ))
+ '.ddgpt-menu { position: absolute ; z-index: 2250 ;'
+ 'padding: 3.5px 5px !important ; font-family: "Source Sans Pro", sans-serif ; font-size: 12px }'
+ '.ddgpt-menu ul { margin: 0 ; padding: 0 ; list-style: none }'
+ '.ddgpt-menu-item { padding: 0 5px ; line-height: 20.5px }'
+ '.ddgpt-menu-item:not(.ddgpt-menu-header):hover {'
+ 'cursor: pointer ; background: white ; color: black ; fill: black }'
+ '#checkmark-icon { fill: #b3f96d } .ddgpt-menu-item:hover #checkmark-icon { fill: green }'
// Glowing modal btns
+ ':root { --glow-color: hsl(186 100% 69%); }'
+ '.glowing-btn { perspective: 2em ; font-weight: 900 ; animation: border-flicker 2s linear infinite ;'
+ '-webkit-box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) ;'
+ 'box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) ;'
+ '-moz-box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) }'
+ '.glowing-txt { animation: text-flicker 3s linear infinite ;'
+ '-webkit-text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) ;'
+ '-moz-text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) ;'
+ 'text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) }'
+ '.faulty-letter { opacity: 0.5 ; animation: faulty-flicker 2s linear infinite }'
+ '.glowing-btn::after { content: "" ; position: absolute ; top: 0 ; bottom: 0 ; left: 0 ; right: 0 ;'
+ 'opacity: 0 ; z-index: -1 ; box-shadow: 0 0 2em 0.2em var(--glow-color) ;'
+ 'background-color: var(--glow-color) ; transition: opacity 100ms linear }'
+ '.glowing-btn:hover { color: rgba(0, 0, 0, 0.8) ; text-shadow: none ; animation: none }'
+ '.glowing-btn:hover .glowing-txt { animation: none }'
+ '.glowing-btn:hover .faulty-letter { animation: none ; text-shadow: none ; opacity: 1 }'
+ '.glowing-btn:hover:before { filter: blur(1.5em) ; opacity: 1 }'
+ '.glowing-btn:hover:after { opacity: 1 }'
+ '@keyframes faulty-flicker { 0% { opacity: 0.1 } 2% { opacity: 0.1 } 4% { opacity: 0.5 } 19% { opacity: 0.5 }'
+ '21% { opacity: 0.1 } 23% { opacity: 1 } 80% { opacity: 0.5 } 83% { opacity: 0.4 } 87% { opacity: 1 }}'
+ '@keyframes text-flicker { 0% { opacity: 0.1 } 2% { opacity: 1 } 8% { opacity: 0.1 } 9% { opacity: 1 }'
+ '12% { opacity: 0.1 } 20% { opacity: 1 } 25% { opacity: 0.3 } 30% { opacity: 1 } 70% { opacity: 0.7 }'
+ '72% { opacity: 0.2 } 77% { opacity: 0.9 } 100% { opacity: 0.9 }}'
+ '@keyframes border-flicker { 0% { opacity: 0.1 } 2% { opacity: 1 } 4% { opacity: 0.1 } 8% { opacity: 1 }'
+ '70% { opacity: 0.7 } 100% { opacity: 1 }}'
// chatgpt.alert() + DDGPT modals
+ '.ddgpt-modal { 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 {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
+ ( scheme == 'dark' ? '[class*="modal-close-btn"]:hover path { stroke: black ; fill: black }' : '' ) // invert dark mode hover paths
+ '[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-weight: bold ; line-height: 32px ; padding: 0 ; margin: 9px 0 14px !important ;'
+ `${ browser.isMobile ? 'text-align: center' : 'justify-self: start' }}` // left-align on desktop, center on mobile
+ '[class*="-modal"] p { justify-self: start ; font-size: 20px }'
+ '[class*="-modal"] button { font-size: 14px }'
// Settings modal
+ '#ddgpt-settings {'
+ `min-width: ${ browser.isPortrait ? 288 : 698 }px ; max-width: 75vw ; word-wrap: break-word ;`
+ 'border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
+ `${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}` // icon color
+ '#ddgpt-settings-title { font-weight: bold ; line-height: 19px ; text-align: center ; margin: 0 3px -3px 0 }'
+ `#ddgpt-settings-title h4 { font-size: ${ browser.isPortrait ? 26 : 31 }px ; font-weight: bold ; margin-top: -39px }`
+ '#ddgpt-settings ul { list-style: none ; padding: 0 ; margin-bottom: 2px ;' // hide bullets, close bottom gap
+ `width: ${ browser.isPortrait ? 100 : 50 }% }` // set width based on column cnt
+ '#ddgpt-settings li {'
+ `color: ${ scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for text
+ `fill: ${ scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for icons
+ `stroke: ${ scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for icons
+ 'height: 25px ; font-size: 14.5px ; transition: transform 0.1s ease ;'
+ `padding: 4px 10px ; border-bottom: 1px dotted ${ scheme == 'dark' ? 'white' : 'black' } ;` // add settings separators
+ 'border-radius: 3px }' // make highlight strips slightly rounded
+ '#ddgpt-settings li.active {'
+ `color: ${ scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' } ;` // for text
+ `fill: ${ scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' } ;` // for icons
+ `stroke: ${ scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' }}` // for icons
+ '#ddgpt-settings li label { padding-right: 20px }' // right-pad labels so toggles don't hug
+ '#ddgpt-settings li:last-of-type { border-bottom: none }' // remove last bottom-border
+ '#ddgpt-settings li, #ddgpt-settings li label { cursor: pointer }' // add finger on hover
+ '#ddgpt-settings li:hover { opacity: 1 ;'
+ 'background: rgba(100, 149, 237, 0.88) ; color: white ; fill: white ; stroke: white ;' // add highlight strip
+ `${ config.fgAnimationsDisabled || browser.isMobile ? '' : 'transform: scale(1.22)' }}` // add zoom
+ '#ddgpt-settings li > input { float: right }' // pos toggles
+ '#scheme-menu-entry > span { margin: 0 -2px }' // align Scheme status
+ '#scheme-menu-entry > span > svg { position: relative ; top: 3px ; margin-left: 4px }' // v-align/left-pad Scheme status icon
+ ( config.fgAnimationsDisabled ? '' : '#arrows-cycle { animation: rotation 5s linear infinite }' )
+ '@keyframes rotation { from { transform: rotate(0deg) } to { transform: rotate(360deg) }}'
+ `#about-menu-entry span { color: ${ scheme == 'dark' ? '#28ee28' : 'green' }}`
+ `#about-menu-entry > span { width: ${ browser.isPortrait ? '15vw' : '92px' } ; height: 20px ; overflow: hidden ;` // outer About status span
+ `${ 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-menu-entry > span > div { text-wrap: nowrap ;'
+ `${ config.fgAnimationsDisabled ? '' : 'animation: ticker linear 60s infinite' }}`
+ '@keyframes ticker { 0% { transform: translateX(100%) } 100% { transform: translateX(-2000%) }}'
+ `.about-em { color: ${ scheme == 'dark' ? 'white' : 'green' } !important }`
)
},
chatbarWidth() {
const chatbar = appDiv.querySelector('#app-chatbar')
if (chatbar) chatbar.style.width = `${
config.widerSidebar && !config.anchored ? 85.6 : config.expanded ? 86.9 : 82.6 }%`
},
rqVisibility() {
const relatedQueriesDiv = appDiv.querySelector('.related-queries')
if (relatedQueriesDiv) { // update visibility based on latest setting
relatedQueriesDiv.style.display = config.rqDisabled || config.anchored ? 'none' : 'flex'
if (!browser.isMobile) appFooter.style.right = ( // counteract right-offset bug from chatbar padding
relatedQueriesDiv.style.display == 'flex' ? 0 : '-72px' )
}
},
scheme(newScheme) {
log.caller = `update.scheme('${newScheme}')`
log.debug(`Updating ${app.name} scheme to ${log.toTitleCase(newScheme)}...`)
scheme = newScheme ; logos.ddgpt.update()
update.appStyle() ; update.stars() ; toggle.btnGlow() ; modals.settings.updateSchemeStatus()
log.debug(`Success! ${app.name} updated to ${log.toTitleCase(newScheme)} scheme`)
},
stars() {
['sm', 'med', 'lg'].forEach(size =>
document.querySelectorAll(`[id*="stars-${size}"]`).forEach(starsDiv =>
starsDiv.id = config.bgAnimationsDisabled ? `stars-${size}-off`
: `${ scheme == 'dark' ? 'white' : 'black' }-stars-${size}`
))
},
tweaksStyle() {
// Update tweaks style based on settings
tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
+ ( config.stickySidebar ? ssbStyles
: config.anchored ? ( anchorStyles + ( config.expanded ? expandedStyles : '' )) : '' )
// Update 'by KudoAI' visibility based on corner space available
const kudoAIspan = appDiv.querySelector('.kudoai')
if (kudoAIspan) {
const visibleBtnCnt = [...appDiv.querySelectorAll('.corner-btn')]
.filter(btn => getComputedStyle(btn).display != 'none').length
kudoAIspan.style.display = visibleBtnCnt <= (
config.anchored && config.expanded ? 10
: !config.anchored && config.widerSidebar ? 7
: browser.isMobile ? 3 : 4 ) ? '' : 'none'
}
// Update <pre> max-height for various mode toggles
const answerPre = appDiv.querySelector('pre'),
relatedQueries = appDiv.querySelector('.related-queries'),
shorterPreHeight = window.innerHeight - relatedQueries?.offsetHeight - 245,
longerPreHeight = window.innerHeight - 255
if (answerPre) answerPre.style.maxHeight = (
config.stickySidebar ? ( relatedQueries?.offsetHeight > 0 ? `${shorterPreHeight}px` : `${longerPreHeight}px` )
: config.anchored ? `${ longerPreHeight - ( config.expanded ? 115 : 365 ) }px` : 'none'
)
}
}
// Define UI functions
function isCenteredMode() { return document.documentElement.classList.toString().includes('center') }
function fillStarryBG(targetNode) {
const starsDivsContainer = document.createElement('div')
starsDivsContainer.style.cssText = 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
+ 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
+ 'z-index: -1'; // allow interactive elems to be clicked
['sm', 'med', 'lg'].forEach(starSize => {
const starsDiv = document.createElement('div')
starsDiv.id = config.bgAnimationsDisabled ? `stars-${starSize}-off`
: `${ scheme == 'dark' ? 'white' : 'black' }-stars-${starSize}`
starsDivsContainer.append(starsDiv)
})
targetNode.prepend(starsDivsContainer)
}
const listenerize = {
appDiv() {
appDiv.addEventListener(inputEvents.down, event => { // to dismiss visible font size slider
if (event.button != 0) return // prevent non-left-click dismissal
if (document.getElementById('font-size-slider-track') // slider is visible
&& !event.target.closest('[id*="font-size"]') // not clicking slider elem
&& getComputedStyle(event.target).cursor != 'pointer') // ...or other interactive elem
fontSizeSlider.toggle('off')
})
},
cornerBtns() {
appDiv.querySelectorAll('.corner-btn').forEach(btn => { // from right to left
if (btn.id == 'chevron-btn') btn.onclick = () => {
if (appDiv.querySelector('#font-size-slider-track')?.classList.contains('active')) fontSizeSlider.toggle('off')
toggle.minimized()
}
else if (btn.id == 'about-btn') btn.onclick = modals.about.show
else if (btn.id == 'settings-btn') btn.onclick = modals.settings.show
else if (btn.id == 'speak-btn') btn.onclick = () => {
const wholeAnswer = appDiv.querySelector('pre').textContent
const cjsSpeakOptions = { voice: 2, pitch: 1, speed: 1.5 }
const sgtDialectMap = [
{ code: 'en', regex: /^(eng(lish)?|en(-\w\w)?)$/i, rate: 2 },
{ code: 'ar', regex: /^(ara?(bic)?|اللغة العربية)$/i, rate: 1.5 },
{ code: 'cs', regex: /^(cze(ch)?|[cč]e[sš].*|cs)$/i, rate: 1.4 },
{ code: 'da', regex: /^dan?(ish|sk)?$/i, rate: 1.3 },
{ code: 'de', regex: /^(german|deu?(tsch)?)$/i, rate: 1.5 },
{ code: 'es', regex: /^(spa(nish)?|espa.*|es(-\w\w)?)$/i, rate: 1.5 },
{ code: 'fi', regex: /^(fin?(nish)?|suom.*)$/i, rate: 1.4 },
{ code: 'fr', regex: /^fr/i, rate: 1.2 },
{ code: 'hu', regex: /^(hun?(garian)?|magyar)$/i, rate: 1.5 },
{ code: 'it', regex: /^ita?(lian[ao]?)?$/i, rate: 1.4 },
{ code: 'ja', regex: /^(ja?pa?n(ese)?|日本語|ja)$/i, rate: 1.5 },
{ code: 'nl', regex: /^(dut(ch)?|flemish|nederlandse?|vlaamse?|nld?)$/i, rate: 1.3 },
{ code: 'pl', regex: /^po?l(ish|ski)?$/i, rate: 1.4 },
{ code: 'pt', regex: /^(por(tugu[eê]se?)?|pt(-\w\w)?)$/i, rate: 1.5 },
{ code: 'ru', regex: /^(rus?(sian)?|русский)$/i, rate: 1.3 },
{ code: 'sv', regex: /^(swe?(dish)?|sv(enska)?)$/i, rate: 1.4 },
{ code: 'tr', regex: /^t[uü]?r(k.*)?$/i, rate: 1.6 },
{ code: 'vi', regex: /^vi[eệ]?t?(namese)?$/i, rate: 1.5 },
{ code: 'zh-CHS', regex: /^(chi(nese)?|zh|中[国國])/i, rate: 2 }
]
const sgtReplyDialect = sgtDialectMap.find(entry => entry.regex.test(config.replyLanguage)) || sgtDialectMap[0],
payload = { text: wholeAnswer, curTime: Date.now(), spokenDialect: sgtReplyDialect.code, rate: sgtReplyDialect.rate.toString() },
key = CryptoJS.enc.Utf8.parse('76350b1840ff9832eb6244ac6d444366'),
iv = CryptoJS.enc.Utf8.parse(atob('AAAAAAAAAAAAAAAAAAAAAA==') || '76350b1840ff9832eb6244ac6d444366')
const securePayload = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
iv: iv, mode: CryptoJS.mode.CBC, pad: CryptoJS.pad.Pkcs7 }).toString()
xhr({ // audio from Sogou TTS
url: 'https://fanyi.sogou.com/openapi/external/getWebTTS?S-AppId=102356845&S-Param='
+ encodeURIComponent(securePayload),
method: 'GET', responseType: 'arraybuffer',
onload: async resp => {
if (resp.status != 200) chatgpt.speak(wholeAnswer, cjsSpeakOptions)
else {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
audioContext.decodeAudioData(resp.response, buffer => {
const audioSrc = audioContext.createBufferSource()
audioSrc.buffer = buffer
audioSrc.connect(audioContext.destination) // connect source to speakers
audioSrc.start(0) // play audio
}).catch(() => chatgpt.speak(wholeAnswer, cjsSpeakOptions))
}}})
}
else if (btn.id == 'font-size-btn') btn.onclick = () => fontSizeSlider.toggle()
else if (btn.id == 'pin-btn') btn.onclick = btn.onmouseover = btn.onmouseout = menus.pin.toggle
else if (btn.id == 'wsb-btn') btn.onclick = () => toggle.sidebar('wider')
else if (btn.id == 'arrows-btn') btn.onclick = () => toggle.expandedMode()
if (!browser.isMobile && btn.id != 'pin-btn') // add hover listeners for tooltips
btn.onmouseover = btn.onmouseout = toggle.tooltip
if (!/wsb|pin/.test(btn.id)) btn.onmouseup = () => { // add zoom/fade-out except to WSB/pin btns
if (config.fgAnimationsDisabled) return
if (!(btn.id == 'font-size-btn' && appDiv.querySelector('#font-size-slider-track')?.classList.contains('active')))
btn.style.animation = 'btn-zoom-fade-out .220s ease-out' // animate except when dismissing Font Size slider
setTimeout(() => { // hide btn after animation nears completion
Object.assign(btn.style, { opacity: '0', visibility: 'hidden', animation: '' })
setTimeout(() => // show btn after short delay
Object.assign(btn.style, { visibility: 'visible', opacity: '1' }), 200)
}, 170)
}
})
},
replySection() {
// Add form key listener
const replyForm = appDiv.querySelector('form')
replyForm.onkeydown = event => {
if (event.key == 'Enter' || event.keyCode == 13) {
if (event.ctrlKey) { // add newline
const chatTextarea = appDiv.querySelector('#app-chatbar'),
caretPos = chatTextarea.selectionStart,
textBefore = chatTextarea.value.substring(0, caretPos),
textAfter = chatTextarea.value.substring(caretPos)
chatTextarea.value = textBefore + '\n' + textAfter // add newline
chatTextarea.selectionStart = chatTextarea.selectionEnd = caretPos + 1 // preserve caret pos
listenerize.replySection.chatbarAutoSizer()
} else if (!event.shiftKey) listenerize.replySection.submitHandler(event)
}}
// Add form submit listener
listenerize.replySection.submitHandler = function(event) {
event.preventDefault()
const chatTextarea = appDiv.querySelector('#app-chatbar')
// No reply, change placeholder + focus chatbar
if (chatTextarea.value.trim() == '') {
chatTextarea.placeholder = `${ msgs.placeholder_typeSomething || 'Type something' }...`
chatTextarea.focus()
// Yes reply, submit it + transform to loading UI
} else {
// Modify/submit msg chain
if (msgChain.length > 2) msgChain.splice(0, 2) // keep token usage maintainable
msgChain = stripQueryAugments(msgChain)
const prevReplyTrimmed = appDiv.querySelector('pre')?.textContent.substring(0, 250 - chatTextarea.value.length) || ''
msgChain.push({ role: 'assistant', content: prevReplyTrimmed })
msgChain.push({ role: 'user', content: augmentQuery(chatTextarea.value) })
get.reply(msgChain)
// Hide/remove elems
appDiv.querySelector('.related-queries')?.remove() // remove related queries
if (!browser.isMobile) // hide 'Send reply' tooltip post-send btn click + reset show.reply()'s counteract right-offset bug from chatbar padding
tooltipDiv.style.opacity = appFooter.style.right = 0
// Show loading status
const replySection = appDiv.querySelector('section')
replySection.classList.add('loading', 'no-user-select')
replySection.innerText = appAlerts.waitingResponse
// Set flags
show.reply.src = null ; show.reply.chatbarFocused = false ; show.reply.userInteracted = true
}
}
replyForm.onsubmit = listenerize.replySection.submitHandler
// Add chatbar autosizer
const chatTextarea = appDiv.querySelector('#app-chatbar'),
{ paddingTop, paddingBottom } = getComputedStyle(chatTextarea),
vOffset = parseInt(paddingTop, 10) + parseInt(paddingBottom, 10)
let prevLength = chatTextarea.value.length
listenerize.replySection.chatbarAutoSizer = () => {
const newLength = chatTextarea.value.length
if (newLength < prevLength) { // if deleting txt
chatTextarea.style.height = 'auto' // ...auto-fit height
if (parseInt(getComputedStyle(chatTextarea).height, 10) < 35) // if down to one line
chatTextarea.style.height = '19px' // ...reset to original height
}
chatTextarea.style.height = chatTextarea.scrollHeight - vOffset + 'px'
prevLength = newLength
}
chatTextarea.oninput = listenerize.replySection.chatbarAutoSizer
// Add button listeners
appDiv.querySelectorAll('.chatbar-btn').forEach(btn => {
if (btn.id == 'shuffle-btn') btn.onclick = () => {
const randQAprompt = 'Generate a single random question on any topic then answer it. '
+ 'Don\'t talk about Canberra, Tokyo, blue whales, photosynthesis, oceans, '
+ 'deserts, mindfulness meditation, the Fibonacci sequence, the liver, '
+ 'Jupiter, the Great Wall of China, Sheakespeare or da Vinci. '
+ 'Try to give an answer that is 25-50 words. '
+ 'Do not type anything but the question and answer. Reply in markdown.'
chatTextarea.value = augmentQuery(randQAprompt)
chatTextarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
show.reply.src = 'shuffle'
}
if (!browser.isMobile) // add hover listener for tooltips
btn.onmouseover = btn.onmouseout = toggle.tooltip
})
}
}
const fontSizeSlider = {
fadeInDelay: 5, // ms
hWheelDistance: 10, // px
createAppend() {
log.caller = 'fontSizeSlider.createAppend()'
log.debug('Creating/appending Font Size slider...')
// Create/ID/classify slider elems
fontSizeSlider.cursorOverlay = document.createElement('div')
fontSizeSlider.cursorOverlay.classList.add('cursor-overlay') // for resize cursor
const slider = document.createElement('div') ; slider.id = 'font-size-slider-track'
slider.className = 'fade-in-less' ; slider.style.display = 'none'
const sliderThumb = document.createElement('div') ; sliderThumb.id = 'font-size-slider-thumb'
sliderThumb.title = Math.floor(config.fontSize *10) /10 + 'px' // font size tooltip
const sliderTip = document.createElement('div') ; sliderTip.id = 'font-size-slider-tip'
// Assemble/insert elems
slider.append(sliderThumb, sliderTip)
appDiv.insertBefore(slider, appDiv.querySelector('.btn-tooltip,' // desktop
+ 'pre')) // mobile
// Init thumb pos
setTimeout(() => {
const iniLeft = (config.fontSize - config.minFontSize) / (config.maxFontSize - config.minFontSize) // left ratio
* (slider.offsetWidth - sliderThumb.offsetWidth) // slider width
sliderThumb.style.left = iniLeft + 'px'
}, fontSizeSlider.fadeInDelay) // to ensure visibility for accurate dimension calcs
// Add event listeners for dragging thumb
let isDragging = false, startX, startLeft
sliderThumb.addEventListener(inputEvents.down, event => {
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.parentNode)
fontSizeSlider.cursorOverlay.parentNode.removeChild(fontSizeSlider.cursorOverlay)
})
// Add event listener for wheel-scrolling thumb
if (!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 answerPre = appDiv.querySelector('pre'),
fontSizePercent = newLeft / sliderWidth,
fontSize = config.minFontSize + fontSizePercent * (config.maxFontSize - config.minFontSize)
answerPre.style.fontSize = fontSize + 'px'
answerPre.style.lineHeight = fontSize * config.lineHeightRatio + 'px'
saveSetting('fontSize', fontSize)
sliderThumb.title = Math.floor(config.fontSize *10) /10 + 'px'
}
return slider
},
toggle(state = '') {
log.caller = `fontSizeSlider.toggle(${ state ? `'${state}'` : '' })`
const slider = document.getElementById('font-size-slider-track') || fontSizeSlider.createAppend(),
replyTip = appDiv.querySelector('.balloon-tip'),
sliderTip = document.getElementById('font-size-slider-tip')
// Show slider
if (state == 'on' || (!state && slider.style.display == 'none')) {
log.debug('Showing Font Size slider...')
// Position slider tip
const btnSpan = document.getElementById('font-size-btn'),
rects = { appDiv: appDiv.getBoundingClientRect(), btnSpan: btnSpan.getBoundingClientRect() }
sliderTip.style.right = `${ rects.appDiv.right - ( rects.btnSpan.left + rects.btnSpan.right )/2 -35.5 }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)
log.debug('Success! Font Size slider shown')
// Hide slider
} else if (state == 'off' || (!state && slider.style.display != 'none')) {
log.debug('Hiding Font Size slider...')
slider.classList.remove('active') ; if (replyTip) replyTip.style.display = ''
sliderTip.style.display = slider.style.display = 'none'
log.debug('Success! Font Size slider hidden')
}
}
}
// Define FACTORY functions
function createAnchor(linkHref, displayContent, attrs = {}) {
const anchor = document.createElement('a'),
defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
finalAttrs = { ...defaultAttrs, ...attrs }
Object.entries(finalAttrs).forEach(([attr, value]) => anchor.setAttribute(attr, value))
if (displayContent) anchor.append(displayContent)
return anchor
}
function createStyle(content) {
const style = document.createElement('style')
if (content) style.innerText = content
return style
}
function createSVGelem(type, attrs) {
const elem = document.createElementNS('http://www.w3.org/2000/svg', type)
for (const attr in attrs) elem.setAttributeNS(null, attr, attrs[attr])
return elem
}
// Define TOGGLE functions
const toggle = {
anchorMode(state = '') {
log.caller = `toggle.anchorMode(${ state ? `'${state}'` : '' })`
const prevState = config.anchored // for restraining notif if no change from #pin-menu 'Sidebar' click
if (state == 'on' || !state && !config.anchored) {
log.debug('Toggling Anchor Mode on...')
saveSetting('anchored', true)
if (config.stickySidebar) toggle.sidebar('sticky') // off
} else {
log.debug('Toggling Anchor Mode off...')
saveSetting('anchored', false)
if (config.expanded) toggle.expandedMode('off')
}
update.tweaksStyle() ; update.chatbarWidth() ; update.rqVisibility() // apply new state to UI
if (modals.settings.get()) { // update visual state of Settings toggle
const anchorToggle = document.querySelector('[id*="anchor"][id*="menu-entry"] input')
if (anchorToggle.checked != config.anchored) modals.settings.toggle.switch(anchorToggle)
}
if (prevState != config.anchored) {
menus.pin.topPos = menus.pin.rightPos = null
notify(( msgs.mode_anchor || 'Anchor Mode' ) + ' ' + menuState.word[+config.anchored])
log.debug(`Success! Anchor Mode toggled ${ config.anchored ? 'ON' : 'OFF' }`)
}
},
animations(layer) {
log.caller = `toggle.animations('${layer}')`
const configKey = layer + 'AnimationsDisabled'
log.debug(`Toggling ${layer.toUpperCase()} animations ${ config[configKey] ? 'ON' : 'OFF' }...`)
saveSetting(configKey, !config[configKey])
update.appStyle() ; if (layer == 'bg') update.stars()
if (layer == 'fg' && modals.settings.get()) {
// Toggle ticker-scroll of About status label
const aboutStatusLabel = document.querySelector('#about-menu-entry > span > div')
aboutStatusLabel.innerHTML = modals.settings.aboutContent[config.fgAnimationsDisabled ? 'short' : 'long']
aboutStatusLabel.style.float = config.fgAnimationsDisabled ? 'right' : ''
// Toggle button glow
if (scheme == 'dark') toggle.btnGlow()
}
log.caller = `toggle.animations('${layer}')`
log.debug(`Success! ${layer.toUpperCase()} animations toggled ${ config[configKey] ? 'OFF' : 'ON' }`)
notify(`${settingsProps[layer + 'AnimationsDisabled'].label} ${menuState.word[+!config[layer + 'AnimationsDisabled']]}`)
},
autoGet() {
log.caller = 'toggle.autoGet()'
log.debug(`Toggling Auto-Get ${ config.autoGet ? 'OFF' : 'ON' }...`)
saveSetting('autoGet', !config.autoGet)
if (appDiv.querySelector('.standby-btn')) show.reply.standbyBtnClickHandler()
if (config.autoGet) // disable Prefix/Suffix mode if enabled
['prefix', 'suffix'].forEach(manualMode => {
if (config[manualMode + 'Enabled']) toggle.manualGet(manualMode) })
notify(`${settingsProps.autoGet.label} ${menuState.word[+config.autoGet]}`)
if (modals.settings.get()) { // update visual state of Settings toggle
const autoGetToggle = document.querySelector('[id*="autoGet"][id*="menu-entry"] input')
if (autoGetToggle.checked != config.autoGet) modals.settings.toggle.switch(autoGetToggle)
}
log.caller = 'toggle.autoGet()'
log.debug(`Success! config.autoget = ${config.autoGet}`)
},
btnGlow(state = '') {
const removeCondition = state == 'off' || scheme != 'dark' || config.fgAnimationsDisabled
document.querySelectorAll('[class*="-modal"] button').forEach((btn, idx) => {
setTimeout(() => btn.classList[removeCondition ? 'remove' : 'add']('glowing-btn'),
(idx +1) *50 *chatgpt.randomFloat()) // to unsync flickers
let btnTextSpan = btn.querySelector('span')
if (!btnTextSpan) { // wrap btn.textContent for .glowing-txt
btnTextSpan = document.createElement('span')
btnTextSpan.textContent = btn.textContent ; btn.textContent = ''
btn.append(btnTextSpan)
}
btnTextSpan.classList[removeCondition ? 'remove' : 'add']('glowing-txt')
})
},
expandedMode(state = '') {
log.caller = `toggle.expandedMode(${ state ? `'${state}'` : '' })`
const toExpand = state == 'on' || !state && !config.expanded
log.debug(`${ toExpand ? 'Expanding' : 'Shrinking' } ${app.name}...`)
saveSetting('expanded', toExpand)
if (config.minimized) toggle.minimized('off') // since user wants to see stuff
update.tweaksStyle() ; update.chatbarWidth() // apply new state to UI
icons.arrowsDiagonal.update() ; tooltipDiv.style.opacity = 0 // update icon/tooltip
log.caller = `toggle.expandedMode(${ state ? `'${state}'` : '' })`
log.debug(`Success! ${app.name} ${ toExpand ? 'expanded' : 'shrunk' }`)
},
manualGet(mode) { // Prefix/Suffix modes
log.caller = `toggle.manualGet('${mode}')`
const modeKey = mode + 'Enabled'
log.debug(`Toggling ${log.toTitleCase(mode)} Mode ${ config[modeKey] ? 'OFF' : 'ON' }...`)
saveSetting(modeKey, !config[modeKey])
if (config[modeKey] && config.autoGet) toggle.autoGet() // disable Auto-Get mode if enabled
notify(`${settingsProps[modeKey].label} ${menuState.word[+config[modeKey]]}`)
if (modals.settings.get()) { // update visual state of Settings toggle
const modeToggle = document.querySelector(`[id*="${modeKey}"][id*="menu-entry"] input`)
if (modeToggle.checked != config[modeKey]) modals.settings.toggle.switch(modeToggle)
}
log.caller = `toggle.manualGet('${mode}')`
log.debug(`Success! config.${modeKey} = ${config[modeKey]}`)
},
minimized(state = '') {
log.caller = `toggle.minimized(${ state ? `'${state}'` : '' })`
const toMinimize = state == 'on' || !state && !config.minimized
log.debug(`${ toMinimize ? 'Mimizing' : 'Restoring' } ${app.name}...`)
saveSetting('minimized', toMinimize)
const chevronBtn = appDiv.querySelector('#chevron-btn')
if (chevronBtn) { // update icon
const chevronSVG = icons[`chevron${ config.minimized ? 'Up' : 'Down' }`].create()
chevronBtn.removeChild(chevronBtn.firstChild) ; chevronBtn.append(chevronSVG)
chevronBtn.onclick = () => {
if (appDiv.querySelector('#font-size-slider-track')?.classList.contains('active')) fontSizeSlider.toggle('off')
toggle.minimized()
}
}
update.appBottomPos() // toggle visual minimization
if (!browser.isMobile) setTimeout(() => tooltipDiv.style.opacity = 0, 1) // remove lingering tooltip
log.caller = `toggle.minimized(${ state ? `'${state}'` : '' })`
log.debug(`Success! ${app.name} ${ toMinimize ? 'minimized' : 'restored' }`)
},
proxyMode() {
log.caller = 'toggle.proxyMode()'
log.debug(`Toggling Proxy Mode ${ config.proxyAPIenabled ? 'OFF' : 'ON' }...`)
saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
notify(( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' ' + menuState.word[+config.proxyAPIenabled])
refreshMenu()
if (modals.settings.get()) { // update visual states of Settings toggles
const proxyToggle = document.querySelector('[id*="proxy"][id*="menu-entry"] input'),
streamingToggle = document.querySelector('[id*="streaming"][id*="menu-entry"] input')
if (proxyToggle.checked != config.proxyAPIenabled) // Proxy state out-of-sync (from using toolbar menu)
modals.settings.toggle.switch(proxyToggle)
if (streamingToggle.checked && !config.proxyAPIenabled // Streaming checked but OpenAI mode
|| !streamingToggle.checked && config.proxyAPIenabled && !config.streamingDisabled) // or Streaming unchecked but enabled in Proxy mode
modals.settings.toggle.switch(streamingToggle)
}
if (appDiv.querySelector('#ddgpt-alert')) location.reload() // re-send query if user alerted
else {
log.caller = 'toggle.proxyMode()'
log.debug(`Success! config.proxyAPIenabled = ${config.proxyAPIenabled}`)
}
},
relatedQueries() {
log.caller = 'toggle.relatedQueries()'
log.debug(`Toggling Related Queries ${ config.rqDisabled ? 'ON' : 'OFF' }...`)
saveSetting('rqDisabled', !config.rqDisabled)
update.rqVisibility()
if (!config.rqDisabled && !appDiv.querySelector('.related-queries')) // get related queries for 1st time
get.related(stripQueryAugments(msgChain)[msgChain.length - 1].content).then(queries => show.related(queries))
.catch(err => { log.error(err.message) ; api.tryNew(get.related) })
update.tweaksStyle() // toggle <pre> max-height
notify(( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' ' + menuState.word[+!config.rqDisabled])
log.debug(`Success! config.rqDisabled = ${config.rqDisabled}`)
},
sidebar(mode, state = '') {
log.caller = `toggle.sidebar('${mode}'${ state ? `, '${state}'` : '' })`
const configKeyName = mode + 'Sidebar',
toToggleOn = state == 'on' || !state && !config[configKeyName],
prevStickyState = config.stickySidebar // for restraining notif if no change from #pin-menu Sidebar-click
log.debug(`Toggling ${log.toTitleCase(mode)} Sidebar ${ toToggleOn ? 'ON' : 'OFF' }`)
if (state == 'on' || !state && !config[configKeyName]) { // toggle on
if (mode == 'sticky' && config.anchored) toggle.anchorMode()
saveSetting(configKeyName, true)
} else saveSetting(configKeyName, false)
update.tweaksStyle() ; update.chatbarWidth() // apply new state to UI
if (mode == 'wider') icons.widescreen.update() // toggle icons everywhere
if (modals.settings.get()) { // update visual state of Settings toggle
const stickySidebarToggle = document.querySelector('[id*="sticky"][id*="menu-entry"] input')
if (stickySidebarToggle.checked != config.stickySidebar) modals.settings.toggle.switch(stickySidebarToggle)
}
if (mode == 'sticky' && prevStickyState == config.stickySidebar)
return log.debug(`No change to ${log.toTitleCase(mode)} Sidebar`)
notify(( msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' )
+ ' ' + menuState.word[+config[configKeyName]])
log.debug(`Success! ${log.toTitleCase(mode)} Sidebar toggled ${ toToggleOn ? 'ON' : 'OFF' }`)
},
streaming() {
log.caller = 'toggle.streaming()'
const scriptCatLink = browser.isFirefox ? 'https://addons.mozilla.org/firefox/addon/scriptcat/'
: browser.isEdge ? 'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh'
: 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf'
if (!streamingSupported.userscriptManager) { // alert userscript manager unsupported, suggest TM/SC
log.debug(`Streaming Mode unsupported in ${getUserscriptManager()}`)
const suggestAlertID = siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
`${settingsProps.streamingDisabled.label} ${ msgs.alert_isOnlyAvailFor || 'is only available for' }`
+ ( !browser.isEdge && !browser.isBrave ? // suggest TM for supported browsers
` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${ msgs.alert_and || 'and' }`
: '' )
+ ` <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a>.` // suggest SC
+ ` (${ msgs.alert_userscriptMgrNoStream || 'Your userscript manager does not support returning stream responses' }.)`
)
const suggestAlert = document.getElementById(suggestAlertID).firstChild
modals.init(suggestAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
} else if (!streamingSupported.browser) { // alert TM/browser unsupported, suggest SC
log.debug('Streaming Mode unsupported in browser')
const suggestAlertID = siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
`${settingsProps.streamingDisabled.label} ${ msgs.alert_isUnsupportedIn || 'is unsupported in' } `
+ `${ browser.isChrome ? 'Chrome' : browser.isEdge ? 'Edge' : 'Brave' } ${ msgs.alert_whenUsing || 'when using' } Tampermonkey. `
+ `${ msgs.alert_pleaseUse || 'Please use' } <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a> `
+ `${ msgs.alert_instead || 'instead' }.`
)
const suggestAlert = document.getElementById(suggestAlertID).firstChild
modals.init(suggestAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
} else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
log.debug('Streaming Mode unsupported in OpenAI mode')
let msg = `${settingsProps.streamingDisabled.label} `
+ `${ msgs.alert_isCurrentlyOnlyAvailBy || 'is currently only available by' } `
+ `${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }. `
+ `(${ msgs.alert_openAIsupportSoon || 'Support for OpenAI API will be added shortly' }!)`
const switchPhrase = msgs.alert_switchingOn || 'switching on'
msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
const alertID = siteAlert(`${ msgs.mode_streaming || 'Streaming Mode' } ${ msgs.alert_unavailable || 'unavailable' }`, msg),
alert = document.getElementById(alertID).firstChild
modals.init(alert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() }
} else { // functional toggle
log.debug(`Toggling Streaming Mode ${ config.streamingDisabled ? 'ON' : 'OFF' }`)
saveSetting('streamingDisabled', !config.streamingDisabled)
notify(settingsProps.streamingDisabled.label + ' ' + menuState.word[+!config.streamingDisabled])
log.debug(`Success! config.streamingDisabled = ${config.streamingDisabled}`)
}
},
tooltip(event) {
const btnElem = event.currentTarget, btnType = btnElem.id.replace(/-btn$/, ''),
cornerBtnTypes = ['chevron', 'about', 'settings', 'speak', 'font-size', 'pin', 'wsb', 'arrows']
// Update text
tooltipDiv.innerText = (
btnType == 'chevron' ? ( config.minimized ? `${ msgs.tooltip_restore || 'Restore' }`
: `${ msgs.tooltip_minimize || 'Minimize' }` )
: btnType == 'about' ? msgs.menuLabel_about || 'About'
: btnType == 'settings' ? msgs.menuLabel_settings || 'Settings'
: btnType == 'speak' ? msgs.tooltip_playAnswer || 'Play answer'
: btnType == 'font-size' ? msgs.tooltip_fontSize || 'Font size'
: btnType == 'wsb' ? (( config.widerSidebar ? `${ msgs.prefix_exit || 'Exit' } ` : '' )
+ ( msgs.menuLabel_widerSidebar || 'Wider Sidebar' ))
: btnType == 'arrows' ? ( config.expanded ? `${ msgs.tooltip_shrink || 'Shrink' }`
: `${ msgs.tooltip_expand || 'Expand' }` )
: btnType == 'copy' ? ( btnElem.firstChild.id == 'copy-icon' ? `${ msgs.tooltip_copy || 'Copy' } ${
( btnElem.parentNode.tagName == 'PRE' ? msgs.tooltip_reply || 'Reply' : msgs.tooltip_code || 'Code' ).toLowerCase() }`
: `${ msgs.notif_copiedToClipboard || 'Copied to clipboard' }!` )
: btnType == 'send' ? msgs.tooltip_sendReply || 'Send reply'
: btnType == 'shuffle' ? msgs.tooltip_askRandQuestion || 'Ask random question' : '' )
// Update position
const elems = { appDiv, btnElem, tooltipDiv },
rects = {} ; Object.keys(elems).forEach(key => rects[key] = elems[key].getBoundingClientRect())
tooltipDiv.style.top = `${ cornerBtnTypes.includes(btnType) ? -15 : rects.btnElem.top - rects.appDiv.top -36 }px`
tooltipDiv.style.right = `${ rects.appDiv.right - ( rects.btnElem.left + rects.btnElem.right )/2 - rects.tooltipDiv.width/2 }px`
// Toggle visibility
tooltipDiv.style.opacity = event.type == 'mouseover' ? 1 : 0
}
}
// Define SESSION functions
const session = {
deleteOpenAIcookies() {
log.caller = 'session.deleteOpenAIcookies()'
log.debug('Deleting OpenAI cookies...')
GM_deleteValue(app.configKeyPrefix + '_openAItoken')
if (getUserscriptManager() != 'Tampermonkey') return
GM_cookie.list({ url: apis.OpenAI.endpoints.auth }, (cookies, error) => {
if (!error) { for (const cookie of cookies) {
GM_cookie.delete({ url: apis.OpenAI.endpoints.auth, name: cookie.name })
}}})
},
generateGPTFLkey() {
log.caller = 'session.generateGPTFLkey()'
log.debug('Generating GPTforLove key...')
let nn = Math.floor(new Date().getTime() / 1e3)
const fD = e => {
let t = CryptoJS.enc.Utf8.parse(e),
o = CryptoJS.AES.encrypt(t, 'vrewbhjvbrejhbevwjh156645', {
mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
})
return o.toString()
}
const gptflKey = fD(nn)
log.debug(gptflKey) ; return gptflKey
},
getOAItoken() {
log.caller = 'session.getOAItoken()'
log.debug('Getting OpenAI token...')
return new Promise(resolve => {
const accessToken = GM_getValue(app.configKeyPrefix + '_openAItoken')
if (accessToken) { log.debug(accessToken) ; resolve(accessToken) }
else {
log.debug(`No token found. Fetching from ${apis.OpenAI.endpoints.session}...`)
xhr({ url: apis.OpenAI.endpoints.session, onload: resp => {
if (session.isBlockedByCF(resp.responseText)) {
appAlert('checkCloudflare') ; return }
try {
const newAccessToken = JSON.parse(resp.responseText).accessToken
GM_setValue(app.configKeyPrefix + '_openAItoken', newAccessToken)
log.debug(`Success! newAccessToken = ${newAccessToken}`)
resolve(newAccessToken)
} catch { if (get.reply.api == 'OpenAI') appAlert('login') ; return }
}})}})
},
isBlockedByCF(resp) {
try {
const html = new DOMParser().parseFromString(resp, 'text/html'),
title = html.querySelector('title')
if (title.innerText == 'Just a moment...') {
log.caller = 'session.isBlockedByCF'
log.debug('Blocked by CloudFlare')
return true
}
} catch (err) { return false }
}
}
// Define API functions
const api = {
pick(caller) {
log.caller = `get.${caller.name}() » api.pick()`
const untriedAPIs = Object.keys(apis).filter(api =>
api != ( caller == get.reply ? 'OpenAI' : '' ) // exclude OpenAI for get.reply() since Proxy Mode
&& !caller.triedAPIs.some(entry => Object.prototype.hasOwnProperty.call(entry, api)) // exclude tried APIs
&& (config.streamingDisabled || apis[api].streamable)) // exclude unstreamable APIs if !config.streamingDisabled
const chosenAPI = untriedAPIs[ // pick random array entry
Math.floor(chatgpt.randomFloat() * untriedAPIs.length)]
if (!chosenAPI) { log.error('No proxy APIs left untried') ; return null }
log.debug('Endpoint chosen', apis[chosenAPI].endpoints?.completions || apis[chosenAPI].endpoint)
return chosenAPI
},
tryNew(caller, reason = 'err') {
log.caller = `get.${caller.name}() » api.tryNew()`
if (caller.status == 'done') return
log.error(`Error using ${ apis[caller.api].endpoints?.completions || apis[caller.api].endpoint } due to ${reason}`)
caller.triedAPIs.push({ [caller.api]: reason })
if (caller.attemptCnt < Object.keys(apis).length -+(caller == get.reply)) {
log.debug('Trying another endpoint...')
caller.attemptCnt++
caller(caller == get.reply ? msgChain : get.related.query)
.then(result => { if (caller == get.related) show.related(result) ; else return })
} else {
log.debug('No remaining untried endpoints')
if (caller == get.reply) appAlert('proxyNotWorking', 'suggestOpenAI')
}
},
clearTimedOut(triedAPIs) { // to retry on new queries
triedAPIs.splice(0, triedAPIs.length, // empty apiArray
...triedAPIs.filter(entry => Object.values(entry)[0] != 'timeout')) // replace w/ err'd APIs
},
createHeaders(api) {
const ip = ipv4.generate({ verbose: false })
const headers = {
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Connection': 'keep-alive', 'Content-Type': 'application/json', 'DNT': '1',
'Host': new URL(apis[api].endpoints?.completions || apis[api].endpoint).hostname,
'Origin': apis[api].expectedOrigin.url,
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors',
'TE': 'trailers', 'X-Forwarded-For': ip, 'X-Real-IP': ip
}
headers.Referer = headers.Origin + '/'
if (api == 'OpenAI') headers.Authorization = 'Bearer ' + config.openAIkey
Object.assign(headers, apis[api].expectedOrigin.headers)
return headers
},
createPayload(api, msgs) {
let payload = {}
if (api == 'OpenAI')
payload = { messages: msgs, model: 'gpt-3.5-turbo', max_tokens: 4000 }
else if (api == 'AIchatOS') {
payload = {
prompt: msgs[msgs.length - 1].content,
withoutContext: false, userId: apis.AIchatOS.userID, network: true
}
} else if (api == 'Free Chat') {
const availModels = apis[api].availModels
payload = { messages: msgs, model: availModels[Math.floor(chatgpt.randomFloat() * availModels.length)] }
} else if (api == 'GPTforLove') {
payload = {
prompt: msgs[msgs.length - 1].content,
secret: session.generateGPTFLkey(), top_p: 1, temperature: 0.8,
systemMessage: 'You are ChatGPT, the version is GPT-4o, a large language model trained by OpenAI.'
+ 'Follow the user\'s instructions carefully.'
}
if (apis.GPTforLove.parentID) payload.options = { parentMessageId: apis.GPTforLove.parentID }
} else if (api == 'MixerBox AI')
payload = { prompt: msgs, model: 'gpt-3.5-turbo' }
return JSON.stringify(payload)
}
}
// Define QUERY AUGMENT functions
function augmentQuery(query) { return query + ` (reply in ${config.replyLanguage})` }
function stripQueryAugments(msgChain) {
const augmentCnt = augmentQuery.toString().match(/\+/g).length
return msgChain.map(msg => { // stripped chain
if (msg.role == 'user') {
let content = msg.content
const augments = content.match(/\s*\([^)]*\)\s*/g)
if (augments) for (let i = 0 ; i < augmentCnt ; i++) // strip augments
content = content.replace(augments[augments.length - 1 - i], '')
return { ...msg, content: content.trim() }
} else return msg // agent's unstripped
})
}
// Define GET functions
const get = {
json(url, callback) { // for dynamic footer
xhr({ method: 'GET', url: url, onload: resp => {
if (resp.status >= 200 && resp.status < 300) {
try { const data = JSON.parse(resp.responseText) ; callback(null, data) }
catch (err) { callback(err, null) }
} else callback(new Error('Failed to load data: ' + resp.statusText), null)
}})
},
async reply(msgChain) {
// Init API attempt props
get.reply.status = 'waiting'
if (!get.reply.triedAPIs) get.reply.triedAPIs = []
if (!get.reply.attemptCnt) get.reply.attemptCnt = 1
// Pick API
get.reply.api = config.proxyAPIenabled ? api.pick(get.reply) : 'OpenAI'
if (!get.reply.api) { // no more proxy APIs left untried
appAlert('proxyNotWorking', 'suggestOpenAI') ; return }
// Init OpenAI key
if (!config.proxyAPIenabled)
config.openAIkey = await Promise.race([session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))])
// Try diff API after 6-9s of no response
else {
const iniAPI = get.reply.api
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 ? 9000 : 6000)
}
// Get/show answer from AI
xhr({
method: apis[get.reply.api].method,
url: apis[get.reply.api].endpoints?.completions || apis[get.reply.api].endpoint,
responseType: config.streamingDisabled || !config.proxyAPIenabled ? 'text' : 'stream',
headers: api.createHeaders(get.reply.api), data: api.createPayload(get.reply.api, msgChain),
onload: resp => dataProcess.text(get.reply, resp),
onloadstart: resp => dataProcess.stream(get.reply, resp),
onerror: err => { log.error(err)
if (!config.proxyAPIenabled) appAlert(!config.openAIkey ? 'login' : ['openAInotWorking', 'suggestProxy'])
else api.tryNew(get.reply)
}
})
// Get/show related queries if enabled on 1st get.reply()
if (!config.rqDisabled && get.reply.attemptCnt == 1)
get.related(stripQueryAugments(msgChain)[msgChain.length - 1].content).then(queries => show.related(queries))
.catch(err => { log.error(err.message) ; api.tryNew(get.related) })
},
async related(query) {
// Init API attempt props
get.related.status = 'waiting'
if (!get.related.triedAPIs) get.related.triedAPIs = []
if (!get.related.attemptCnt) get.related.attemptCnt = 1
// Pick API
get.related.api = api.pick(get.related)
if (!get.related.api) return // no more proxy APIs left untried
// Init OpenAI key
if (get.related.api == 'OpenAI')
config.openAIkey = await Promise.race([session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))])
// Init prompt
const rqPrompt = 'Give me a numbered list of '
+ `${ get.related.replyIsQuestion ? 'possible answers to this question' : 'queries related to this one' }:\n\n"${query}"\n\n`
+ ( get.related.replyIsQuestion ? 'Generate answers as if in reply to a search engine chatbot asking the question.'
// Extended instructions for non-question queries
: get.related.api == 'Free Chat' ? '' // to evade long query detection
: ( 'Make sure to suggest a variety that can even greatly deviate from the original topic. '
+ 'For example, if the original query asked about someone\'s wife, '
+ ' a good related query could involve a different relative and using their name. '
+ 'Another example, if the query asked about a game/movie/show, '
+ ' good related queries could involve pertinent characters. '
+ 'Another example, if the original query asked how to learn JavaScript, '
+ ' good related queries could ask why/when/where instead, even replacing JS w/ other languages. '
+ 'But the key is variety. Do not be repetitive. '
+ ' You must entice user to want to ask one of your related queries.' ))
+ ` Reply in ${config.replyLanguage}`
// Try diff API after 7s of no response
const iniAPI = get.related.api
get.related.query = query // expose to api.tryNew() in case modded
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)
// Get queries
return new Promise(resolve => xhr({
method: apis[get.related.api].method,
url: apis[get.related.api].endpoints?.completions || apis[get.related.api].endpoint,
responseType: 'text', headers: api.createHeaders(get.related.api),
data: api.createPayload(get.related.api, [{ role: 'user', content: rqPrompt }]),
onload: resp => dataProcess.text(get.related, resp).then(resolve),
onerror: err => { log.error(err) ; api.tryNew(get.related) }
}))
}
}
// Define PROCESS functions
const dataProcess = {
initFailFlags(api) { // escape/merge URLs w/ fail flags
const { failFlags = [], endpoint = apis[api].endpoints.completions, expectedOrigin } = apis[api],
escapedAPIurls = [endpoint, expectedOrigin.url].map(url => url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
return new RegExp([...failFlags, ...escapedAPIurls].join('|'))
},
stream(caller, stream) {
if (config.streamingDisabled || !config.proxyAPIenabled) return
log.caller = `get.${caller.name}() » dataProcess.stream()`
const failFlagsAndURLs = dataProcess.initFailFlags(caller.api),
reader = stream.response.getReader() ; let accumulatedChunks = ''
reader.read().then(processStreamText).catch(err => log.error('Error processing stream', err.message))
function processStreamText({ done, value }) {
if (done) { caller.sender = null
if (appDiv.querySelector('.loading')) // no text shown
api.tryNew(caller)
else { // text was shown
caller.status = 'done' ; caller.attemptCnt = null
show.copyBtns() ; api.clearTimedOut(caller.triedAPIs)
} return
}
let chunk = new TextDecoder('utf8').decode(new Uint8Array(value))
if (caller.api == 'MixerBox AI') { // pre-process chunks
const extractedChunks = Array.from(chunk.matchAll(/data:(.*)/g), match => match[1]
.replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
.filter(match => !/(?:message_(?:start|end)|done)/.test(match))
chunk = extractedChunks.join('')
}
accumulatedChunks = apis[caller.api].accumulatesText ? chunk : accumulatedChunks + chunk
try { // to show stream text
let textToShow = ''
if (caller.api == 'GPTforLove') { // extract parentID + latest chunk text
const jsonLines = accumulatedChunks.split('\n'),
nowResult = JSON.parse(jsonLines[jsonLines.length - 1])
if (nowResult.id) apis.GPTforLove.parentID = nowResult.id // for contextual replies
textToShow = nowResult.text
} else textToShow = accumulatedChunks
const failMatch = textToShow.match(failFlagsAndURLs)
if (failMatch) {
log.debug('Response text', textToShow)
log.error('Fail flag detected', failMatch[0])
if (caller.status !== 'done' && !caller.sender) api.tryNew(caller)
return
} else if (caller.status != 'done') { // app waiting or sending
if (!caller.sender) caller.sender = caller.api // app is waiting, become sender
if (caller.sender == caller.api // app is sending from this caller.api
&& textToShow.trim() != '' // empty chunk not read
) show.reply(textToShow)
}
} catch (err) { log.error('Error showing stream', err.message) }
return reader.read().then(({ done, value }) => {
if (caller.sender == caller.api) // am designated sender, recurse
processStreamText({ done, value })
}).catch(err => log.error('Error reading stream', err.message))
}
},
text(caller, resp) {
return new Promise(resolve => {
if (caller == get.reply && config.proxyAPIenabled && !config.streamingDisabled || caller.status == 'done') return
log.caller = `get.${caller.name}() » dataProcess.text()`
const failFlagsAndURLs = dataProcess.initFailFlags(caller.api) ; let respText = ''
if (resp.status != 200) {
log.error('Response status', resp.status) ; log.info('Response', resp)
if (caller == get.reply && caller.api == 'OpenAI')
appAlert(resp.status == 401 ? 'login'
: resp.status == 403 ? 'checkCloudflare'
: resp.status == 429 ? ['tooManyRequests', 'suggestProxy']
: ['openAInotWorking', 'suggestProxy'] )
else api.tryNew(caller)
} else if (caller.api == 'OpenAI' && resp.response) {
const failMatch = resp.response.match(failFlagsAndURLs)
if (failMatch) { // suggest proxy or try diff API
log.debug('Response text', resp.response)
log.error('Fail flag detected', failMatch[0])
if (caller == get.reply) appAlert('openAInotWorking', 'suggestProxy')
else api.tryNew(caller)
} else {
try { // to show response or return related queries
respText = JSON.parse(resp.response).choices[0].message.content
handleProcessCompletion()
} catch (err) { handleProcessError(err) }
}
} else if (resp.responseText) {
if (caller.api == 'AIchatOS') {
try { // to show response or return related queries
const text = resp.responseText, chunkSize = 1024
let currentIdx = 0
while (currentIdx < text.length) {
const chunk = text.substring(currentIdx, currentIdx + chunkSize)
respText += chunk ; currentIdx += chunkSize
}
handleProcessCompletion()
} catch (err) { handleProcessError(err) }
} else if (caller.api == 'Free Chat') {
try { // to show response or return related queries
respText = resp.responseText ; handleProcessCompletion()
} catch (err) { handleProcessError(err) }
} else if (caller.api == 'GPTforLove') {
try { // to show response or return related queries
let chunks = resp.responseText.trim().split('\n'),
lastObj = JSON.parse(chunks[chunks.length - 1])
if (lastObj.id) apis.GPTforLove.parentID = lastObj.id
respText = lastObj.text ; handleProcessCompletion()
} catch (err) { handleProcessError(err) }
} else if (caller.api == 'MixerBox AI') {
try { // to show response or return related queries
const extractedData = Array.from(resp.responseText.matchAll(/data:(.*)/g), match => match[1]
.replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
.filter(match => !/(?:message_(?:start|end)|done)/.test(match))
respText = extractedData.join('') ; handleProcessCompletion()
} catch (err) { handleProcessError(err) }
}
} else if (caller.status != 'done') { // proxy 200 response failure
log.info('Response', resp.responseText) ; api.tryNew(caller) }
function handleProcessCompletion() {
if (caller.status != 'done') {
const failMatch = respText.match(failFlagsAndURLs)
if (failMatch) {
log.debug('Response text', respText)
log.error('Fail flag detected', failMatch[0])
api.tryNew(caller)
} else {
log.debug('Response text', respText)
caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
if (caller == get.reply) { show.reply(respText) ; show.copyBtns() }
else resolve(arrayify(respText))
}}}
function handleProcessError(err) { // suggest proxy or try diff API
log.debug('Response text', resp.response)
log.error(appAlerts.parseFailed, err)
if (caller.api == 'OpenAI' && caller == get.reply) appAlert('openAInotWorking', 'suggestProxy')
else api.tryNew(caller)
}
function arrayify(strList) { // for get.related() calls
log.caller = 'dataProcess.text » arrayify()'
log.debug('Arrayifying related queries...')
return (strList.trim().match(/\d+\.?\s*(.+?)(?=\n|\\n|$)/g) || [])
.slice(0, 5) // limit to 1st 5
.map(match => match.replace(/\*\*/g, '') // strip markdown boldenings
.replace(/^['"]*(?:\d+\.?\s*)?['"]*(.*?)['"]*$/g, '$1')) // strip numbering + quotes
}
})}
}
// Define SHOW functions
const show = {
copyBtns() {
if (document.getElementById('copy-btn')) return
const fadeDuration = 220, // ms
reappearDelay = 200, // ms
fadeDurationOffset = 150 // ms — for early hide to not trigger overflow scrollbar
appDiv.querySelectorAll('#ddgpt > pre, code').forEach(parentElem => {
const copySpan = document.createElement('span'),
copySVG = icons.copy.create(parentElem)
copySpan.id = 'copy-btn' ; copySVG.id = 'copy-icon'
copySpan.className = 'no-mobile-tap-outline'
copySpan.append(copySVG) ; let elemToPrepend = copySpan
// Wrap code button in div for v-offset
if (parentElem.tagName == 'CODE') {
elemToPrepend = document.createElement('div')
elemToPrepend.style.height = '11px'
elemToPrepend.append(copySpan)
}
// Add listeners
if (!browser.isMobile) copySpan.onmouseover = copySpan.onmouseout = toggle.tooltip
copySpan.onclick = event => { // copy text, update icon + tooltip status
const copySVG = copySpan.querySelector('#copy-icon')
if (!copySVG) return // since clicking on copied icon
const textContainer = copySpan.parentNode.tagName == 'PRE' ? copySpan.parentNode // reply container
: copySpan.parentNode.parentNode, // code container
textToCopy = textContainer.textContent.replace(/^>> /, '').trim(),
checkmarksSVG = icons.checkmarkDouble.create() ; checkmarksSVG.id = 'copied-icon'
setTimeout(() => copySpan.replaceChild(checkmarksSVG, copySVG), // change to copied icon
fadeDuration + reappearDelay - fadeDurationOffset) // ...after copySpan reappears
setTimeout(() => copySpan.replaceChild(copySVG, checkmarksSVG), 1355) // change back to copy icon
navigator.clipboard.writeText(textToCopy) // copy text to clipboard
if (!browser.isMobile) toggle.tooltip(event) // show copied status in tooltip
}
copySpan.onmouseup = () => { // zoom/fade-out
if (config.fgAnimationsDisabled) return
const copySVG = copySpan.querySelector('#copy-icon')
if (!copySVG) return // since clicking on copied icon
copySpan.style.animation = `btn-zoom-fade-out .${fadeDuration}s ease-out`
setTimeout(() => { // hide copySpan after animation nears completion
Object.assign(copySpan.style, { opacity: '0', visibility: 'hidden', animation: '' })
setTimeout(() => // show copySpan after reappearDelay
Object.assign(copySpan.style, { visibility: 'visible', opacity: '1' }), reappearDelay)
}, fadeDuration - fadeDurationOffset)
}
// Prepend button
parentElem.prepend(elemToPrepend)
})
},
reply(answer) {
// Build answer interface up to reply section if missing
if (!appDiv.querySelector('pre')) {
while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear app content
fillStarryBG(appDiv) // add stars
// Create/append title
const appHeaderLogo = logos.ddgpt.create()
appHeaderLogo.width = 181 ; appHeaderLogo.style.margin = '-7px 0'
const appTitleAnchor = createAnchor(app.urls.app, appHeaderLogo)
appTitleAnchor.classList.add('app-name', 'no-user-select')
appDiv.append(appTitleAnchor)
// Create/append corner buttons div
const cornerBtnsDiv = document.createElement('div')
cornerBtnsDiv.id = 'corner-btns' ; cornerBtnsDiv.className = 'no-mobile-tap-outline'
appDiv.append(cornerBtnsDiv)
// Create/append Chevron button
if (!isCentered && !browser.isMobile) {
var chevronSpan = document.createElement('span'),
chevronSVG = icons[`chevron${ config.minimized ? 'Up' : 'Down' }`].create()
chevronSpan.id = 'chevron-btn' // for toggle.tooltip()
chevronSpan.className = 'corner-btn' ; chevronSpan.style.margin = '-1.5px 1px 0 11px'
chevronSpan.style.display = 'none' // to activate from anchorStyles only
chevronSpan.append(chevronSVG) ; cornerBtnsDiv.append(chevronSpan)
}
// Create/append About button
const aboutSpan = document.createElement('span'),
aboutSVG = icons.questionMarkCircle.create()
aboutSpan.id = 'about-btn' // for toggle.tooltip()
aboutSpan.className = 'corner-btn'
aboutSpan.append(aboutSVG) ; cornerBtnsDiv.append(aboutSpan)
// Create/append Settings button
const settingsSpan = document.createElement('span'),
settingsSVG = icons.sliders.create()
settingsSpan.id = 'settings-btn' // for toggle.tooltip()
settingsSpan.className = 'corner-btn' ; settingsSpan.style.margin = '0 10.5px 0 0.5px'
settingsSpan.append(settingsSVG) ; cornerBtnsDiv.append(settingsSpan)
// Create/append Speak button
if (answer != 'standby') {
var speakerSpan = document.createElement('span'),
speakerSVG = icons.speaker.create()
speakerSpan.id = 'speak-btn' // for toggle.tooltip()
speakerSpan.className = 'corner-btn' ; speakerSpan.style.margin = '-2px 8px 0 0'
speakerSpan.append(speakerSVG) ; cornerBtnsDiv.append(speakerSpan)
}
// Create/append Font Size button
if (answer != 'standby') {
var fontSizeSpan = document.createElement('span'),
fontSizeSVG = icons.fontSize.create()
fontSizeSpan.id = 'font-size-btn' // for toggle.tooltip()
fontSizeSpan.className = 'corner-btn' ; fontSizeSpan.style.marginRight = '10px'
fontSizeSpan.append(fontSizeSVG) ; cornerBtnsDiv.append(fontSizeSpan)
}
// Create/append Pin button
if (!isCentered && !browser.isMobile) {
var pinSpan = document.createElement('span'),
pinSVG = icons.pin.create()
pinSpan.id = 'pin-btn' // for toggle.sidebar() + toggle.tooltip()
pinSpan.className = 'corner-btn' ; pinSpan.style.margin = '1px 9px 0 0'
pinSpan.append(pinSVG) ; cornerBtnsDiv.append(pinSpan)
// Create/append Wider Sidebar button
var wsbSpan = document.createElement('span'),
wsbSVG = icons.widescreen.create()
wsbSpan.id = 'wsb-btn' // for toggle.sidebar() + toggle.tooltip()
wsbSpan.className = 'corner-btn' ; wsbSpan.style.margin = `${ browser.isFirefox ? 0.5 : 0 }px 13.5px 0 0`
wsbSpan.append(wsbSVG) ; cornerBtnsDiv.append(wsbSpan)
// Create/append Expand/Shrink button
var arrowsSpan = document.createElement('span'),
arrowsSVG = icons.arrowsDiagonal.create()
arrowsSVG.style.transform = 'rotate(-7deg)' // tilt slightly to hint expansions are often horizontal-only
arrowsSpan.id = 'arrows-btn' // for toggle.tooltip()
arrowsSpan.className = 'corner-btn' ; arrowsSpan.style.margin = '0.5px 12px 0 0'
arrowsSpan.style.display = 'none' // to activate from anchorStyles only
arrowsSpan.append(arrowsSVG) ; cornerBtnsDiv.append(arrowsSpan)
}
// Add tooltips
if (!browser.isMobile) appDiv.append(tooltipDiv)
// Add corner button listeners
listenerize.cornerBtns()
// Create/append 'by KudoAI'
const kudoAIspan = document.createElement('span')
kudoAIspan.classList.add('kudoai', 'no-user-select') ; kudoAIspan.textContent = 'by '
kudoAIspan.append(createAnchor(app.urls.publisher, 'KudoAI'))
appDiv.querySelector('.app-name').insertAdjacentElement('afterend', kudoAIspan)
// Show standby state if prefix/suffix mode on
if (answer == 'standby') {
const standbyBtn = document.createElement('button')
standbyBtn.classList.add('standby-btn', 'no-mobile-tap-outline')
standbyBtn.textContent = msgs.btnLabel_sendQueryToApp || `Send search query to ${app.name}`
appDiv.append(standbyBtn)
show.reply.standbyBtnClickHandler = function() {
appAlert('waitingResponse')
if (!browser.isMobile) appFooter.style.right = 0 // reset counteract right-offset bug from chatbar padding
msgChain.push({ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) })
show.reply.userInteracted = true ; show.reply.chatbarFocused = false
menus.pin.topPos = menus.pin.rightPos = null
get.reply(msgChain)
}
standbyBtn.onclick = show.reply.standbyBtnClickHandler
// Otherwise create/append answer bubble
} else {
const answerPre = document.createElement('pre'),
balloonTipSpan = document.createElement('span')
balloonTipSpan.className = 'balloon-tip'
appDiv.append(balloonTipSpan, answerPre)
}
update.tweaksStyle() // show/hide 'by KudoAI', update pre-height based on mode
}
// Build reply section if missing
if (!appDiv.querySelector('#app-chatbar')) {
// Init/clear reply section content/classes
const replySection = appDiv.querySelector('section') || document.createElement('section')
while (replySection.firstChild) replySection.removeChild(replySection.firstChild)
replySection.classList.remove('loading', 'no-user-select')
// Create/append section elems
const replyForm = document.createElement('form'),
continueChatDiv = document.createElement('div'),
chatTextarea = document.createElement('textarea')
continueChatDiv.className = 'continue-chat'
chatTextarea.id = 'app-chatbar' ; chatTextarea.rows = '1'
chatTextarea.placeholder = ( answer == 'standby' ? msgs.placeholder_askSomethingElse || 'Ask something else'
: msgs.tooltip_sendReply || 'Send reply' ) + '...'
continueChatDiv.append(chatTextarea)
replyForm.append(continueChatDiv) ; replySection.append(replyForm)
if (!browser.isMobile) appFooter.style.right = '-72px' // counteract right-offset bug from chatbar padding
appDiv.append(replySection);
// Create/append chatbar buttons
['send', 'shuffle'].forEach(btnType => {
const btnElem = document.createElement(btnType === 'send' ? 'button' : 'div')
btnElem.id = `${btnType}-btn` ; btnElem.classList.add('chatbar-btn', 'no-mobile-tap-outline')
btnElem.style.right = `${ btnType == 'send' ? ( browser.isFirefox ? 8 : 7 ) : ( browser.isFirefox ? 11.5 : 9.5 )}px`
btnElem.append(icons[btnType == 'send' ? 'arrowUp' : 'arrowsTwistedRight'].create())
continueChatDiv.append(btnElem)
})
// Add listeners
listenerize.replySection()
// Scroll to top on mobile if user interacted
if (browser.isMobile && show.reply.userInteracted) {
document.body.scrollTop = 0 // Safari
document.documentElement.scrollTop = 0 // Chromium/FF/IE
}
}
// Render/show answer if query sent
if (answer != 'standby') {
const answerPre = appDiv.querySelector('pre')
try { // to render markdown
answerPre.innerHTML = marked.parse(answer) } catch (err) { log.error(err.message) }
hljs.highlightAll() // highlight code
if (scheme == 'dark' && answerPre.firstChild?.tagName == 'P')
answerPre.firstChild.prepend('>> ') // since speech balloon tip missing
// Typeset math
answerPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons
codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';<br>') })
const elemsToRenderMathIn = [answerPre, ...answerPre.querySelectorAll('*')]
elemsToRenderMathIn.forEach(elem => {
renderMathInElement(elem, { // typeset math
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
{ left: '\\begin{equation}', right: '\\end{equation}', display: true },
{ left: '\\begin{align}', right: '\\end{align}', display: true },
{ left: '\\begin{alignat}', right: '\\end{alignat}', display: true },
{ left: '\\begin{gather}', right: '\\end{gather}', display: true },
{ left: '\\begin{CD}', right: '\\end{CD}', display: true },
{ left: '\\[', right: '\\]', display: true }
],
throwOnError: false
})})
if (config.stickySidebar) update.tweaksStyle() // to reset answerPre height
// Auto-scroll if active
if (config.autoScroll && !browser.isMobile && config.proxyAPIenabled && !config.streamingDisabled) {
if (config.stickySidebar || config.anchored) answerPre.scrollTop = answerPre.scrollHeight
else window.scrollBy({ top: appDiv.querySelector('#app-chatbar').getBoundingClientRect().bottom - window.innerHeight +12 })
}
}
// Focus chatbar conditionally
if (!show.reply.chatbarFocused // do only once
&& !browser.isMobile // exclude mobile devices to not auto-popup OSD keyboard
&& ((!config.autoFocusChatbarDisabled && ( config.anchored // include Anchored mode if AF enabled
|| ( appDiv.offsetHeight < window.innerHeight - appDiv.getBoundingClientRect().top ))) // ...or un-Anchored if fully above fold
|| (config.autoFocusChatbarDisabled && config.anchored && show.reply.userInteracted)) // ...or Anchored if AF disabled & user interacted
) { appDiv.querySelector('#app-chatbar').focus() ; show.reply.chatbarFocused = true }
// Update styles
if (config.anchored) update.appBottomPos() // restore minimized/restored state if anchored
update.chatbarWidth()
show.reply.userInteracted = false
},
related(queries) {
log.caller = 'show.related()'
if (get.reply.status == 'waiting') { // recurse until get.reply() finishes showing answer
setTimeout(() => show.related(queries), 500, queries) ; return }
// Re-get.related() if current reply is question to suggest answers
const currentReply = appDiv.querySelector('#ddgpt > pre')?.textContent.trim()
if (show.reply.src != 'shuffle' && !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 && !appDiv.querySelector('.related-queries')) {
// Create/classify/append parent div
const relatedQueriesDiv = document.createElement('div') ; relatedQueriesDiv.className = 'related-queries'
appDiv.append(relatedQueriesDiv)
// Fill each child div, add attributes + icon + listener
queries.forEach((query, idx) => {
const relatedQueryDiv = document.createElement('div'),
relatedQuerySVG = icons.arrowDownRight.create()
// Add attributes
relatedQueryDiv.title = msgs.tooltip_sendRelatedQuery || 'Send related query'
relatedQueryDiv.classList.add('related-query', 'fade-in', 'no-user-select', 'no-mobile-tap-outline')
relatedQueryDiv.setAttribute('tabindex', 0)
relatedQueryDiv.textContent = query
// Assemble/insert elems
relatedQueryDiv.prepend(relatedQuerySVG) ; relatedQueriesDiv.append(relatedQueryDiv)
// Add fade + listeners
setTimeout(() => {
relatedQueryDiv.classList.add('active')
relatedQueryDiv.onclick = relatedQueryDiv.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 relatedQuery = event.target.textContent, chatbar = appDiv.querySelector('textarea')
if (chatbar) {
chatbar.value = relatedQuery
if (/\[[^[\]]+\]/.test(relatedQuery)) { // highlight 1st bracket-enclosed placeholder
chatbar.focus()
listenerize.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 * 100)
})
get.related.replyIsQuestion = null
update.tweaksStyle() // to shorten <pre> max-height
if (!browser.isMobile) appFooter.style.right = 0 // reset show.reply()'s counteract right-offset bug from chatbar padding
}
}
}
// Run MAIN routine
log.debug('Registering toolbar menu...') ; registerMenu() ; log.debug('Success! Menu registered')
// Init ALERTS
const appAlerts = {
waitingResponse: `${ msgs.alert_waitingFor || 'Waiting for' } ${app.name} ${ msgs.alert_response || 'response' }...`,
login: `${ msgs.alert_login || 'Please login' } @ `,
checkCloudflare: `${ msgs.alert_checkCloudflare || 'Please pass Cloudflare security check' } @ `,
tooManyRequests: `${ msgs.alert_tooManyRequests || 'API is flooded with too many requests' }.`,
parseFailed: `${ msgs.alert_parseFailed || 'Failed to parse response JSON' }.`,
proxyNotWorking: `${ msgs.mode_proxy || 'Proxy Mode' } ${ msgs.alert_notWorking || 'is not working' }.`,
openAInotWorking: `OpenAI API ${ msgs.alert_notWorking || 'is not working' }.`,
suggestProxy: `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }`,
suggestOpenAI: `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOff || 'switching off' } ${ msgs.mode_proxy || 'Proxy Mode' }`
}
// Create/ID/classify/listenerize DDGPT container
const appDiv = document.createElement('div') ; appDiv.id = 'ddgpt'
appDiv.classList.add('fade-in') ; listenerize.appDiv()
// Stylize APP elems
const appStyle = createStyle() ; update.appStyle() ; document.head.append(appStyle);
['hljs', 'wsbg', 'bsbg'].forEach(cssType => // code highlighting, white stars, black stars
document.head.append(createStyle(GM_getResourceText(`${cssType}CSS`))))
// Stylize SITE elems
const tweaksStyle = createStyle(),
wsbStyles = 'section[data-area="mainline"] { max-width: 590px !important }' // max before centered mode changes
+ 'section[data-area="sidebar"] { max-width: 530px !important ; flex-basis: 530px !important }',
ssbStyles = '#ddgpt { position: sticky ; top: 14px }'
+ '#ddgpt ~ * { display: none }' // hide sidebar contents
+ 'body, div.site-wrapper { overflow: clip }', // replace `overflow: hidden` to allow stickiness
anchorStyles = '#ddgpt { position: fixed ; bottom: -7px ; right: 35px ; width: 388px }'
+ '[class*="feedback"], .related-queries, #wsb-btn { display: none }'
+ '#chevron-btn, #arrows-btn { display: block !important }',
expandedStyles = '#ddgpt { width: 528px }'
update.tweaksStyle() ; document.head.append(tweaksStyle)
// Create/stylize TOOLTIPs
if (!browser.isMobile) {
var tooltipDiv = document.createElement('div') ; tooltipDiv.classList.add('btn-tooltip', 'no-user-select')
document.head.append(createStyle('.btn-tooltip {'
+ 'background-color: rgba(0, 0, 0, 0.64) ; padding: 4px 6px ; border-radius: 6px ; border: 1px solid #d9d9e3 ;' // bubble style
+ 'font-size: 0.87em ; color: white ; fill: white ; stroke: white ;' // font/icon style
+ 'position: absolute ;' // for update.tooltip() calcs
+ 'box-shadow: 3px 5px 16px 0 rgb(0 0 0 / 21%) ;' // drop shadow
+ 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 1250 }' // visibility
))
}
// Create/classify/fill feedback FOOTER
const appFooter = document.createElement('footer')
appFooter.classList.add('fade-in', // DDGPT class
'feedback-prompt') // DDG class
let footerContent = createAnchor('#', msgs.link_shareFeedback || 'Share feedback', { target: '_self' })
footerContent.className = 'js-feedback-prompt-generic' // DDG footer class
footerContent.onclick = () => modals.feedback.show({ sites: 'feedback' })
appFooter.append(footerContent)
// APPEND DDGPT + footer to DDG
const appElems = [appFooter, appDiv]
const appDivContainer = await new Promise(resolve => {
new MutationObserver((_, obs) => {
const container = document.querySelector(browser.isMobile || isCentered ? '[data-area*="mainline"]' : '[class*="sidebar"]')
if (container) { obs.disconnect() ; resolve(container) }
}).observe(document.body, { childList: true, subtree: true })
})
appElems.forEach(elem => appDivContainer.prepend(elem))
appElems.reverse().forEach((elem, idx) => // fade in staggered
setTimeout(() => elem.classList.add('active'), idx * 550 - 200))
// REPLACE appDivContainer max-width w/ min-width for better UI
if (!browser.isMobile) { appDivContainer.style.maxWidth = '' ; appDivContainer.style.minWidth = '448px' }
// Check for active TEXT CAMPAIGNS to replace footer CTA
get.json('https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json',
(err, advertisersData) => { if (err) return
// Init vars
let chosenAdvertiser, adSelected
const re_appName = new RegExp(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
})()
// Select random, active advertiser
for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData))))
if (details.campaigns.text) { chosenAdvertiser = advertiser ; break }
// Fetch a random, active creative
if (chosenAdvertiser) {
const campaignsURL = 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/'
+ chosenAdvertiser + '/text/campaigns.json'
get.json(campaignsURL, (err, campaignsData) => { if (err) return
// Select random, active campaign
for (const [campaignName, campaign] of shuffle(applyBoosts(Object.entries(campaignsData)))) {
const campaignIsActive = campaign.active && (!campaign.endDate || currentDate <= campaign.endDate)
if (!campaignIsActive) continue // to next campaign since campaign inactive
// Select random active group
for (const [groupName, adGroup] of shuffle(applyBoosts(Object.entries(campaign.adGroups)))) {
// Skip disqualified groups
if (/^self$/i.test(groupName) && !re_appName.test(campaignName) // self-group for other apps
|| re_appName.test(campaignName) && !/^self$/i.test(groupName) // non-self group for this app
|| adGroup.active == false // group explicitly disabled
|| adGroup.targetBrowsers && // target browser(s) exist...
!adGroup.targetBrowsers.some( // ...but doesn't match user's
browser => new RegExp(browser, 'i').test(navigator.userAgent))
|| adGroup.targetLocations && ( // target locale(s) exist...
!config.userLocale || !adGroup.targetLocations.some( // ...and user locale is missing or excluded
loc => loc.includes(config.userLocale) || config.userLocale.includes(loc)))
) continue // to next group
// Filter out inactive ads, pick random active one
const activeAds = adGroup.ads.filter(ad => ad.active != false)
if (activeAds.length == 0) continue // to next group since no ads active
const chosenAd = activeAds[Math.floor(chatgpt.randomFloat() * activeAds.length)] // random active one
// Build destination URL
let destinationURL = chosenAd.destinationURL || adGroup.destinationURL
|| campaign.destinationURL || ''
if (destinationURL.includes('http')) { // insert UTM tags
const [baseURL, queryString] = destinationURL.split('?'),
queryParams = new URLSearchParams(queryString || '')
queryParams.set('utm_source', app.name.toLowerCase())
queryParams.set('utm_content', 'app_footer_link')
destinationURL = baseURL + '?' + queryParams.toString()
}
// Update footer content
footerContent.setAttribute('class', '') // reset for re-fade
const newFooterContent = destinationURL ? createAnchor(destinationURL)
: document.createElement('span')
footerContent.replaceWith(newFooterContent) ; footerContent = newFooterContent
footerContent.classList.add('fade-in', // DDGPT fade class
'js-feedback-prompt-generic') // DDG footer class
footerContent.textContent = chosenAd.text
footerContent.setAttribute('title', chosenAd.tooltip || '')
setTimeout(() => footerContent.classList.add('active'), 100) // to trigger fade
adSelected = true ; break
}
if (adSelected) break // out of campaign loop after ad selection
}})}
function shuffle(list) {
let currentIdx = list.length, tempValue, randomIdx
while (currentIdx != 0) { // elements remain to be shuffled
randomIdx = Math.floor(chatgpt.randomFloat() * currentIdx) ; currentIdx -= 1
tempValue = list[currentIdx] ; list[currentIdx] = list[randomIdx] ; list[randomIdx] = tempValue
}
return list
}
function applyBoosts(list) {
let boostedList = [...list],
boostedListLength = boostedList.length - 1 // for applying multiple boosts
list.forEach(([name, data]) => { // check for boosts
if (data.boost) { // boost flagged entry's selection probability
const boostPercent = parseInt(data.boost, 10) / 100,
entriesNeeded = Math.ceil(boostedListLength / (1 - boostPercent)) // total entries needed
* boostPercent - 1 // reduced to boosted entries needed
for (let i = 0 ; i < entriesNeeded ; i++) boostedList.push([name, data]) // saturate list
boostedListLength += entriesNeeded // update for subsequent calculations
}})
return boostedList
}
})
// 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)
// Show STANDBY mode or get/show ANSWER
let msgChain = [{ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) }]
if (!config.autoGet && !/src=(?:first-run|asktip)/.test(location.href) // Auto-Get disabled and not queried from other site or 1st run
|| config.prefixEnabled && !/.*q=%2F/.test(document.location) // prefix required but not present
|| config.suffixEnabled && !/.*q=.*(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(document.location)) { // suffix required but not present
show.reply('standby')
if (!config.rqDisabled)
get.related(stripQueryAugments(msgChain)[msgChain.length - 1].content).then(queries => show.related(queries))
.catch(err => { log.error(err.message) ; api.tryNew(get.related) })
} else { appAlert('waitingResponse') ; get.reply(msgChain) }
// Add key listener to DISMISS modals
document.onkeydown = modals.keyHandler
// Observe for DDG SCHEME CHANGES to update DDGPT scheme if auto-scheme mode
new MutationObserver(handleSchemeChange).observe( // class changes from DDG appearance settings
document.documentElement, { attributes: true, attributeFilter: ['class'] })
function handleSchemeChange() {
if (config.scheme) return // since light/dark hard-set
const newScheme = chatgpt.isDarkMode() ? 'dark' : 'light'
if (newScheme != scheme) update.scheme(newScheme)
}
})()