Greasy Fork is available in English.

DuckDuckGPT 🤖

Přidává AI odpovědi do DuckDuckGo (poháněno GPT-4o!)

Nainstalovat skript?
Skript doporučený autorem

Mohlo by se vám také líbit Vymazat ChatGPT Historii 🕶️.

Nainstalovat skript
// ==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             2024.6.30.13
// @license             MIT
// @icon                https://media.ddgpt.com/images/icons/duckduckgpt/icon48.png?af89302
// @icon64              https://media.ddgpt.com/images/icons/duckduckgpt/icon64.png?af89302
// @compatible          chrome except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible          firefox
// @compatible          edge except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible          opera after allowing userscript manager access to search page results in opera://extensions
// @compatible          brave except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible          vivaldi
// @compatible          waterfox
// @compatible          librewolf
// @compatible          ghost
// @compatible          qq
// @compatible          whale
// @compatible          kiwi
// @compatible          mask
// @compatible          orion
// @match               *://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             sogou.com
// @require             https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@2.9.3/dist/chatgpt.min.js#sha256-EDN+mCc+0Y4YVzJEoNikd4/rAIaJDLAdb+erWvupXTM=
// @require             https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require             https://cdn.jsdelivr.net/npm/generate-ip@2.4.2/dist/generate-ip.min.js#sha256-PRvQIDVWK/a+aAqEFVQv7RePbRe/tX6tWQVM80rAe2M=
// @require             https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha256-g3pvpbDHNrUrveKythkPMF2j/J7UFoHbUyFQcFe1yEY=
// @require             https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require             https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require             https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @resource hljsCSS    https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css#sha256-v0N76BFFkH0dCB8bUr4cHSVN8A/zCaOopMuSmJWV/5w=
// @resource bsbgCSS    https://cdn.jsdelivr.net/gh/KudoAI/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 BROWSER FLAGS
    const isChrome = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Chrome'),
          isFirefox = chatgpt.browser.isFirefox(),
          isEdge = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Edge'),
          isBrave = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Brave'),
          isMobile = chatgpt.browser.isMobile()

    // Init CONFIG
    const config = {
        appName: 'DuckDuckGPT', appSymbol: '🤖', keyPrefix: 'duckDuckGPT',
        appURL: 'https://www.duckduckgpt.com', gitHubURL: 'https://github.com/KudoAI/duckduckgpt',
        greasyForkURL: 'https://greasyfork.org/scripts/459849-duckduckgpt',
        minFontSize: 13, maxFontSize: 24, lineHeightRatio: 1.28,
        latestAssetCommitHash: '5cdd663' } // for cached messages.json
    config.updateURL = config.greasyForkURL.replace('https://', 'https://update.')
        .replace(/(\d+)-?([a-zA-Z-]*)$/, (_, id, name) => `${ id }/${ !name ? 'script' : name }.meta.js`)
    config.supportURL = config.gitHubURL + '/issues/new'
    config.assetHostURL = config.gitHubURL.replace('github.com', 'cdn.jsdelivr.net/gh') + `@${config.latestAssetCommitHash}/`
    config.userLanguage = chatgpt.getUserLanguage()
    config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
    loadSetting('autoGet', 'autoFocusChatbarDisabled', 'autoScroll', 'bgAnimationsDisabled', 'fontSize',
                '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', 16.4) // init reply font size if unset
    if ( // disable streaming in unspported envs
        !/Tampermonkey|ScriptCat/.test(getUserscriptManager()) // unsupported userscript manager
        || getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave) // TM in browser that triggers STATUS_ACCESS_VIOLATION
    ) saveSetting('streamingDisabled', true)
    if (!config.notFirstRun && isMobile) saveSetting('autoGet', true) // reverse default auto-get disabled if mobile
    saveSetting('notFirstRun', true)

    // Init UI VARS
    let scheme = config.scheme || ( chatgpt.isDarkMode() ? 'dark' : 'light' )
    const isCentered = isCenteredMode()

    // Init FETCHER
    const xhr = getUserscriptManager() == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest
          
    // Init API props
    const openAIendpoints = { auth: 'https://auth0.openai.com', session: 'https://chatgpt.com/api/auth/session' }
    const apis = {
        'AIchatOS': {
            endpoint: 'https://api.binjie.fun/api/generateStream', expectedOrigin: 'https://chat18.aichatos8.com',
            method: 'POST', streamable: true, accumulatesText: false, failFlags: ['很抱歉地', '系统公告'] },
        'GPTforLove': {
            endpoint: 'https://api11.gptforlove.com/chat-process', expectedOrigin: 'https://ai27.gptforlove.com',
            method: 'POST', streamable: true, accumulatesText: true },
        'MixerBox AI': {
            endpoint: 'https://chatai.mixerbox.com/api/chat/stream', expectedOrigin: 'https://chatai.mixerbox.com',
            method: 'POST', streamable: true, accumulatesText: false },
        'OpenAI': {
            endpoint: 'https://api.openai.com/v1/chat/completions', expectedOrigin: 'https://chatgpt.com',
            method: 'POST', streamable: true }
    }
    const apiIDs = { gptForLove: { parentID: '' }, aiChatOS: { userID: '#/chat/' + Date.now() }}

    // Init INPUT EVENTS
    const inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
          inputEvents[action] = ( window.PointerEvent ? 'pointer' : isMobile ? 'touch' : 'mouse' ) + action)

    // Init MESSAGES
    let msgs = {}
    const msgsLoaded = new Promise(resolve => {
        const msgHostDir = config.assetHostURL + 'greasemonkey/_locales/',
              msgLocaleDir = ( config.userLanguage ? config.userLanguage.replace('-', '_') : 'en' ) + '/'
        let msgHref = msgHostDir + msgLocaleDir + 'messages.json', msgXHRtries = 0
        xhr({ method: 'GET', url: msgHref, onload: onLoad })
        function onLoad(resp) {
            try { // to return localized messages.json
                const msgs = JSON.parse(resp.responseText), flatMsgs = {}
                for (const key in msgs)  // remove need to ref nested keys
                    if (typeof msgs[key] == 'object' && 'message' in msgs[key])
                        flatMsgs[key] = msgs[key].message
                resolve(flatMsgs)
            } catch (err) { // if bad response
                msgXHRtries++ ; if (msgXHRtries == 3) return resolve({}) // try up to 3X (original/region-stripped/EN) only
                msgHref = config.userLanguage.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
                    msgHref.replace(/([^_]+_[^_]+)_[^/]*(\/.*)/, '$1$2') // ...strip region before retrying
                        : ( msgHostDir + 'en/messages.json' ) // else use default English messages
                xhr({ method: 'GET', url: msgHref, onload: onLoad })
            }
        }
    }) ; if (!config.userLanguage.startsWith('en')) try { msgs = await msgsLoaded } catch (err) {}

    // Init SETTINGS props
    const settingsProps = {
        proxyAPIenabled: { type: 'toggle',
            label: msgs.menuLabel_proxyAPImode || 'Proxy API Mode',
            helptip: msgs.helptip_proxyAPImode || 'Uses a Proxy API for no-login access to AI' },
        streamingDisabled: { type: 'toggle',
            label: msgs.mode_streaming || 'Streaming Mode',
            helptip: msgs.helptip_streamingMode || 'Receive replies in a continuous text stream' },
        autoGet: { type: 'toggle',
            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,
            label: msgs.menuLabel_autoFocusChatbar || 'Auto-Focus Chatbar',
            helptip: msgs.helptip_autoFocusChatbar || 'Auto-focus chatbar whenever it appears' },
        autoScroll: { type: 'toggle', mobile: false,
            label: `${ msgs.mode_autoScroll || 'Auto-Scroll' } (${ msgs.menuLabel_whenStreaming || 'when streaming' })`,
            helptip: msgs.helptip_autoScroll || 'Auto-scroll responses as they generate in Streaming Mode' },
        rqDisabled: { type: 'toggle',
            label: `${ msgs.menuLabel_show || 'Show' } ${ msgs.menuLabel_relatedQueries || 'Related Queries' }`,
            helptip: msgs.helptip_showRelatedQueries || 'Show related queries below chatbar' },
        prefixEnabled: { type: 'toggle',
            label: `${ msgs.menuLabel_require || 'Require' } "/" ${ msgs.menuLabel_beforeQuery || 'before query' }`,
            helptip: msgs.helptip_prefixMode || 'Require "/" before queries for answers to show' },
        suffixEnabled: { type: 'toggle',
            label: `${ msgs.menuLabel_require || 'Require' } "?" ${ msgs.menuLabel_afterQuery || 'after query' }`,
            helptip: msgs.helptip_suffixMode || 'Require "?" after queries for answers to show' },
        widerSidebar: { type: 'toggle', mobile: false, centered: false,
            label: msgs.menuLabel_widerSidebar || 'Wider Sidebar',
            helptip: msgs.helptip_widerSidebar || 'Horizontally expand search page sidebar' },
        stickySidebar: { type: 'toggle', mobile: false, centered: false,
            label: msgs.menuLabel_stickySidebar || 'Sticky Sidebar',
            helptip: msgs.helptip_stickySidebar || 'Makes DuckDuckGPT visible in sidebar even as you scroll' },
        bgAnimationsDisabled: { type: 'toggle',
            label: msgs.menuLabel_bgAnimations || 'Background Animations',
            helptip: msgs.helptip_bgAnimations || 'Show animated backgrounds in UI components' },
        replyLanguage: { type: 'prompt',
            label: msgs.menuLabel_replyLanguage || 'Reply Language',
            helptip: msgs.helptip_replyLanguage || 'Language for DuckDuckGPT to reply in' },
        scheme: { type: 'modal',
            label: msgs.menuLabel_colorScheme || 'Color Scheme',
            helptip: msgs.helptip_colorScheme || 'Scheme to display DuckDuckGPT UI components in' },
        about: { type: 'modal',
            label: `${ msgs.menuLabel_about || 'About' } ${config.appName}...` }
    }

    // Init MENU objs
    const menuIDs = [] // to store registered cmds for removal while preserving order
    const menuState = {
        symbol: ['❌', '✔️'], separator: getUserscriptManager() == 'Tampermonkey' ? ' — ' : ': ',
        word: [(msgs.state_off || 'Off').toUpperCase(), (msgs.state_on || 'On').toUpperCase()]
    }

    // Define SCRIPT functions

    function loadSetting(...keys) { keys.forEach(key => config[key] = GM_getValue(config.keyPrefix + '_' + key, false)) }
    function saveSetting(key, value) { GM_setValue(config.keyPrefix + '_' + key, value) ; config[key] = value }
    function safeWindowOpen(url) { window.open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
    function getUserscriptManager() { try { return GM_info.scriptHandler } catch (err) { return 'other' }}

    // Define MENU functions

    function registerMenu() {

        // Add command to toggle proxy API mode
        const pmLabel  = menuState.symbol[+config.proxyAPIenabled] + ' '
                       + settingsProps.proxyAPIenabled.label + ' '
                       + menuState.separator + menuState.word[+config.proxyAPIenabled]
        menuIDs.push(GM_registerMenuCommand(pmLabel, toggle.proxyMode))

        // Add command to launch About modal
        const aboutLabel = `💡 ${settingsProps.about.label}`
        menuIDs.push(GM_registerMenuCommand(aboutLabel, modals.about.show))

        // Add command to launch Settings modal
        const settingsLabel = `⚙️ ${ msgs.menuLabel_settings || 'Settings' }`
        menuIDs.push(GM_registerMenuCommand(settingsLabel, modals.settings.show))
    }

    function promptReplyLang() {
        while (true) {
            let replyLanguage = prompt(
                ( msgs.prompt_updateReplyLang || 'Update reply language' ) + ':', config.replyLanguage)
            if (replyLanguage == null) break // user cancelled so do nothing
            else if (!/\d/.test(replyLanguage)) {
                replyLanguage = ( // auto-case for menu/alert aesthetics
                    [2, 3].includes(replyLanguage.length) || replyLanguage.includes('-') ? replyLanguage.toUpperCase()
                      : replyLanguage.charAt(0).toUpperCase() + replyLanguage.slice(1).toLowerCase() )
                saveSetting('replyLanguage', replyLanguage || config.userLanguage)
                siteAlert(( msgs.alert_langUpdated || 'Language updated' ) + '!', // title
                    `${ config.appName } ${ msgs.alert_willReplyIn || 'will reply in' } `
                        + ( replyLanguage || msgs.alert_yourSysLang || 'your system language' ) + '.',
                    '', '', 330) // confirmation width
                if (modals.settings.get()) // update settings menu status label
                    document.querySelector('#replyLanguage-menu-entry span').textContent = replyLanguage
                break
    }}}

    function refreshMenu() {
        if (getUserscriptManager() == 'OrangeMonkey') return
        for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu()
    }

    function updateCheck() {

        // Fetch latest meta
        const currentVer = GM_info.script.version
        xhr({
            method: 'GET', url: config.updateURL + '?t=' + Date.now(),
            headers: { 'Cache-Control': 'no-cache' },
            onload: resp => { const updateAlertWidth = 409

                // Compare versions
                const latestVer = /@version +(.*)/.exec(resp.responseText)[1]
                for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
                    const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
                          latestSubVer = parseInt(latestVer.split('.')[i], 10) || 0
                    if (currentSubVer > latestSubVer) break // out of comparison since not outdated
                    else if (latestSubVer > currentSubVer) { // if outdated

                        // Alert to update
                        const updateModalID = siteAlert(( msgs.alert_updateAvail || 'Update available' ) + '! 🚀', // title
                            `${ msgs.alert_newerVer || 'An update to' } ${ config.appName } `
                                + `(v${ latestVer }) ${ msgs.alert_isAvail || 'is available' }!  `
                                + '<a target="_blank" rel="noopener" style="font-size: 1.1rem" '
                                    + 'href="' + config.gitHubURL + '/commits/main/greasemonkey/'
                                    + config.updateURL.replace(/.*\/(.*)meta\.js/, '$1user.js') + '"'
                                    + `>${ msgs.link_viewChanges || 'View changes' }</a>`,
                            function update() { // button
                                safeWindowOpen(config.updateURL.replace('meta.js', 'user.js') + '?t=' + Date.now())
                            }, '', updateAlertWidth
                        )

                        // Localize button labels if needed
                        if (!config.userLanguage.startsWith('en')) {
                            const updateAlert = document.querySelector(`[id="${ updateModalID }"]`),
                                  updateBtns = updateAlert.querySelectorAll('button')
                            updateBtns[1].textContent = msgs.buttonLabel_update || 'Update'
                            updateBtns[0].textContent = msgs.buttonLabel_dismiss || 'Dismiss'
                        }

                        return
                }}

                // Alert to no update found, nav back
                siteAlert(( msgs.alert_upToDate || 'Up-to-date' ) + '!', // title
                    `${ config.appName } (v${ currentVer }) ${ msgs.alert_isUpToDate || 'is up-to-date' }!`, // msg
                        '', '', updateAlertWidth)
                modals.about.show()
    }})}

    // Define FEEDBACK functions

    function notify(msg, position = '', notifDuration = '', shadow = '') {
        chatgpt.notify(`${ config.appSymbol } ${ msg }`, position, notifDuration,
            shadow || scheme == 'dark' ? '' : 'shadow' )
    }

    function siteAlert(title = '', msg = '', btns = '', checkbox = '', width = '') {
        return chatgpt.alert(`${ config.appSymbol } ${ title }`, msg, btns, checkbox, width )}

    function appAlert(...alerts) {
        alerts = alerts.flat() // flatten array args nested by spread operator
        while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear appDiv content
        const alertP = document.createElement('p')
        alertP.id = '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)) deleteOpenAIcookies()
            if (msg.includes(appAlerts.waitingResponse)) alertP.classList.add('loading')

            // Add login link to login msgs
            if (msg.includes('@'))
                msg += '<a class="alert-link" target="_blank" rel="noopener" href="https://chatgpt.com">chatgpt.com</a>,'
                     + ` ${ msgs.alert_thenRefreshPage || 'then refresh this page' }.`
                     + ` (${ msgs.alert_ifIssuePersists || 'If issue persists' },`
                     + ` ${( msgs.alert_try || 'Try' ).toLowerCase() }`
                     + ` ${ msgs.alert_switchingOn || 'switching on' }`
                     + ` ${ msgs.mode_proxy || 'Proxy Mode' })`

            // Hyperlink msgs.alert_switching<On|Off>
            const foundState = ['On', 'Off'].find(state =>
                msg.includes(msgs['alert_switching' + state]) || new RegExp(`\\b${state}\\b`, 'i').test(msg))
            if (foundState) { // hyperlink switch phrase for click listener to toggle.proxyMode()
                const switchPhrase = msgs['alert_switching' + foundState] || 'switching ' + foundState.toLowerCase()
                msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
            }

            // Create/fill/append msg span
            const msgSpan = document.createElement('span')
            msgSpan.innerHTML = msg ; alertP.append(msgSpan)

            // Activate toggle link if necessary
            msgSpan.querySelector('[href="#"]')?.addEventListener('click', toggle.proxyMode)
        })
        appDiv.append(alertP)
    }

    function consoleInfo(msg) { console.info(`${ config.appSymbol } ${ config.appName } » ${ msg }`) }
    function consoleErr(label, msg) { console.error(`${config.appSymbol} ${config.appName} » ${label}${ msg ? `: ${msg}` : '' }`)}

    // Define MODAL functions

    const modals = {
        init(modal) {
            modal.classList.add('.ddgpt-modal')
            modal.onwheel = event => event.preventDefault() // disable wheel-scrolling
        },

        about: {
            show() {
                if (modals.settings.get()) modals.settings.hide()

                // Create/init modal
                const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
                const aboutModalID = siteAlert(
                    config.appName, // title
                    '🏷️ ' + ( msgs.about_version || 'Version' ) + ': ' + GM_info.script.version + '\n'
                        + '⚡ ' + ( msgs.about_poweredBy || 'Powered by' ) + ': '
                            + '<a href="https://chatgpt.js.org" target="_blank" rel="noopener">chatgpt.js</a>'
                            + ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
                        + '📜 ' + ( msgs.about_sourceCode || 'Source code' ) + ':\n '
                            + `<a href="${ config.gitHubURL }" target="_blank" rel="nopener">`
                                + config.gitHubURL + '</a>',
                    [ // buttons
                        function checkForUpdates() { updateCheck() },
                        function getSupport() { safeWindowOpen(config.supportURL) },
                        function leaveAReview() { modals.feedback.show() },
                        function moreChatGPTapps() { safeWindowOpen('https://github.com/adamlui/chatgpt-apps') }
                    ], '', 527) // modal width
                const aboutModal = document.getElementById(aboutModalID).firstChild
                modals.init(aboutModal) // add class, disable wheel-scrolling

                // Resize/format buttons to include emoji + localized label + hide Dismiss button
                for (const btn of aboutModal.querySelectorAll('button')) {
                    btn.style.height = '52px' // re-size to fit meaty text content
                    if (/updates/i.test(btn.textContent)) btn.textContent = (
                        '🚀 ' + ( msgs.buttonLabel_updateCheck || 'Check for Updates' ))
                    else if (/support/i.test(btn.textContent)) btn.textContent = (
                        '🧠 ' + ( msgs.buttonLabel_getSupport || 'Get Support' ))
                    else if (/review/i.test(btn.textContent)) btn.textContent = (
                        '⭐ ' + ( msgs.buttonLabel_leaveReview || 'Leave a Review' ))
                    else if (/apps/i.test(btn.textContent)) btn.textContent = (
                        '🤖 ' + ( msgs.buttonLabel_moreApps || 'More ChatGPT Apps' ))
                    else btn.style.display = 'none' // hide Dismiss button
            }}
        },

        feedback: {
            show() {

                // Create/init modal
                const feedbackModalID = siteAlert(`${
                    msgs.alert_choosePlatform || 'Choose a platform' }:`, '',
                    [ // buttons
                        function greasyFork() { safeWindowOpen(
                            config.greasyForkURL + '/feedback#post-discussion') },
                        function github() { safeWindowOpen(
                            config.gitHubURL + '/discussions/new/choose') },
                        function productHunt() { safeWindowOpen(
                            'https://www.producthunt.com/products/duckduckgpt/reviews/new') },
                        function futurepedia() { safeWindowOpen(
                            'https://www.futurepedia.io/tool/duckduckgpt#duckduckgpt-review') },
                        function alternativeTo() { safeWindowOpen(
                            'https://alternativeto.net/software/duckduckgpt/about/') }
                    ], '', 408) // modal width
                const feedbackModal = document.getElementById(feedbackModalID).firstChild
                modals.init(feedbackModal) // add class, disable wheel-scrolling

                // Re-style button cluster
                const buttons = feedbackModal.querySelector('.modal-buttons')
                buttons.style.cssText += 'display: flex ; flex-wrap: wrap ; justify-content: center ;'
                                       + ' margin-top: 14px !important' // close gap between title/btns

                // Format button labels + add v-padding
                buttons.querySelectorAll('button').forEach((btn, idx) => {
                    if (idx == 0) btn.style.display = 'none' // hide Dismiss button
                    else if (btn.textContent == 'Github') btn.textContent = 'GitHub'
                    else if (btn.textContent == 'Alternative To') btn.textContent = 'AlternativeTo'
                    btn.style.marginTop = btn.style.marginBottom = '5px' // v-pad btns
                })
            }
        },

        scheme: {
            show() {

                // Create/init modal
                const schemeModalID = siteAlert(`${
                    config.appName } ${( msgs.menuLabel_colorScheme || 'Color Scheme' ).toLowerCase() }:`, '',
                    [ // buttons
                        function auto() { updateScheme('auto') },
                        function light() { updateScheme('light') },
                        function dark() { updateScheme('dark') }
                ])
                const schemeModal = document.getElementById(schemeModalID).firstChild
                modals.init(schemeModal) // add class, disable wheel-scrolling

                // Center button cluster
                schemeModal.querySelector('.modal-buttons').style.justifyContent = 'center'

                // Re-format each button
                const buttons = schemeModal.querySelectorAll('button'),
                      schemes = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'}
                for (const btn of buttons) {
                    const btnScheme = btn.textContent.toLowerCase()

                    // Emphasize active scheme
                    btn.classList = (
                        config.scheme == btn.textContent.toLowerCase() || (btn.textContent == 'Auto' && !config.scheme)
                          ? 'primary-modal-btn' : '' )

                    // Prepend emoji + localize labels
                    if (Object.prototype.hasOwnProperty.call(schemes, btnScheme))
                        btn.textContent = `${schemes[btnScheme]} ${ // emoji
                            msgs['scheme_' + btnScheme] || msgs['menuLabel_' + btnScheme] || btnScheme.toUpperCase() }`
                    else btn.style.display = 'none' // hide Dismiss button

                    // Clone button to replace listener to not dismiss modal on click
                    const newBtn = btn.cloneNode(true) ; btn.parentNode.replaceChild(newBtn, btn)
                    newBtn.onclick = event => {
                        event.stopPropagation() // disable chatgpt.js dismissAlert()
                        updateScheme(btnScheme) // call corresponding scheme func
                        schemeModal.querySelectorAll('button').forEach(btn => btn.classList = '') // clear prev emphasized active scheme
                        newBtn.classList = 'primary-modal-btn' // emphasize newly active scheme
                        newBtn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis
                        setTimeout(() => { newBtn.style.pointerEvents = 'auto'; }, 100) // re-enable hover fx after 100ms to flicker emphasis
                    }
                }

                function updateScheme(newScheme) {
                    scheme = newScheme == 'auto' ? ( chatgpt.isDarkMode() ? 'dark' : 'light' ) : newScheme
                    saveSetting('scheme', newScheme == 'auto' ? false : newScheme)
                    if (modals.settings.get()) // update Settings menu status label
                        document.querySelector('#scheme-menu-entry span').textContent = newScheme
                    updateAppLogoSrc() ; updateAppStyle() ; updateStars() ; schemeNotify(newScheme)
                }

                function schemeNotify(scheme) {
                    notify(` ${ msgs.menuLabel_colorScheme || 'Color Scheme' }: `
                           + ( scheme == 'light' ? msgs.scheme_light   || 'Light' :
                               scheme == 'dark'  ? msgs.scheme_dark    || 'Dark'
                                                 : msgs.menuLabel_auto || 'Auto' ).toUpperCase()
                )}
            }
        },

        settings: {

            clickHandler(event) {
                if (event.target == event.currentTarget || event.target instanceof SVGPathElement)
                    modals.settings.hide()
            },

            createAppend() {

                // Init core elems
                const settingsContainer = document.createElement('div') ; settingsContainer.id = 'ddgpt-settings-bg'
                settingsContainer.classList = 'no-user-select'
                const settingsModal = document.createElement('div') ; settingsModal.id = 'ddgpt-settings'
                modals.init(settingsModal) // add class, disable wheel-scrolling
                const settingsIcon = icons.ddgpt.create()
                settingsIcon.style.cssText = 'width: 56px ; position: relative ; top: -33px ; margin: 0 41% -12px' // size/pos icon
                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)
                const settingsList = document.createElement('ul')

                // Create/append setting icons/labels/toggles
                Object.keys(settingsProps).forEach((key, idx) => {
                    const setting = settingsProps[key]
                    if (isMobile && setting.mobile == false || isCentered && setting.centered == false) return

                    // Create/append item/label elems
                    const settingItem = document.createElement('li') ; settingItem.id = key + '-menu-entry'
                    settingItem.title = setting.helptip || '' // for hover assistance
                    const settingLabel = document.createElement('label') ; settingLabel.textContent = setting.label
                    settingItem.append(settingLabel) ; settingsList.append(settingItem)

                    // Create/prepend icons
                    let settingIcon
                    if (key == 'proxyAPIenabled') {
                        settingIcon = icons.sunglasses.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -0.5px ; margin-right: 9px'
                    } else if (key == 'streamingDisabled') {
                        settingIcon = icons.signalStream.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: 0.5px ; margin-right: 9px'
                    } else if (key == 'autoGet') {
                        settingIcon = icons.autoSpeechBalloon.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
                    } else if (key == 'autoFocusChatbarDisabled') {
                        settingIcon = icons.inwardCarets.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
                    } else if (key == 'autoScroll') {
                        settingIcon = icons.downArrows.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3.5px ; left: -1.5px ; margin-right: 6px'
                    } else if (key == 'rqDisabled') {
                        settingIcon = icons.speechBalloon.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px ; transform: scaleY(-1)'
                    } else if (key == 'prefixEnabled') {
                        settingIcon = icons.slash.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px'
                    } else if (key == 'suffixEnabled') {
                        settingIcon = icons.questionMark.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7px'
                    } else if (key == 'widerSidebar') {
                        settingIcon = icons.widescreen.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7.5px'
                    } else if (key == 'stickySidebar') {
                        settingIcon = icons.pin.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 7.5px'
                    } else if (key.includes('bgAnimation')) {
                        settingIcon = icons.sparkles.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 6.5px'
                    } else if (key == 'replyLanguage') {
                        settingIcon = icons.language.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 9px'
                    } else if (key == 'scheme') {
                        settingIcon = icons.scheme.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: -1.5px ; margin-right: 8px'
                    } else if (key == 'about') {
                        settingIcon = icons.about.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -3px ; margin-right: 5.5px'
                    }
                    settingItem.prepend(settingIcon)

                    // Create/append toggles/listeners
                    if (setting.type == 'toggle') {

                        // Init toggle input
                        const settingToggle = document.createElement('input'),
                              settingToggleAttrs = [['type', 'checkbox'], ['disabled', true]]
                        settingToggleAttrs.forEach(([attr, value]) => settingToggle.setAttribute(attr, value))
                        settingToggle.checked = config[key] ^ key.includes('Disabled')
                        settingToggle.style.display = 'none' // hide checkbox

                        // Create/stylize switch
                        const switchSpan = document.createElement('span')
                        const switchStyles = {
                            position: 'relative', left: '-1px', bottom:'-5.5px', float: 'right',
                            backgroundColor: settingToggle.checked ? '#ccc' : '#AD68FF', // init opposite  final color
                            width: '26px', height: '13px', '-webkit-transition': '.4s', transition: '0.4s',  borderRadius: '28px'
                        }
                        Object.assign(switchSpan.style, switchStyles)

                        // Create/stylize knob
                        const knobSpan = document.createElement('span')
                        const knobWidth = 11
                        const knobStyles = {
                            position: 'absolute', left: '1px', bottom: '1px',
                            width: `${ knobWidth }px`, height: `${ knobWidth }px`, content: '""', borderRadius: '28px',
                            transform: settingToggle.checked ? // init opposite final pos
                                'translateX(0)' : 'translateX(14px) translateY(0)',
                            backgroundColor: 'white',  '-webkit-transition': '0.2s', transition: '0.2s'
                        }
                        Object.assign(knobSpan.style, knobStyles)

                        // Append elems
                        switchSpan.append(knobSpan) ; settingItem.append(settingToggle, switchSpan)

                        // Update visual state w/ animation
                        setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), idx *25 -25)

                        // Add click listener
                        settingItem.onclick = () => {
                            modals.settings.toggle.switch(settingToggle) // visually switch toggle

                            // Call specialized toggle funcs
                            if (key.includes('proxy')) toggle.proxyMode()
                            else if (key.includes('streaming')) toggle.streaming()
                            else if (key.includes('rq')) toggle.relatedQueries()
                            else if (key.includes('Sidebar')) toggle.sidebar(key.match(/(.*?)Sidebar$/)[1])
                            else if (key.includes('Animations')) toggle.bgAnimations()

                            // ...or generically toggle/notify
                            else {
                                saveSetting(key, !config[key]) // update config
                                notify(`${settingsProps[key].label} ${menuState.word[+key.includes('Disabled') ^ +config[key]]}`)
                            }
                        }

                    // Add config status + listeners to pop-up settings
                    } else {
                        const configStatusSpan = document.createElement('span')
                        configStatusSpan.style.cssText = 'float: right ; font-size: 11px ; margin-top: 3px ;'
                            + ( !key.includes('about') ? 'text-transform: uppercase !important' : '' )
                        if (key.includes('replyLang')) {
                            configStatusSpan.textContent = config.replyLanguage
                            settingItem.onclick = promptReplyLang
                        } else if (key.includes('scheme')) {
                            configStatusSpan.textContent = config.scheme || 'Auto'
                            settingItem.onclick = modals.scheme.show
                        } else if (key.includes('about')) {
                            configStatusSpan.textContent = `v${GM_info.script.version}`
                            settingItem.onclick = modals.about.show
                        } settingItem.append(configStatusSpan)
                    }
                })

                // Create close button
                const closeBtn = document.createElement('div') ; closeBtn.id = 'ddgpt-settings-close-btn'
                closeBtn.title = msgs.tooltip_close || 'Close'
                const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
                const closeSVGattrs = [['height', '8px'], ['viewBox', '0 0 14 14'], 'fill', 'none']
                closeSVGattrs.forEach(([attr, val]) => closeSVG.setAttribute(attr, val))
                const closeSVGpath = createSVGpath({
                    d: 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z' })
                closeSVG.append(closeSVGpath) ; closeBtn.append(closeSVG)

                // Assemble/append elems
                settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsList)
                settingsContainer.append(settingsModal) ; document.body.append(settingsContainer)

                // Add listeners to dismiss modal
                const dismissElems = [settingsContainer, closeBtn, closeSVG]
                dismissElems.forEach(elem => elem.onclick = modals.settings.clickHandler)
                document.onkeydown = modals.settings.keyHandler

                return settingsContainer
            },

            get() { return document.getElementById('ddgpt-settings') },

            hide() {
                const settingsContainer = modals.settings.get()?.parentNode
                if (!settingsContainer) return
                settingsContainer.style.animation = 'alert-zoom-fade-out 0.075s ease-out' // chatgpt.js keyframes
                setTimeout(() => settingsContainer.remove(), 50) // delay for fade-out
            },

            keyHandler() {
                const dismissKeys = ['Escape', 'Esc'], dismissKeyCodes = [27]
                if (dismissKeys.includes(event.key) || dismissKeyCodes.includes(event.keyCode)) {
                    const settingsModal = modals.settings.get()
                    if (settingsModal && settingsModal.style.display != 'none'
                        && (event.key.includes('Esc') || event.keyCode == 27))
                            modals.settings.hide()
                }
            },

            show() {
                const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
                settingsContainer.style.display = ''
                if (isMobile) { // scale 93% to viewport sides
                    const settingsModal = settingsContainer.querySelector('#ddgpt-settings'),
                          scaleRatio = 0.93 * window.innerWidth / settingsModal.offsetWidth
                    settingsModal.style.transform = `scale(${scaleRatio})`
                }
                setTimeout(() => { // delay non-0 opacity's for transition fx
                    settingsContainer.style.backgroundColor = ( 
                        `rgba(67, 70, 72, ${ scheme === 'dark' ? 0.62 : 0.33 })`)
                    settingsContainer.classList.add('animated'); },
                100)
            },

            toggle: {
                switch(settingToggle) {
                    settingToggle.checked = !settingToggle.checked    
                    modals.settings.toggle.updateStyles(settingToggle)        
                },

                updateStyles(settingToggle) { // for .toggle.show() + staggered switch animations in .createAppend()
                    const switchSpan = settingToggle.parentNode.querySelector('span'),
                          knobSpan = switchSpan.querySelector('span')
                    setTimeout(() => {
                        switchSpan.style.backgroundColor = settingToggle.checked ? '#ad68ff' : '#ccc'
                        switchSpan.style.boxShadow = settingToggle.checked ? '2px 1px 9px #d8a9ff' : 'none'
                        knobSpan.style.transform = settingToggle.checked ? 'translateX(14px) translateY(0)' : 'translateX(0)'
                    }, 1) // min delay to trigger transition fx
                }
            }
        }
    }

    // Define ICON functions

    const icons = {

        about: {
            create() {
                const aboutSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      aboutSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 56.693 56.693']]
                aboutSVGattrs.forEach(([attr, value]) => aboutSVG.setAttribute(attr, value))
                aboutSVG.append(createSVGpath({ stroke: 'none',
                    d: 'M28.765,4.774c-13.562,0-24.594,11.031-24.594,24.594c0,13.561,11.031,24.594,24.594,24.594  c13.561,0,24.594-11.033,24.594-24.594C53.358,15.805,42.325,4.774,28.765,4.774z M31.765,42.913c0,0.699-0.302,1.334-0.896,1.885  c-0.587,0.545-1.373,0.82-2.337,0.82c-0.993,0-1.812-0.273-2.431-0.814c-0.634-0.551-0.954-1.188-0.954-1.891v-1.209  c0-0.703,0.322-1.34,0.954-1.891c0.619-0.539,1.438-0.812,2.431-0.812c0.964,0,1.75,0.277,2.337,0.82  c0.594,0.551,0.896,1.186,0.896,1.883V42.913z M38.427,24.799c-0.389,0.762-0.886,1.432-1.478,1.994  c-0.581,0.549-1.215,1.044-1.887,1.473c-0.643,0.408-1.248,0.852-1.798,1.315c-0.539,0.455-0.99,0.963-1.343,1.512  c-0.336,0.523-0.507,1.178-0.507,1.943v0.76c0,0.504-0.247,1.031-0.735,1.572c-0.494,0.545-1.155,0.838-1.961,0.871l-0.167,0.004  c-0.818,0-1.484-0.234-1.98-0.699c-0.532-0.496-0.801-1.055-0.801-1.658c0-1.41,0.196-2.611,0.584-3.572  c0.385-0.953,0.86-1.78,1.416-2.459c0.554-0.678,1.178-1.27,1.854-1.762c0.646-0.467,1.242-0.93,1.773-1.371  c0.513-0.428,0.954-0.885,1.312-1.354c0.328-0.435,0.489-0.962,0.489-1.608c0-1.066-0.289-1.83-0.887-2.334  c-0.604-0.512-1.442-0.771-2.487-0.771c-0.696,0-1.294,0.043-1.776,0.129c-0.471,0.083-0.905,0.223-1.294,0.417  c-0.384,0.19-0.745,0.456-1.075,0.786c-0.346,0.346-0.71,0.783-1.084,1.301c-0.336,0.473-0.835,0.83-1.48,1.062  c-0.662,0.239-1.397,0.175-2.164-0.192c-0.689-0.344-1.11-0.793-1.254-1.338c-0.135-0.5-0.135-1.025-0.002-1.557  c0.098-0.453,0.369-1.012,0.83-1.695c0.451-0.67,1.094-1.321,1.912-1.938c0.814-0.614,1.847-1.151,3.064-1.593  c1.227-0.443,2.695-0.668,4.367-0.668c1.648,0,3.078,0.249,4.248,0.742c1.176,0.496,2.137,1.157,2.854,1.967  c0.715,0.809,1.242,1.738,1.568,2.762c0.322,1.014,0.486,2.072,0.486,3.146C39.024,23.075,38.823,24.024,38.427,24.799z' }
                ))
                return aboutSVG
            }
        },

        autoSpeechBalloon: {
            create() {
                const autoSpeechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      autoSpeechBalloonSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
                autoSpeechBalloonSVGattrs.forEach(([attr, value]) => autoSpeechBalloonSVG.setAttribute(attr, value))
                autoSpeechBalloonSVG.append(createSVGpath({ stroke: 'none', d: 'M323-41v-247h-10q-105 0-172.5-67T73-528q0-105 74-179t179-74h36l-44-44 69-69 162 162-162 162-69-69 44-44h-36q-64 0-109.5 45.5T171-528q0 64 45.5 109.5T326-373h95v96l96-96h117q64 0 109.5-45.5T789-528q0-64-45.5-109.5T634-683h10v-98h-10q105 0 179 74t74 179q0 105-74 179t-179 74h-77L323-41Z' }))
                return autoSpeechBalloonSVG
            }
        },

        ddgpt: {
            create() {
                const ddgptIcon = document.createElement('img')
                ddgptIcon.src = 'data:image/png;base64,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'
                return ddgptIcon
            }
        },

        downArrows: {
            create() {
                const downArrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      downArrowsSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 24 24']]
                downArrowsSVGattrs.forEach(([attr, value]) => downArrowsSVG.setAttribute(attr, value))
                downArrowsSVG.append(
                    createSVGpath({ stroke: 'none', d: 'M18,13H6a1,1,0,0,1,0-2H18a1,1,0,0,1,0,2Z' }),
                    createSVGpath({ stroke: 'none', d: 'M14.71,18.29a1,1,0,0,1,0,1.42l-2,2a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42l.29.3V16a1,1,0,0,1,2,0v2.59l.29-.3A1,1,0,0,1,14.71,18.29ZM11.29,8.71a1,1,0,0,0,1.42,0l2-2a1,1,0,1,0-1.42-1.42l-.29.3V3a1,1,0,0,0-2,0V5.59l-.29-.3A1,1,0,0,0,9.29,6.71Z' })
                )
                return downArrowsSVG
            }
        },

        fontSize: {
            create() {
                const fontSizeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      fontSizeSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
                fontSizeSVGattrs.forEach(([attr, value]) => fontSizeSVG.setAttribute(attr, value))
                fontSizeSVG.append(
                    createSVGpath({ 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' }),
                    createSVGpath({ stroke: 'none', d: 'M502.572 448.199h-77.475a9.423 9.423 0 0 1-9.173-7.268l-16-68.114a9.423 9.423 0 0 0-9.173-7.268H294.19a9.423 9.423 0 0 0-9.173 7.268l-16 68.114a9.423 9.423 0 0 1-9.173 7.268h-75.241c-6.322 0-10.851-6.104-9.017-12.155L286.362 70.491a9.422 9.422 0 0 1 9.017-6.69h96.947a9.422 9.422 0 0 1 9.021 6.702l110.245 365.554c1.825 6.047-2.703 12.142-9.02 12.142zM379.385 287.395l-27.959-118.047c-2.293-9.683-16.081-9.661-18.344.029l-27.57 118.047c-1.38 5.91 3.106 11.565 9.175 11.565h55.529c6.082-.001 10.571-5.676 9.169-11.594z' })
                )
                return fontSizeSVG
            }
        },

        inwardCarets: {
            create() {
                const caretsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      caretsSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
                caretsSVGattrs.forEach(([attr, value]) => caretsSVG.setAttribute(attr, value))
                caretsSVG.append(createSVGpath({ stroke: '', d: 'M11.29,9.71a1,1,0,0,0,1.42,0l5-5a1,1,0,1,0-1.42-1.42L12,7.59,7.71,3.29A1,1,0,0,0,6.29,4.71Zm1.42,4.58a1,1,0,0,0-1.42,0l-5,5a1,1,0,0,0,1.42,1.42L12,16.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z' }))
                return caretsSVG
            }

        },

        language: {
            create() {
                const languageSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      languageSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
                languageSVGattrs.forEach(([attr, value]) => languageSVG.setAttribute(attr, value))
                languageSVG.append(createSVGpath({ stroke: 'none', d: 'm459-48 188-526h125L960-48H847l-35-100H603L568-48H459ZM130-169l-75-75 196-196q-42-45-78-101t-55-105h117q17 32 40.5 67.5T325-514q35-37 70-93t64-119H0v-106h290v-80h106v80h290v106H572q-23 74-70 152T399-438l82 85-39 111-118-121-194 194Zm508-79h139l-69-197-70 197Z' })                )
                return languageSVG                
            }
        },

        pin: {
            filledSVGpath() { return createSVGpath({ 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'
            })},

            hollowSVGpath() { return createSVGpath({ 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-.146zm.122 2.112v-.002.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a4.507 4.507 0 0 0-.288-.076 4.922 4.922 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a4.924 4.924 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034.114 0 .23-.011.343-.04L9.927 2.028c-.029.113-.04.23-.04.343a1.779 1.779 0 0 0 .062.46z'
            })},

            create() {
                const pinSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      pinSVGattrs = [['id', 'pin-svg'], ['width', 17], ['height', 17], ['viewBox', '0 0 16 16']]
                pinSVGattrs.forEach(([attr, value]) => pinSVG.setAttribute(attr, value))
                pinSVG.append(icons.pin[config.stickySidebar ? 'filledSVGpath' : 'hollowSVGpath']())
                return pinSVG
            },

            update(pinSVG) {
                pinSVG.removeChild(pinSVG.firstChild) // clear path
                pinSVG.append(icons.pin[config.stickySidebar ? 'filledSVGpath' : 'hollowSVGpath']())
                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(createSVGpath({ stroke: 'none', d: 'M428-383q0-71 16-111t63-74q47-35 58.5-55.5T577-683q0-35-25-57.5T488-763q-26 0-61 18t-50 70l-114-47q27-82 90.5-122.5T488-885q93 0 151.5 59.5T698-682q0 55-17 95t-70 83q-37 29-48.5 55T550-383H428Zm60 265q-41 0-69.5-28.5T390-216q0-41 28.5-69.5T488-314q41 0 69.5 28.5T586-216q0 41-28.5 69.5T488-118Z' }))
                return questionMarkSVG
            }
        },
        
        scheme: {
            create() {
                const schemeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      schemeSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
                schemeSVGattrs.forEach(([attr, value]) => schemeSVG.setAttribute(attr, value))
                schemeSVG.append(createSVGpath({ stroke: 'none', d: 'M479.92-34q-91.56 0-173.4-35.02t-142.16-95.34q-60.32-60.32-95.34-142.24Q34-388.53 34-480.08q0-91.56 35.02-173.4t95.34-142.16q60.32-60.32 142.24-95.34Q388.53-926 480.08-926q91.56 0 173.4 35.02t142.16 95.34q60.32 60.32 95.34 142.24Q926-571.47 926-479.92q0 91.56-35.02 173.4t-95.34 142.16q-60.32 60.32-142.24 95.34Q571.47-34 479.92-34ZM530-174q113-19 186.5-102.78T790-480q0-116.71-73.5-201.35Q643-766 530-785v611Z' }))
                return schemeSVG
            }
        },

        shuffledArrows: {
            create() {
                const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      arrowsSVGattrs = [['width', 21], ['height', 21], ['viewBox', '-1 -1 32 32']]
                arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
                arrowsSVG.append(createSVGpath({ stroke: '', d: 'M23.707,16.293L28.414,21l-4.707,4.707l-1.414-1.414L24.586,22H23c-2.345,0-4.496-1.702-6.702-3.753c0.498-0.458,0.984-0.92,1.46-1.374C19.624,18.6,21.393,20,23,20h1.586l-2.293-2.293L23.707,16.293zM23,11h1.586l-2.293,2.293l1.414,1.414L28.414,10l-4.707-4.707l-1.414,1.414L24.586,9H23c-2.787,0-5.299,2.397-7.957,4.936C12.434,16.425,9.736,19,7,19H4v2h3c3.537,0,6.529-2.856,9.424-5.618C18.784,13.129,21.015,11,23,11zM11.843,14.186c0.5-0.449,0.995-0.914,1.481-1.377C11.364,11.208,9.297,10,7,10H4v2h3C8.632,12,10.25,12.919,11.843,14.186z' }))
                return arrowsSVG
            }
        },
        
        signalStream: {
            create() {
                const signalStreamSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      signalStreamSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 0 32 32']]
                signalStreamSVGattrs.forEach(([attr, value]) => signalStreamSVG.setAttribute(attr, value))
                signalStreamSVG.append(createSVGpath({ 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(createSVGpath({ stroke: '', d: 'M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z' }))
                return slashSVG
            }
        },
        
        sliders: {
            create() {
                const slidersSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      slidersSVGattrs = [['width', 20], ['height', 20], ['viewBox', '0 -960 960 960']]
                slidersSVGattrs.forEach(([attr, value]) => slidersSVG.setAttribute(attr, value))
                slidersSVG.append(createSVGpath({ stroke: 'none', d: 'M435.48-102.48V-360H533v80h320v97.52H533v80h-97.52Zm-328.48-80V-280h257.52v97.52H107Zm160-169.04v-80H107v-96.96h160v-80h97.52v256.96H267Zm168.48-80v-96.96H853v96.96H435.48Zm160-168.48v-257.52H693v80h160V-680H693v80h-97.52ZM107-680v-97.52h417.52V-680H107Z' }))
                return slidersSVG
            }
        },
        
        sparkles: {
            create() {
                const sparklesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      sparklesSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 16 16']]
                sparklesSVGattrs.forEach(([attr, value]) => sparklesSVG.setAttribute(attr, value))
                sparklesSVG.append(createSVGpath({ stroke: 'none', d: 'M7.657 6.247c.11-.33.576-.33.686 0l.645 1.937a2.89 2.89 0 0 0 1.829 1.828l1.936.645c.33.11.33.576 0 .686l-1.937.645a2.89 2.89 0 0 0-1.828 1.829l-.645 1.936a.361.361 0 0 1-.686 0l-.645-1.937a2.89 2.89 0 0 0-1.828-1.828l-1.937-.645a.361.361 0 0 1 0-.686l1.937-.645a2.89 2.89 0 0 0 1.828-1.828l.645-1.937zM3.794 1.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387A1.734 1.734 0 0 0 4.593 5.69l-.387 1.162a.217.217 0 0 1-.412 0L3.407 5.69A1.734 1.734 0 0 0 2.31 4.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387A1.734 1.734 0 0 0 3.407 2.31l.387-1.162zM10.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732L9.1 2.137a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L10.863.1z' }))
                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(
                    createSVGpath({ stroke: '', 'stroke-width': '2px', fill: 'none',
                        d: 'M24.5,26c2.881,-2.652 4.5,-6.249 4.5,-10c0,-3.751 -1.619,-7.348 -4.5,-10' }),
                    createSVGpath({ stroke: '', 'stroke-width': '2px', fill: 'none',
                        d: 'M22,20.847c1.281,-1.306 2,-3.077 2,-4.924c0,-1.846 -0.719,-3.617 -2,-4.923' }),
                    createSVGpath({ stroke: 'none', fill: '',
                        d: 'M9.957,10.88c-0.605,0.625 -1.415,0.98 -2.262,0.991c-4.695,0.022 -4.695,0.322 -4.695,4.129c0,3.806 0,4.105 4.695,4.129c0.846,0.011 1.656,0.366 2.261,0.991c1.045,1.078 2.766,2.856 4.245,4.384c0.474,0.49 1.18,0.631 1.791,0.36c0.611,-0.272 1.008,-0.904 1.008,-1.604c0,-4.585 0,-11.936 0,-16.52c0,-0.7 -0.397,-1.332 -1.008,-1.604c-0.611,-0.271 -1.317,-0.13 -1.791,0.36c-1.479,1.528 -3.2,3.306 -4.244,4.384Z' })
                )
                return speakerSVG
            }
        },

        speechBalloon: {
            create() {
                const speechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      speechBalloonSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 -960 960 960']]
                speechBalloonSVGattrs.forEach(([attr, value]) => speechBalloonSVG.setAttribute(attr, value))
                speechBalloonSVG.append(createSVGpath({ stroke: 'none', d: 'M350-212q-32.55 0-55.27-22.73Q272-257.45 272-290v-64h492v-342h63.67q33.33 0 55.83 22.72Q906-650.55 906-618v576L736-212H350ZM54-256v-582.4q0-32.38 22.72-54.99Q99.45-916 132-916h482q32.55 0 55.28 22.72Q692-870.55 692-838v334q0 32.55-22.72 55.27Q646.55-426 614-426H224L54-256Zm540-268v-294H152v294h442Zm-442 0v-294 294Z' }))
                return speechBalloonSVG
            }
        },

        sunglasses: {
            create() {
                const sunglassesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      sunglassesSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
                sunglassesSVGattrs.forEach(([attr, value]) => sunglassesSVG.setAttribute(attr, value))
                sunglassesSVG.append(createSVGpath({ stroke: 'none', d: 'M507.44,185.327c-4.029-5.124-10.185-8.112-16.704-8.112c0,0-48.021,0-156.827,0h-65.774H243.87h-65.774c-108.806,0-156.827,0-156.827,0c-6.519,0-12.675,2.988-16.714,8.112c-4.028,5.125-5.486,11.815-3.965,18.152c0,0,12.421,56.269,19.927,82.534c7.506,26.265,26.265,48.772,86.29,48.772s59.827,0,74.828,0c21.258,0,46.256-19.99,55.028-45.023c4.97-14.16,12.756-32.738,19.338-47.876c6.582,15.138,14.368,33.716,19.338,47.876c8.773,25.033,33.77,45.023,55.028,45.023c15.001,0,14.803,0,74.828,0s78.784-22.507,86.29-48.772c7.496-26.264,19.918-82.534,19.918-82.534C512.935,197.142,511.478,190.452,507.44,185.327z M90.339,278.734C45.314,263.732,40.318,198.7,40.318,198.7s22.507,0,55.028,0L90.339,278.734z M340.464,278.734c-45.015-15.001-50.022-80.034-50.022-80.034s22.508,0,55.029,0L340.464,278.734z' }))
                return sunglassesSVG
            }
        },

        upArrow: {
            create() {
                const upArrowSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      upArrowSVGattrs = [['width', 16], ['height', 16], ['viewBox', '4 2 16 16'],
                                         ['stroke-width', '2'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round']]
                upArrowSVGattrs.forEach(([attr, value]) => upArrowSVG.setAttribute(attr, value))
                upArrowSVG.append(createSVGpath({ stroke: '', fill: 'none', 'stroke-width': '2', linecap: 'round', 'stroke-linejoin': 'round',
                    d: 'M7 11L12 6L17 11M12 18V7' }))
                return upArrowSVG
            }
        },

        widescreen: {
            wideSVGpath() { return createSVGpath({
                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 createSVGpath({
                stroke: '', fill: '', 'fill-rule': 'evenodd', d: 'm28,11 0,14 -20,0 0,-14 z m-18,2 16,0 0,10 -16,0 0,-10 z'
            })},

            create() {
                const widescreenSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      widescreenSVGattrs = [['id', 'ws-svg'], ['width', 18], ['height', 18], ['viewBox', '8 8 20 20']]
                widescreenSVGattrs.forEach(([attr, value]) => widescreenSVG.setAttribute(attr, value))
                widescreenSVG.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
                return widescreenSVG
            },

            update(widescreenSVG) {
                widescreenSVG.removeChild(widescreenSVG.firstChild) // clear path
                widescreenSVG.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
                return widescreenSVG
            }
        }
    }

    // Define UI functions

    function isCenteredMode() { return document.documentElement.classList.toString().includes('center') }

    function updateTitleAnchor() {
        if (appDiv.querySelector('.loading, #ddgpt-alert')) return // only update reply UI

        const appTitleVisible = !!appDiv.querySelector('.app-name'),
              logoVisible = !!appDiv.querySelector('img')

        // Create/fill/classify/style/append/update title anchor
        if (!appTitleVisible || !logoVisible) {
            const appTitleAnchor = createAnchor(config.appURL, (() => {
                if (appLogoImg.loaded) { // size/pos/return app logo img
                    appLogoImg.width = 181 ; appLogoImg.style.margin = '-7px 0'
                    return appLogoImg
                } else { // create/fill/return app name span
                    const appNameSpan = document.createElement('span')
                    appNameSpan.innerText = '🤖 ' + config.appName
                    return appNameSpan
                }
            })())
            appTitleAnchor.classList.add('app-name', 'no-user-select')
            if (!appTitleVisible) appDiv.append(appTitleAnchor)
            else appDiv.querySelector('.app-name').replaceWith(appTitleAnchor) // for appLogoImg.onload() callback
        }
    }

    function updateAppLogoSrc() {
        appLogoImg.onerror = () => appLogoImg.style.display = 'none'
        appLogoImg.src = 'data:image/png;base64,'
            + ( scheme == 'light' ? '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'
                                  : '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' )
    }

    function updateAppStyle() {
        appStyle.innerText = (
            '.no-user-select { -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none }'
          + '#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 ;'
              + 'clip-path: polygon(-1% -3%, 45% -3%, 45% -8%, 101% -8%, 101% 102%, -1% 102%) ;' // show tooltips but bound starry bg
              + 'flex-grow: 1 ; word-wrap: break-word ; white-space: pre-wrap ; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.06) ;'
              + `background: radial-gradient(ellipse at bottom, ${ scheme == 'dark' ? '#2f3031 0%, #090a0f' : 'white 0%, white' } 100%) ;`
              + `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 ;'
              + ( scheme == 'dark' ? 'fill: white ; stroke: white;' : 'fill: #adadad ; stroke: #adadad' ) + '}'
          + `.corner-btn:hover { ${ scheme == 'dark' ? 'fill: #aaa ; stroke: #aaa' : 'fill: black ; stroke: black' } ;`
              + 'transform: scale(1.185) ; transition: transform 0.05s ease }'
          + '#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: 10px ; 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-thumb { 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 0px ; cursor: ew-resize }'
          + '#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 ;'
              + 'transform: scale(1) ; transition: transform 0.1s ease !important }'
          + '.standby-btn:hover { border-radius: 4px ; transform: scale(1.025) ;'
              + `${ scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' }}`
          + '#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 ;'
              + ( scheme == 'dark' ? 'background: #3a3a3acf ; color: #f2f2f2' : 'background: #eaeaeacf ; color: #202124' ) + '}'
          + '@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' ? '#3a3a3acf' : '#eaeaeacf' ) + '}'
          + '#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 ;'
              + `${ isFirefox ? 'top: -20px ; margin: -3px 0 -10px' : 'top: -25px ; margin: -7px 0 -15px' }}`
          + '.related-query {'
              + `margin: 4px 4px ${ scheme == 'dark' ? 7 : 2 }px 0 ; padding: 4px 10px 5px 10px ;`
              + `color: ${ scheme == 'dark' ? '#f2f2f2' : '#767676' } ;`
              + `background: ${ scheme == 'dark' ? '#424242' : '#dadada12' } ;`
              + `border: 1px solid ${ scheme == 'dark' ? '#777' : '#e1e1e1' } ; font-size: 0.88em ; cursor: pointer ;`
              + 'border-radius: 0 13px 12px 13px ; width: fit-content ; flex: 0 0 auto ;'
              + `box-shadow: 1px 3px ${ scheme == 'dark' ? '11px -8px lightgray' : '8px -6px rgba(169, 169, 169, 0.75)' };`
              + 'transform: scale(1) ; transition: transform 0.1s ease !important }'
          + '.related-query:hover, .related-query:focus { transform: scale(1.025) !important ;'
              + `background: ${ scheme == 'dark' ? '#a2a2a270': '#e5edff ; 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) ; transition: opacity 0.5s ease, transform 0.5s ease }'
          + '.fade-in-less { opacity: 0 ; transition: opacity 0.2s ease }'
          + '.fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) }'
          + '.chatbar-btn { z-index: 560 ;'
              + `border: none ; float: right ; position: relative ; bottom: ${ isFirefox ? 50 : 55 }px ; background: none ; cursor: pointer ;`
              + `${ scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }}`
          + '.chatbar-btn:hover {'
              + `${ scheme == 'dark' ? 'color: #white ; fill: #white ; stroke: #white' : 'color: #638ed4 ; fill: #638ed4 ; stroke: #638ed4' }}`
          + ( // 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 -21px }' // 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 { padding: 11px 15px 6px 12px !important }' // pad site notifications
          + '.chatgpt-modal > div { padding: 20px 25px 24px 25px !important ;' // increase alert padding
              + 'background-color: white !important ; color: black }'
          + '.chatgpt-modal h2 { margin: 0 ; padding: 0 ; font-weight: bold }' // shrink margin/padding around alert titles, force bold
          + '.modal-close-btn { top: px !important ; right: -11px !important }' // re-pos modal close button
          + `.modal-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}`
          + `.modal-close-btn:hover { background-color: #${ scheme == 'dark' ? '666464' : 'f2f2f2' } !important }`
          + '.chatgpt-modal p { margin: -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 ${ isMobile ? -5 : -15 }px !important }` // pos modal buttons
          + '.chatgpt-modal button {' // modal buttons
              + 'font-size: 1rem ; text-transform: uppercase ; min-width: 121px ;'
              + `padding: ${ 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' ? // additional darkmode alert styles
              ( '.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {'
                  + 'background-color: black !important ; color: white }'
              + '.primary-modal-btn { background: white !important ; color: black !important }'
              + '.chatgpt-modal button:hover { background-color: #00cfff !important ; color: black !important }' ) : '' )

          // Settings modal
          + '#ddgpt-settings-bg {'
              + 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
              + 'background-color: rgba(67, 70, 72, 0) ;' // init dim bg but no opacity
              + 'transition: background-color 0.05s ease ;' // speed to transition in show alert routine
              + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
          + '#ddgpt-settings {'
              + 'opacity: 0 ; transform: translateX(-2px) translateY(3px) ; min-width: 288px ; max-width: 75vw ; word-wrap: break-word ;'
              + 'transition: opacity 0.1s cubic-bezier(.165,.84,.44,1), transform 0.3s cubic-bezier(.165,.84,.44,1) ;'
              + `border: 1px solid ${ scheme == 'dark' ? 'white ; color: white' : '#b5b5b5 ; color: black' } ;`
              + 'background-image: linear-gradient(180deg,'
                  + `${ scheme == 'dark' ? '#75c451 -70%, black 57%' : '#7683b7 -70%, white 42%' }) ;`
              + 'padding: 11px ; margin: 12px 23px ; 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-bg.animated > div { opacity: 0.98 ; transform: translateX(0) translateY(0) }'
          + '@keyframes alert-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }'
              + '50% { opacity: 0.25 ; transform: scale(1.05) }'
              + '100% { opacity: 0 ; transform: scale(1.35) }}'
          + '#ddgpt-settings-title { font-weight: bold ; line-height: 19px ; text-align: center ; margin: 0 3px -3px 0 }'
          + '#ddgpt-settings-title h4 { font-size: 26px ; font-weight: bold ; margin-top: -39px }' // 'Settings'
          + '#ddgpt-settings-close-btn {'
              + 'cursor: pointer ; width: 20px ; height: 20px ; border-radius: 17px ; float: right ;'
              + 'position: absolute ; top: 10px ; right: 13px }'
          + `#ddgpt-settings-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
          + '#ddgpt-settings-close-btn svg { margin: 6.5px }' // center SVG for hover underlay
          + `#ddgpt-settings-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
          + '#ddgpt-settings ul { list-style: none ; margin-bottom: -7px }' // hide bullets, close bottom gap
          + '#ddgpt-settings li { 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 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 {'
              + 'background: rgba(100, 149, 237, 0.88) ; color: white ; fill: white ; stroke: white ;' // add highlight strip
              + `${ isMobile ? '' : 'transform: scale(1.16)' }}` // add zoom
          + '#ddgpt-settings li > input { float: right }' // pos toggles
          + `#about-menu-entry span { color: ${ scheme == 'dark' ? '#28ee28' : 'green' }}`
    )}

    function updateTweaksStyle() {

        // Update tweaks style based on settings (for tweaks init + show.reply() + toggle.sidebar())
        const isStandbyMode = appDiv.querySelector('.standby-btn'),
              answerIsLoaded = appDiv.querySelector('.corner-btn')
        tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
                              + ( config.stickySidebar && !isStandbyMode && answerIsLoaded ? ssbStyles : '' )

        // Update 'by KudoAI' visibility based on corner space available
        const kudoAIspan = appDiv.querySelector('.kudoai')
        if (kudoAIspan) kudoAIspan.style.display = (
            appDiv.querySelectorAll('.corner-btn').length < ( isMobile ? 3 : !config.widerSidebar ? 6 : 8 )) ? '' : 'none'

        // Update <pre> max-height in Sticky Sidebar mode based on RQ visibility (for get.reply()'s RQ show + menu RQ toggle)
        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 ? 'none' : (
            relatedQueries?.offsetHeight > 0 ? `${ shorterPreHeight }px` : `${ longerPreHeight }px` )
    }

    const fontSizeSlider = {
        fadeInDelay: 5, // ms
        hWheelDistance: 10, // px

        createAppend: function() {

            // Create/append slider elems
            fontSizeSlider.cursorOverlay = document.createElement('div')
            fontSizeSlider.cursorOverlay.classList.add('cursor-overlay') // for resize cursor
            const slider = document.createElement('div') ; slider.id = 'font-size-slider-track'
            slider.className = 'fade-in-less' ; slider.style.display = 'none'
            const sliderThumb = document.createElement('div') ; sliderThumb.id = 'font-size-slider-thumb'
            slider.append(sliderThumb)
            appDiv.insertBefore(slider, appDiv.querySelector('.btn-tooltip,' // desktop
                                                           + 'pre')) // mobile
            // Init thumb pos
            setTimeout(() => {
                const iniLeft = (config.fontSize - config.minFontSize) / (config.maxFontSize - config.minFontSize) // left ratio
                              * (slider.offsetWidth - sliderThumb.offsetWidth) // slider width
                sliderThumb.style.left = iniLeft + 'px'
            }, fontSizeSlider.fadeInDelay) // to ensure visibility for accurate dimension calcs

            // Add event listeners for dragging thumb
            let isDragging = false, startX, startLeft
            sliderThumb.addEventListener(inputEvents.down, event => {
                isDragging = true ; startX = event.clientX ; startLeft = sliderThumb.offsetLeft     
                document.body.appendChild(fontSizeSlider.cursorOverlay)
            })
            document.addEventListener(inputEvents.move, event => {
                if (isDragging) moveThumb(startLeft + event.clientX - startX) })
            document.addEventListener(inputEvents.up, () => {
                isDragging = false
                if (fontSizeSlider.cursorOverlay.parentNode)
                    fontSizeSlider.cursorOverlay.parentNode.removeChild(fontSizeSlider.cursorOverlay)
            })

            // Add event listener for wheel-scrolling thumb
            if (!isMobile) slider.onwheel = event => {
                event.preventDefault()
                moveThumb(sliderThumb.offsetLeft - Math.sign(event.deltaY) * fontSizeSlider.hWheelDistance)
            }

            // Add event listener for seek/dragging by inputEvents.down on track
            slider.addEventListener(inputEvents.down, event => {
                const clientX = event.clientX || event.touches?.[0]?.clientX
                moveThumb(clientX - slider.getBoundingClientRect().left - sliderThumb.offsetWidth / 2)
                isDragging = true ; startX = clientX ; startLeft = sliderThumb.offsetLeft // manually init dragging
                document.body.appendChild(fontSizeSlider.cursorOverlay)
            })

            function moveThumb(newLeft) {

                // Bound thumb
                const sliderWidth = slider.offsetWidth - sliderThumb.offsetWidth
                if (newLeft < 0) newLeft = 0
                if (newLeft > sliderWidth) newLeft = sliderWidth
    
                // Move thumb
                sliderThumb.style.left = newLeft + 'px'
    
                // Adjust font size based on thumb position
                const answerPre = appDiv.querySelector('pre'),
                      fontSizePercent = newLeft / sliderWidth,
                      fontSize = config.minFontSize + fontSizePercent * (config.maxFontSize - config.minFontSize)
                answerPre.style.fontSize = fontSize + 'px'
                answerPre.style.lineHeight = fontSize * config.lineHeightRatio + 'px'
                saveSetting('fontSize', fontSize)
            }

            return slider            
        },

        toggle: function(state = '') {
            const slider = document.getElementById('font-size-slider-track') || fontSizeSlider.createAppend()

            // Toggle visibility
            const balloonTip = appDiv.querySelector('.balloon-tip')
            if (state == 'on' || (!state && slider.style.display == 'none')) {
                slider.style.display = '' ; balloonTip.style.display = 'none'
                setTimeout(() => slider.classList.add('active'), fontSizeSlider.fadeInDelay)
            } else if (state == 'off' || (!state && slider.style.display != 'none')) {
                slider.classList.remove('active') ; balloonTip.style.display = ''
                setTimeout(() => slider.style.display = 'none', 55)
            }
        }
    }

    function updateStars() {
        ['sm', 'med', 'lg'].forEach(size => appDiv.querySelector(
            `[id$="stars-${size}"]`).id = `${scheme == 'dark' ? 'white' : 'black' }-stars-${size}`)
    }

    function updateTooltip(buttonType) { // text & position
        const cornerBtnTypes = ['about', 'settings', 'speak', 'ssb', 'font-size', 'wsb']
                  .filter(type => appDiv.querySelector(`#${type}-btn`)) // exclude invisible ones
        const [ctrAddend, spreadFactor] = appDiv.querySelector('.standby-btn') ? [8, 28] : [6, 27.5],
              iniRoffset = spreadFactor * ( buttonType == 'send' ? 1.5
                                          : buttonType == 'shuffle' ? 2.5
                                          : cornerBtnTypes.indexOf(buttonType) +1 ) + ctrAddend
        // Update text
        tooltipDiv.innerText = (
            buttonType == 'about' ? msgs.menuLabel_about || 'About'
          : buttonType == 'settings' ? msgs.menuLabel_settings || 'Settings'
          : buttonType == 'speak' ? msgs.tooltip_playAnswer || 'Play answer'
          : buttonType == 'ssb' ? (( config.stickySidebar ? `${ msgs.prefix_exit || 'Exit' } ` :  '' )
                                   + ( msgs.menuLabel_stickySidebar || 'Sticky Sidebar' ))
          : buttonType == 'font-size' ? msgs.tooltip_fontSize || 'Font size'
          : buttonType == 'wsb' ? (( config.widerSidebar ? `${ msgs.prefix_exit || 'Exit' } ` :  '' )
                                   + ( msgs.menuLabel_widerSidebar || 'Wider Sidebar' ))
          : buttonType == 'send' ? msgs.tooltip_sendReply || 'Send reply'
          : buttonType == 'shuffle' ? msgs.tooltip_askRandQuestion || 'Ask random question' : '' )

        // Update position
        tooltipDiv.style.top = `${ !/send|shuffle/.test(buttonType) ? -15
          : tooltipDiv.eventYpos - appDiv.getBoundingClientRect().top - 36 }px`
        tooltipDiv.style.right = `${ iniRoffset - tooltipDiv.getBoundingClientRect().width / 2 }px`
    }

    function handleRQevent(event) { // for attachment/removal in `get.reply()` + `show.reply().handleSubmit()`
        const keys = [' ', 'Spacebar', 'Enter', 'Return'], keyCodes = [32, 13]    
        if (keys.includes(event.key) || keyCodes.includes(event.keyCode) || event.type == 'click') {
            event.preventDefault() // prevent scroll on space taps
            appDiv.querySelector('.related-queries').remove() // remove related queries

            // Send related query
            const chatbar = appDiv.querySelector('textarea')
            if (chatbar) {
                chatbar.value = event.target.textContent
                show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
                chatbar.dispatchEvent(new KeyboardEvent('keydown', {
                    key: 'Enter', bubbles: true, cancelable: true }))
            }
    }}

    // Define FACTORY functions

    function createAnchor(linkHref, displayContent, attrs = {}) {
        const anchor = document.createElement('a'),
              defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
              finalAttrs = { ...defaultAttrs, ...attrs }
        Object.entries(finalAttrs).forEach(([attr, value]) => anchor.setAttribute(attr, value))
        if (displayContent) {
            if (typeof displayContent == 'string') anchor.textContent = displayContent
            else if (displayContent instanceof HTMLElement) anchor.append(displayContent)
        }
        return anchor
    }

    function createStyle(content) {
        const style = document.createElement('style')
        if (content) style.innerText = content
        return style
    }

    function createSVGpath(attrs) {
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
        for (const attr in attrs) path.setAttributeNS(null, attr, attrs[attr])
        return path
    }

    // Define TOGGLE functions

    const toggle = {

        bgAnimations() {
            saveSetting('bgAnimationsDisabled', !config.bgAnimationsDisabled)
            const starSizes = ['sm', 'med', 'lg']
            appDiv.querySelectorAll('[id*="stars-"]').forEach((starsDiv, idx) => starsDiv.id = (
                config.bgAnimationsDisabled ? 'stars-off' : `${ scheme == 'dark' ? 'white' : 'black' }-stars-${starSizes[idx]}` ))
            notify(`${settingsProps.bgAnimationsDisabled.label} ${menuState.word[+!config.bgAnimationsDisabled]}`)
        },

        proxyMode() {
            saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
            notify(( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' ' + menuState.word[+config.proxyAPIenabled])
            refreshMenu()
            if (modals.settings.get()) { // update visual state of Settings toggle
                const proxyToggle = document.querySelector('[id*="proxy"][id*="menu-entry"] input')
                if (proxyToggle.checked != config.proxyAPIenabled) modals.settings.toggle.switch(proxyToggle)
            }
            if (appDiv.querySelector('#ddgpt-alert')) location.reload() // re-send query if user alerted 
        },

        relatedQueries() {
            saveSetting('rqDisabled', !config.rqDisabled)
            const relatedQueriesDiv = appDiv.querySelector('.related-queries')
            if (relatedQueriesDiv) { // update visibility based on latest setting
                relatedQueriesDiv.style.display = config.rqDisabled ? 'none' : 'flex'
                appFooter.style.right = ( // counteract right-offset bug from chatbar padding
                    relatedQueriesDiv.style.display == 'flex' ? 0 : '-72px' )
            }
            if (!config.rqDisabled && !relatedQueriesDiv) { // get related queries for 1st time
                const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
                get.related(lastQuery).then(queries => show.related(queries))
                    .catch(err => { consoleErr(err.message)
                        if (get.related.status != 'done') api.tryNew(get.related) })
            }
            updateTweaksStyle() // toggle <pre> max-height
            notify(( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' ' + menuState.word[+!config.rqDisabled])
        },

        sidebar(mode) {
            saveSetting(mode + 'Sidebar', !config[mode + 'Sidebar'])
            updateTweaksStyle()
            const wsbSVGs = document.querySelectorAll('#ws-svg'),
                  ssbSVGs = document.querySelectorAll('#pin-svg')
            if (mode == 'wider' && wsbSVGs.length > 0)
                wsbSVGs.forEach(svg => icons.widescreen.update(svg))                
            else if (mode == 'sticky' && ssbSVGs.length > 0)
                ssbSVGs.forEach(svg => icons.pin.update(svg))  
            notify(( msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' )
                + ' ' + menuState.word[+config[mode + 'Sidebar']])
        },

        streaming() {
            const streamingToggle = document.querySelector('[id*="streaming"][id*="menu-entry"] input'),
                  scriptCatLink = isFirefox ? 'https://addons.mozilla.org/firefox/addon/scriptcat/'
                                : isEdge    ? 'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh'
                                            : 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf'
            if (!/Tampermonkey|ScriptCat/.test(getUserscriptManager())) { // alert userscript manager unsupported, suggest TM/SC
                siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
                    `${settingsProps.streamingDisabled.label} ${ msgs.alert_isOnlyAvailFor || 'is only available for' }`
                        + ( !isEdge && !isBrave ? // suggest TM for supported browsers
                            ` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${ msgs.alert_and || 'and' }`
                                : '' )
                        + ` <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a>.` // suggest SC
                        + ` (${ msgs.alert_userscriptMgrNoStream || 'Your userscript manager does not support returning stream responses' }.)`
                )
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else if (getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave)) { // alert TM/browser unsupported, suggest SC
                siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
                    `${settingsProps.streamingDisabled.label} ${ msgs.alert_isUnsupportedIn || 'is unsupported in' } `
                        + `${ isChrome ? 'Chrome' : isEdge ? 'Edge' : 'Brave' } ${ msgs.alert_whenUsing || 'when using' } Tampermonkey. `
                        + `${ msgs.alert_pleaseUse || 'Please use' } <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a> `
                            + `${ msgs.alert_instead || 'instead' }.`
                )
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
                let msg = `${settingsProps.streamingDisabled.label} `
                        + `${ msgs.alert_isCurrentlyOnlyAvailBy || 'is currently only available by' } `
                        + `${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }. `
                        + `(${ msgs.alert_openAIsupportSoon || 'Support for OpenAI API will be added shortly' }!)`
                const switchPhrase = msgs.alert_switchingOn || 'switching on'
                msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
                const alertID = siteAlert(`${ msgs.mode_streaming || 'Streaming Mode' } ${ msgs.alert_unavailable || 'unavailable' }`, msg),
                      alert = document.getElementById(alertID)
                alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() }
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else { // functional toggle
                saveSetting('streamingDisabled', !config.streamingDisabled)
                notify(settingsProps.streamingDisabled.label + ' ' + menuState.word[+!config.streamingDisabled])
            }
        },

        tooltip(event) { // visibility
            tooltipDiv.eventYpos = event.currentTarget.getBoundingClientRect().top // for updateTooltip() y-pos calc
            updateTooltip(event.currentTarget.id.replace(/-btn$/, ''))
            tooltipDiv.style.opacity = event.type == 'mouseover' ? 1 : 0
        }
    }

    // Define SESSION functions

    function isBlockedbyCloudflare(resp) {
        try {
            const html = new DOMParser().parseFromString(resp, 'text/html'),
                  title = html.querySelector('title')
            return title.innerText == 'Just a moment...'
        } catch (err) { return false }
    }

    function deleteOpenAIcookies() {
        GM_deleteValue(config.keyPrefix + '_openAItoken')
        if (getUserscriptManager() != 'Tampermonkey') return
        GM_cookie.list({ url: openAIendpoints.auth }, (cookies, error) => {
            if (!error) { for (const cookie of cookies) {
                GM_cookie.delete({ url: openAIendpoints.auth, name: cookie.name })
    }}})}

    function getOpenAItoken() {
        return new Promise(resolve => {
            const accessToken = GM_getValue(config.keyPrefix + '_openAItoken')
            consoleInfo('OpenAI access token: ' + accessToken)
            if (!accessToken) {
                xhr({ url: openAIendpoints.session, onload: resp => {
                    if (isBlockedbyCloudflare(resp.responseText)) {
                        appAlert('checkCloudflare') ; return }
                    try {
                        const newAccessToken = JSON.parse(resp.responseText).accessToken
                        GM_setValue(config.keyPrefix + '_openAItoken', newAccessToken)
                        resolve(newAccessToken)
                    } catch { if (get.reply.api == 'OpenAI') appAlert('login') ; return }
                }})
            } else resolve(accessToken)
    })}

    function generateGPTforLoveKey() {
        let nn = Math.floor(new Date().getTime() / 1e3)
        const fD = e => {
            let t = CryptoJS.enc.Utf8.parse(e),
                o = CryptoJS.AES.encrypt(t, 'fjfsd我w4真3dd服iuhf了wf', {
                    mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
            })
            return o.toString()
        }
        return fD(nn)
    }

    // Define API functions

    const api = {

        pick: function(caller) {
            const logPrefix = `get.${caller.name}() » `
            const untriedAPIs = Object.keys(apis).filter(api =>
                   api != ( caller == get.reply ? 'OpenAI' : '' ) // exclude OpenAI for get.reply() since Proxy Mode
                && !caller.triedAPIs.some(entry => Object.prototype.hasOwnProperty.call(entry, api)) // exclude tried APIs
                && (config.streamingDisabled || apis[api].streamable)) // exclude unstreamable APIs if config.streamingDisabled
            const chosenAPI = untriedAPIs[ // pick random array entry
                Math.floor(chatgpt.randomFloat() * untriedAPIs.length)]
            if (!chosenAPI) { consoleErr('No proxy APIs left untried') ; return null }

            // Log chosen API endpoint
            consoleInfo(logPrefix + 'Endpoint used: ' + apis[chosenAPI].endpoint)
            return chosenAPI
        },

        tryNew: function(caller, reason = 'err') {
            consoleErr(`Error using ${apis[caller.api].endpoint} due to ${reason}`)
            if (caller.attemptCnt < Object.keys(apis).length -+(caller == get.reply)) {
                consoleInfo('Trying another endpoint...')
                caller.triedAPIs.push({ [caller.api]: reason }) ; caller.attemptCnt++
                caller(caller == get.reply ? msgChain : stripQueryAugments(msgChain)[msgChain.length - 1].content)
                    .then(result => { if (caller == get.related) show.related(result) ; else return })
            } else {
                consoleInfo('No remaining untried endpoints')
                if (caller == get.reply) appAlert('proxyNotWorking', 'suggestOpenAI')
            }
        },

        clearTimedOut: function(triedAPIs) { // to retry on new queries
            triedAPIs.splice(0, triedAPIs.length, // empty apiArray
                ...triedAPIs.filter(entry => Object.values(entry)[0] != 'timeout')) // replace w/ err'd APIs
        },

        createHeaders: function(api) {
            const ip = ipv4.generate({ verbose: false })
            let headers = { 'Content-Type': 'application/json', 'X-Forwarded-For': ip, 'X-Real-IP': ip }
            if (api == 'OpenAI') headers.Authorization = 'Bearer ' + config.openAIkey
            headers.Referer = headers.Origin = apis[api].expectedOrigin || '' // preserve expected traffic src
            return headers
        },

        createPayload: function(api, msgs) {
            let payload = {}
            if (api == 'OpenAI')
                payload = { messages: msgs, model: 'gpt-3.5-turbo', max_tokens: 4000 }
            else if  (api == 'AIchatOS') {
                payload = {
                    prompt: msgs[msgs.length - 1].content,
                    withoutContext: false, userId: apiIDs.aiChatOS.userID, network: true
                }
            } else if (api == 'GPTforLove') {
                payload = {
                    prompt: msgs[msgs.length - 1].content,
                    secret: generateGPTforLoveKey(), top_p: 1, temperature: 0.8,
                    systemMessage: 'You are ChatGPT, the version is GPT-4o, a large language model trained by OpenAI. Follow the user\'s instructions carefully.'
                }
                if (apiIDs.gptForLove.parentID) payload.options = { parentMessageId: apiIDs.gptForLove.parentID }
            } else if (api == 'MixerBox AI')
                payload = { prompt: msgs, model: 'gpt-3.5-turbo' }
            return JSON.stringify(payload)
        }
    }

    // Define QUERY AUGMENT functions

    function augmentQuery(query) { return query + ` (reply in ${config.replyLanguage})` }

    function stripQueryAugments(msgChain) {
        const augmentCnt = augmentQuery.toString().match(/\+/g).length
        return msgChain.map(msg => { // stripped chain
            if (msg.role == 'user') {
                let content = msg.content
                const augments = content.match(/\s*\([^)]*\)\s*/g)
                if (augments) for (let i = 0 ; i < augmentCnt ; i++) // strip augments
                    content = content.replace(augments[augments.length - 1 - i], '')
                return { ...msg, content: content.trim() }
            } else return msg // agent's unstripped
        })
    }

    // Define GET functions

    const get = {

        async reply(msgChain) {

            // Init API attempt props
            get.reply.status = 'waiting'
            if (!get.reply.triedAPIs) get.reply.triedAPIs = []
            if (!get.reply.attemptCnt) get.reply.attemptCnt = 1

            // Pick API
            get.reply.api = config.proxyAPIenabled ? api.pick(get.reply) : 'OpenAI'
            if (!get.reply.api) { // no more proxy APIs left untried
                appAlert('proxyNotWorking', 'suggestOpenAI') ; return }

            if (!config.proxyAPIenabled) // init OpenAI key
                config.openAIkey = await Promise.race([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])
            else setTimeout(() => { // try diff API after 6-9s of no response
                if (config.proxyAPIenabled && get.reply.status != 'done' && !get.reply.sender)
                    api.tryNew(get.reply, 'timeout') }, config.streamingDisabled ? 9000 : 6000)

            // Get/show answer from ChatGPT
            xhr({
                method: apis[get.reply.api].method, url: apis[get.reply.api].endpoint,
                responseType: config.streamingDisabled || !config.proxyAPIenabled ? 'text' : 'stream',
                headers: api.createHeaders(get.reply.api), data: api.createPayload(get.reply.api, msgChain),
                onload: resp => dataProcess.text(get.reply, resp),
                onloadstart: resp => dataProcess.stream(get.reply, resp),
                onerror: err => { consoleErr(err.message)
                    if (!config.proxyAPIenabled) appAlert(!config.openAIkey ? 'login' : ['openAInotWorking', 'suggestProxy'])
                    else if (get.reply.status != 'done') api.tryNew(get.reply)
                }
            })

            // Get/show related queries if enabled on 1st get.reply()
            if (!config.rqDisabled && get.reply.attemptCnt == 1) {
                const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
                get.related(lastQuery).then(queries => show.related(queries))
                    .catch(err => { consoleErr(err.message)
                        if (get.related.status != 'done') api.tryNew(get.related) })
            }
        },

        json(url, callback) { // for dynamic footer
            xhr({ method: 'GET', url: url, onload: resp => {
                if (resp.status >= 200 && resp.status < 300) {
                    try { const data = JSON.parse(resp.responseText) ; callback(null, data) }
                    catch (err) { callback(err, null) }
                } else callback(new Error('Failed to load data: ' + resp.statusText), null)
            }})
        },

        async related(query) {

            // Init API attempt props
            get.related.status = 'waiting'
            if (!get.related.triedAPIs) get.related.triedAPIs = []
            if (!get.related.attemptCnt) get.related.attemptCnt = 1

            // Pick API
            get.related.api = api.pick(get.related)
            if (!get.related.api) return // no more proxy APIs left untried

            // Init OpenAI key
            if (get.related.api == 'OpenAI')
                config.openAIkey = await Promise.race([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])

            // Try diff API after 7s of no response
            setTimeout(() => { if (get.related.status != 'done') api.tryNew(get.related, 'timeout') }, 7000)

            return new Promise(resolve => {
                const rqPrompt = 'Show a numbered list of queries related to this one:\n\n' + query
                   + '\n\nMake sure to suggest a variety that can even greatly deviate from the original topic.'
                   + ' For example, if the original query asked about someone\'s wife,'
                       + ' a good related query could involve a different relative and using their name.'
                   + ' Another example, if the query asked about a game/movie/show,'
                       + ' good related queries could involve pertinent characters.'
                   + ' Another example, if the original query asked how to learn JavaScript,'
                       + ' good related queries could ask why/when/where instead, even replacing JS w/ other languages.'
                   + ' But the key is variety. Do not be repetitive.'
                       + ' You must entice user to want to ask one of your related queries.'
                   + ` Reply in ${config.replyLanguage}`
                xhr({
                    method: apis[get.related.api].method, url: apis[get.related.api].endpoint,
                    responseType: 'text', headers: api.createHeaders(get.related.api),
                    data: api.createPayload(get.related.api, [{ role: 'user', content: rqPrompt }]),
                    onload: resp => dataProcess.text(get.related, resp).then(resolve),
                    onerror: err => { consoleErr(err.message) ; if (get.related.status != 'done') api.tryNew(get.related) }
            })})
        }
    }

    // Define PROCESS functions

    const dataProcess = {

        text(caller, resp) {
            return new Promise(resolve => {
                let respText = ''
                const logPrefix = `get.${caller.name}() » dataProcess.text() » `
                if (caller == get.reply && config.proxyAPIenabled && !config.streamingDisabled || caller.status == 'done')
                    return
                if (resp.status != 200) {
                    consoleErr(logPrefix + 'Response status', resp.status)
                    consoleErr(logPrefix + 'Response', JSON.stringify(resp))
                    if (caller == get.reply && caller.api == 'OpenAI')
                        appAlert(resp.status == 401 ? 'login'
                               : resp.status == 403 ? 'checkCloudflare'
                               : resp.status == 429 ? ['tooManyRequests', 'suggestProxy']
                                                    : ['openAInotWorking', 'suggestProxy'] )
                    else if (caller.status != 'done')
                        api.tryNew(caller)
                } else if (caller.api == 'OpenAI') {
                    if (resp.response) {
                        try { // to show response or return related queries
                            respText = JSON.parse(resp.response).choices[0].message.content
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText) ; else resolve(arrayify(respText))
                        } catch (err) { // suggest proxy or try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.response)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
                            else if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else { // suggest proxy or try diff API
                        if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
                        else if (caller.status != 'done') api.tryNew(caller)
                    }
                } else if (caller.api == 'AIchatOS') {
                    if (resp.responseText
                        && !new RegExp([apis.AIchatOS.expectedOrigin, ...apis.AIchatOS.failFlags]
                            .map(str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // escape special chars
                            .join('|')).test(resp.responseText)) {
                                try { // to show response or return related queries
                                    const text = resp.responseText, chunkSize = 1024
                                    let currentIdx = 0
                                    while (currentIdx < text.length) {
                                        const chunk = text.substring(currentIdx, currentIdx + chunkSize)
                                        currentIdx += chunkSize ; respText += chunk
                                    }
                                    if (!respText) throw new Error()
                                    caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                                    if (caller == get.reply) show.reply(respText) ; else resolve(arrayify(respText))
                                } catch (err) { // try diff API
                                    consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                                    consoleErr(logPrefix + appAlerts.parseFailed, err)
                                    if (caller.status != 'done') api.tryNew(caller)
                                }
                    } else if (caller.status != 'done') api.tryNew(caller)
                } else if (caller.api == 'GPTforLove') {
                    if (resp.responseText && !resp.responseText.includes('Fail')) {
                        try { // to show response or return related queries
                            let chunks = resp.responseText.trim().split('\n'),
                                lastObj = JSON.parse(chunks[chunks.length - 1])
                            if (lastObj.id) apiIDs.gptForLove.parentID = lastObj.id
                            respText = lastObj.text
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText) ; else resolve(arrayify(respText))
                        } catch (err) { // try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else if (caller.status != 'done') api.tryNew(caller)
                } else if (caller.api == 'MixerBox AI') {
                    if (resp.responseText) {
                        try { // to show response or return related queries
                            const extractedData = Array.from(resp.responseText.matchAll(/data:(.*)/g), match => match[1]
                                .replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
                                .filter(match => !/(?:message_(?:start|end)|done)/.test(match))
                            respText = extractedData.join('')
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText) ; else resolve(arrayify(respText))
                        } catch (err) { // try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else if (caller.status != 'done') api.tryNew(caller)
                }

                function arrayify(strList) { // for get.related() calls
                    return (strList.match(/\d+\.\s*(.*?)(?=\n|$)/g) || [])
                        .slice(0, 5) // limit to 1st 5
                        .map(match => match.replace(/^\d+\.\s*/, '')) // strip numbering
                }
            })
        },

        stream(caller, stream) {
            if (config.streamingDisabled || !config.proxyAPIenabled) return
            const reader = stream.response.getReader() ; let accumulatedChunks = ''
            reader.read().then(processStreamText).catch(err => consoleErr('Error processing stream', err.message))
            function processStreamText({ done, value }) {
                if (done) {
                    caller.status = 'done' ; caller.sender = null
                    api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                    return
                }
                let chunk = new TextDecoder('utf8').decode(new Uint8Array(value))
                if (caller.api == 'MixerBox AI') { // pre-process chunks
                    const extractedChunks = Array.from(chunk.matchAll(/data:(.*)/g), match => match[1]
                        .replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
                        .filter(match => !/(?:message_(?:start|end)|done)/.test(match))
                    chunk = extractedChunks.join('')
                }
                accumulatedChunks = apis[caller.api].accumulatesText ? chunk : accumulatedChunks + chunk
                if (/['"]?status['"]?:\s*['"]Fail['"]/.test(accumulatedChunks)) { // GPTforLove fail
                    consoleErr('Response', accumulatedChunks)
                    if (caller.status != 'done' && !caller.sender) api.tryNew(caller)
                    return
                }
                try { // to show stream text
                    let textToShow
                    if (caller.api == 'GPTforLove') { // extract parentID + latest chunk text
                        const jsonLines = accumulatedChunks.split('\n'),
                              nowResult = JSON.parse(jsonLines[jsonLines.length - 1])
                        if (nowResult.id) apiIDs.gptForLove.parentID = nowResult.id // for contextual replies
                        textToShow = nowResult.text
                    } else textToShow = accumulatedChunks
                    if (textToShow && caller.status != 'done') { // text ready, app waiting or sending
                        if (!caller.sender) caller.sender = caller.api // app is waiting, become sender
                        if (caller.sender == caller.api) show.reply(textToShow)
                    }
                } catch (err) { consoleErr('Error showing stream', err.message) }
                return reader.read().then(({ done, value }) => {
                    if (caller.sender == caller.api) // am designated sender, recurse
                        processStreamText({ done, value })
                }).catch(err => consoleErr('Error reading stream', err.message))
            }
        }
    }

    // Define SHOW functions

    const show = {

        reply(answer) {

            // Hide font size slider if visible
            if (appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')

            // Build answer interface up to reply section if missing
            if (!appDiv.querySelector('pre')) {
                while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild); // clear app content

                // Fill starry BG
                ['sm', 'med', 'lg'].forEach((size, idx) => {
                    const starsDiv = document.createElement('div')
                    starsDiv.id = config.bgAnimationsDisabled ? 'stars-off'
                                : `${ scheme == 'dark' ? 'white' : 'black' }-stars-${size}`
                    starsDiv.style.height = `${ idx +1 }px` // so toggle.bgAnimations() doesn't change height
                    appDiv.append(starsDiv)
                })

                // Create/append app title anchor
                updateTitleAnchor()

                // Create/append corner buttons div
                const cornerBtnsDiv = document.createElement('div') ; cornerBtnsDiv.id = 'corner-btns'
                appDiv.append(cornerBtnsDiv)

                // Create/append About button
                const aboutSpan = document.createElement('span'),
                      aboutSVG = icons.about.create()
                aboutSpan.id = 'about-btn' // for toggle.tooltip()
                aboutSpan.className = 'corner-btn'
                aboutSpan.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 = '-1px 9px 0 0'
                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 Sticky Sidebar button
                if (!isCentered && !isMobile) {
                    var ssbSpan = document.createElement('span'),
                        ssbSVG = icons.pin.create()
                    ssbSpan.id = 'ssb-btn' // for toggle.sidebar() + toggle.tooltip()
                    ssbSpan.className = 'corner-btn' ; ssbSpan.style.margin = '1px 9px 0 0'
                    ssbSpan.append(ssbSVG) ; cornerBtnsDiv.append(ssbSpan)
                }

                // 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 Wider Sidebar button
                if (!isCentered && !isMobile) {   
                    var wsbSpan = document.createElement('span'),
                        wsbSVG = icons.widescreen.create()
                    wsbSpan.id = 'wsb-btn' // for toggle.sidebar() + toggle.tooltip()
                    wsbSpan.className = 'corner-btn' ; wsbSpan.style.margin = `${ isFirefox ? 1 : 0 }px 13.5px 0 0`
                    wsbSpan.append(wsbSVG) ; cornerBtnsDiv.append(wsbSpan)
                }

                // Add tooltips
                if (!isMobile) appDiv.append(tooltipDiv)

                // Add corner button listeners
                aboutSVG.onclick = modals.about.show
                settingsSVG.onclick = modals.settings.show
                if (speakerSVG) speakerSVG.onclick = () => {
                    const dialectMap = [
                        { code: 'en', regex: /^(eng(lish)?|en(-\w\w)?)$/i, rate: 2 },
                        { code: 'ar', regex: /^(ara?(bic)?|اللغة العربية)$/i, rate: 1.5 },
                        { code: 'cs', regex: /^(cze(ch)?|[cč]e[sš].*|cs)$/i, rate: 1.4 },
                        { code: 'da', regex: /^dan?(ish|sk)?$/i, rate: 1.3 },
                        { code: 'de', regex: /^(german|deu?(tsch)?)$/i, rate: 1.5 },
                        { code: 'es', regex: /^(spa(nish)?|espa.*|es(-\w\w)?)$/i, rate: 1.5 },
                        { code: 'fi', regex: /^(fin?(nish)?|suom.*)$/i, rate: 1.4 },
                        { code: 'fr', regex: /^fr/i, rate: 1.2 },
                        { code: 'hu', regex: /^(hun?(garian)?|magyar)$/i, rate: 1.5 },
                        { code: 'it', regex: /^ita?(lian[ao]?)?$/i, rate: 1.4 },
                        { code: 'ja', regex: /^(ja?pa?n(ese)?|日本語|ja)$/i, rate: 1.5 },
                        { code: 'nl', regex: /^(dut(ch)?|flemish|nederlandse?|vlaamse?|nld?)$/i, rate: 1.3 },
                        { code: 'pl', regex: /^po?l(ish|ski)?$/i, rate: 1.4 },
                        { code: 'pt', regex: /^(por(tugu[eê]se?)?|pt(-\w\w)?)$/i, rate: 1.5 },
                        { code: 'ru', regex: /^(rus?(sian)?|русский)$/i, rate: 1.3 },
                        { code: 'sv', regex: /^(swe?(dish)?|sv(enska)?)$/i, rate: 1.4 },
                        { code: 'tr', regex: /^t[uü]?r(k.*)?$/i, rate: 1.6 },
                        { code: 'vi', regex: /^vi[eệ]?t?(namese)?$/i, rate: 1.5 },
                        { code: 'zh-CHS', regex: /^(chi(nese)?|zh|中[国國])/i, rate: 2 }
                    ]
                    const replyDialect = dialectMap.find(entry => entry.regex.test(config.replyLanguage)) || dialectMap[0],
                          payload = { text: appDiv.querySelector('pre').textContent, curTime: Date.now(),
                                      spokenDialect: replyDialect.code, rate: replyDialect.rate.toString() },
                          key = CryptoJS.enc.Utf8.parse('76350b1840ff9832eb6244ac6d444366'),
                          iv = CryptoJS.enc.Utf8.parse(atob('AAAAAAAAAAAAAAAAAAAAAA==') || '76350b1840ff9832eb6244ac6d444366')
                    const securePayload = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
                        iv: iv, mode: CryptoJS.mode.CBC, pad: CryptoJS.pad.Pkcs7 }).toString()
                    xhr({ // audio from Sogou TTS
                        url: 'https://fanyi.sogou.com/openapi/external/getWebTTS?S-AppId=102356845&S-Param='
                            + encodeURIComponent(securePayload),
                        method: 'GET', responseType: 'arraybuffer',
                        onload: async resp => {
                            if (resp.status != 200) chatgpt.speak(answer, { voice: 2, pitch: 1, speed: 1.5 })
                            else {
                                const audioContext = new (window.AudioContext || window.webkitAudioContext)()
                                audioContext.decodeAudioData(resp.response, buffer => {
                                    const audioSrc = audioContext.createBufferSource()
                                    audioSrc.buffer = buffer
                                    audioSrc.connect(audioContext.destination) // connect source to speakers
                                    audioSrc.start(0) // play audio
                        })}}
                    })
                }
                if (ssbSVG) ssbSVG.onclick = () => toggle.sidebar('sticky')
                if (fontSizeSVG) fontSizeSVG.onclick = () => fontSizeSlider.toggle()
                if (wsbSVG) wsbSVG.onclick = () => toggle.sidebar('wider')
                if (!isMobile) // add hover listeners for tooltips
                    [aboutSpan, settingsSpan, speakerSpan, ssbSpan, fontSizeSpan, wsbSpan].forEach(span => {
                        if (span) span.onmouseover = span.onmouseout = toggle.tooltip })

                // Create/append 'by KudoAI'
                const kudoAIspan = document.createElement('span')
                kudoAIspan.classList.add('kudoai', 'no-user-select') ; kudoAIspan.textContent = 'by '
                kudoAIspan.append(createAnchor('https://www.kudoai.com', 'KudoAI'))
                appDiv.querySelector('.app-name').insertAdjacentElement('afterend', kudoAIspan)
                updateTweaksStyle() // show/hide based on corner space available

                // Show standby state if prefix/suffix mode on
                if (answer == 'standby') {
                    const standbyBtn = document.createElement('button')
                    standbyBtn.className = 'standby-btn'
                    standbyBtn.textContent = msgs.buttonLabel_sendQueryToGPT || 'Send search query to GPT'
                    appDiv.append(standbyBtn)
                    standbyBtn.onclick = () => {
                        appAlert('waitingResponse')
                        msgChain.push({ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) })
                        show.reply.submitSrc = 'click' ; show.reply.chatbarFocused = false
                        get.reply(msgChain)
                    }

                // Otherwise create/append answer bubble
                } else {
                    const answerPre = document.createElement('pre'),
                          balloonTipSpan = document.createElement('span')
                    balloonTipSpan.className = 'balloon-tip'
                    appDiv.append(balloonTipSpan, answerPre)
                }
            }

            // Build reply section if missing
            if (!appDiv.querySelector('#app-chatbar')) {

                // Init/clear reply section content/classes
                const replySection = appDiv.querySelector('section') || document.createElement('section')
                while (replySection.firstChild) replySection.removeChild(replySection.firstChild)
                replySection.classList.remove('loading', 'no-user-select')

                // Create/append section elems
                const replyForm = document.createElement('form'),
                      continueChatDiv = document.createElement('div'),
                      chatTextarea = document.createElement('textarea')
                continueChatDiv.className = 'continue-chat'
                chatTextarea.id = 'app-chatbar' ; chatTextarea.rows = '1'
                chatTextarea.placeholder = ( answer == 'standby' ? msgs.placeholder_askSomethingElse || 'Ask something else'
                                                                 : msgs.tooltip_sendReply || 'Send reply' ) + '...'
                continueChatDiv.append(chatTextarea)
                replyForm.append(continueChatDiv) ; replySection.append(replyForm)
                appFooter.style.right = '-72px' // counteract right-offset bug from chatbar padding
                appDiv.append(replySection)

                // Create/append send button
                const sendBtn = document.createElement('button'),
                      sendSVG = icons.upArrow.create()
                sendBtn.id = 'send-btn' ; sendBtn.className = 'chatbar-btn'
                sendBtn.style.right = `${ isFirefox ? 8 : 7 }px`
                sendBtn.append(sendSVG) ; continueChatDiv.append(sendBtn)

                // Create/append shuffle button
                const shuffleBtn = document.createElement('div')
                shuffleBtn.id = 'shuffle-btn' ; shuffleBtn.className = 'chatbar-btn'
                shuffleBtn.style.right = `${ isFirefox ? 10 : 8 }px`
                const shuffleSVG = icons.shuffledArrows.create()
                shuffleBtn.append(shuffleSVG) ; continueChatDiv.append(shuffleBtn)

                // Add reply section listeners
                replyForm.onkeydown = handleEnter ; replyForm.onsubmit = handleSubmit
                chatTextarea.onmouseover = chatTextarea.onfocus = () => { if (scheme == 'dark')
                    appDiv.style.background = 'radial-gradient(ellipse at bottom, #373f4b 0%, #090a0f 100%)' }
                chatTextarea.onmouseout = chatTextarea.onblur = () => { if (scheme == 'dark' && document.activeElement != chatTextarea)
                    appDiv.style.background = 'radial-gradient(ellipse at bottom, #2f3031 0%, #090a0f 100%)' }
                chatTextarea.oninput = autosizeChatbar
                shuffleBtn.onclick = () => {
                    const randQAprompt = 'Generate a single random question on any topic then answer it.'
                                       + `${ !config.proxyAPIenabled ? 'Don\'t talk about Canberra, Tokyo, blue whales, photosynthesis,'
                                                                     + ' deserts, mindfulness meditation, the Fibonacci sequence,'
                                                                     + ' Jupiter, the Great Wall of China, Sheakespeare or da Vinci.' : '' }`
                                       + 'Try to give an answer that is 25-50 words.'
                                       + 'Do not type anything but the question and answer. Reply in markdown.'
                    chatTextarea.value = augmentQuery(randQAprompt)
                    show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
                    chatTextarea.dispatchEvent(new KeyboardEvent('keydown', {
                        key: 'Enter', bubbles: true, cancelable: true }))
                }
                if (!isMobile) { // add hover listeners for tooltips
                    sendBtn.onmouseover = sendBtn.onmouseout = toggle.tooltip
                    shuffleBtn.onmouseover = shuffleBtn.onmouseout = toggle.tooltip
                }

                // Scroll to top on mobile if user interacted
                if (isMobile && show.reply.submitSrc) {
                    document.body.scrollTop = 0 // Safari
                    document.documentElement.scrollTop = 0 // Chromium/FF/IE
                }
            }

            // Render/show answer if query sent
            if (answer != 'standby') {
                const answerPre = appDiv.querySelector('pre')
                answerPre.innerHTML = marked.parse(answer) // render markdown
                hljs.highlightAll() // highlight code

                // Typeset math
                answerPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons
                    codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';<br>') })
                const elemsToRenderMathIn = [answerPre, ...answerPre.querySelectorAll('*')]
                elemsToRenderMathIn.forEach(elem => {
                    renderMathInElement(elem, { // typeset math
                        delimiters: [
                            { left: '$$', right: '$$', display: true },
                            { left: '$', right: '$', display: false },
                            { left: '\\(', right: '\\)', display: false },
                            { left: '\\[', right: '\\]', display: true },
                            { left: '\\begin{equation}', right: '\\end{equation}', display: true },
                            { left: '\\begin{align}', right: '\\end{align}', display: true },
                            { left: '\\begin{alignat}', right: '\\end{alignat}', display: true },
                            { left: '\\begin{gather}', right: '\\end{gather}', display: true },
                            { left: '\\begin{CD}', right: '\\end{CD}', display: true },
                            { left: '\\[', right: '\\]', display: true }
                        ],
                        throwOnError: false
                })})

                if (config.stickySidebar) updateTweaksStyle() // to reset answerPre height

                // Auto-scroll if active
                if (config.autoScroll && !isMobile && config.proxyAPIenabled && !config.streamingDisabled) {
                    if (config.stickySidebar) answerPre.scrollTop = answerPre.scrollHeight
                    else window.scrollBy({ top: appDiv.querySelector('#app-chatbar').getBoundingClientRect().bottom - window.innerHeight +12 })
                }
            }

            // Focus chatbar conditionally
            if (!config.autoFocusChatbarDisabled && !show.reply.chatbarFocused // do only once if enabled
                && !isMobile // exclude mobile devices to not auto-popup OSD keyboard
                && ( appDiv.offsetHeight < window.innerHeight - appDiv.getBoundingClientRect().top )) { // app fully above fold
                    appDiv.querySelector('#app-chatbar').focus() ; show.reply.chatbarFocused = true }

            show.reply.submitSrc = 'none' // for reply section builder's mobile scroll-to-top if user interacted

            function handleEnter(event) {
                if (event.key == 'Enter' || event.keyCode == 13) {
                    if (event.ctrlKey) { // add newline
                        const chatTextarea = appDiv.querySelector('#app-chatbar'),
                              caretPos = chatTextarea.selectionStart,
                              textBefore = chatTextarea.value.substring(0, caretPos),
                              textAfter = chatTextarea.value.substring(caretPos)
                        chatTextarea.value = textBefore + '\n' + textAfter // add newline
                        chatTextarea.selectionStart = chatTextarea.selectionEnd = caretPos + 1 // preserve ibeam pos
                        autosizeChatbar()
                    } else if (!event.shiftKey) handleSubmit(event)
            }}

            function handleSubmit(event) {
                event.preventDefault()
                const chatTextarea = appDiv.querySelector('#app-chatbar')

                // No reply, change placeholder + focus chatbar
                if (chatTextarea.value.trim() == '') {
                    chatTextarea.placeholder = `${ msgs.placeholder_typeSomething || 'Type something' }...`
                    chatTextarea.focus()

                // Yes reply, submit it + transform to loading UI
                } else {

                    // Modify/submit msg chain
                    if (msgChain.length > 2) msgChain.splice(0, 2) // keep token usage maintainable
                    msgChain = stripQueryAugments(msgChain)
                    const prevReplyTrimmed = appDiv.querySelector('pre')?.textContent.substring(0, 250 - chatTextarea.value.length) || ''
                    msgChain.push({ role: 'assistant', content: prevReplyTrimmed })
                    msgChain.push({ role: 'user', content: augmentQuery(chatTextarea.value) })
                    get.reply(msgChain)

                    // Hide/remove elems
                    appDiv.querySelector('.related-queries')?.remove() // remove related queries
                    if (!isMobile) tooltipDiv.style.opacity = 0 // hide 'Send reply' tooltip post-send btn click

                    // Show loading status
                    const replySection = appDiv.querySelector('section')
                    replySection.classList.add('loading', 'no-user-select')
                    replySection.innerText = appAlerts.waitingResponse

                    show.reply.chatbarFocused = false // for auto-focus routine
                }
            }

            // Autosize chatbar function
            const chatTextarea = appDiv.querySelector('#app-chatbar'),
                  { paddingTop, paddingBottom } = getComputedStyle(chatTextarea),
                  vOffset = parseInt(paddingTop, 10) + parseInt(paddingBottom, 10)
            let prevLength = chatTextarea.value.length
            function autosizeChatbar() {
                const newLength = chatTextarea.value.length
                if (newLength < prevLength) { // if deleting txt
                    chatTextarea.style.height = 'auto' // ...auto-fit height
                    if (parseInt(getComputedStyle(chatTextarea).height, 10) < 35) // if down to one line
                        chatTextarea.style.height = '19px' // ...reset to original height
                }
                chatTextarea.style.height = chatTextarea.scrollHeight - vOffset + 'px'
                prevLength = newLength
            }
        },

        related(queries) {
            if (!show.related.greenlit) { // wait for get.reply() to finish showing answer
                show.related.statusChecker = setInterval(() => {
                    if (get.reply.status != 'waiting') {
                        show.related.greenlit = true
                        show.related(queries)
                        clearInterval(show.related.statusChecker)
                }}, 500, queries)
            } else { // show queries from latest statusChecker call
                show.related.greenlit = false
                if (queries && !appDiv.querySelector('.related-queries')) {

                    // Create/classify/append parent div
                    const relatedQueriesDiv = document.createElement('div') ; relatedQueriesDiv.className = 'related-queries'
                    appDiv.append(relatedQueriesDiv)

                    // Fill each child div, add attributes + icon + listener
                    queries.forEach((query, idx) => {
                        const relatedQueryDiv = document.createElement('div'),
                              relatedQuerySVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                              relatedQuerySVGpath = document.createElementNS('http://www.w3.org/2000/svg','path')

                        // Add attributes
                        relatedQueryDiv.title = msgs.tooltip_sendRelatedQuery || 'Send related query'
                        relatedQueryDiv.classList.add('related-query', 'fade-in', 'no-user-select')
                        relatedQueryDiv.setAttribute('tabindex', 0)
                        relatedQueryDiv.textContent = query

                        // Create icon
                        for (const [attr, value] of [
                            ['viewBox', '0 0 24 24'], ['width', 18], ['height', 18], ['fill', 'currentColor']
                        ]) relatedQuerySVG.setAttribute(attr, value)
                        relatedQuerySVGpath.setAttribute('d',
                            'M16 10H6.83L9 7.83l1.41-1.41L9 5l-6 6 6 6 1.41-1.41L9 14.17 6.83 12H16c1.65 0 3 1.35 3 3v4h2v-4c0-2.76-2.24-5-5-5z')
                        relatedQuerySVG.style.transform = 'rotate(180deg)' // flip arrow upside down

                        // Assemble/insert elems
                        relatedQuerySVG.append(relatedQuerySVGpath) ; relatedQueryDiv.prepend(relatedQuerySVG)
                        relatedQueriesDiv.append(relatedQueryDiv)

                        // Add fade + listeners
                        setTimeout(() => {
                            relatedQueryDiv.classList.add('active')
                            relatedQueryDiv.onclick = relatedQueryDiv.onkeydown = handleRQevent
                        }, idx * 100)
                    })

                    updateTweaksStyle() // to shorten <pre> max-height
                    appFooter.style.right = 0 // reset show.reply()'s counteract right-offset bug from chatbar padding
        }}}
    }

    // Run MAIN routine

    registerMenu() // create browser toolbar menu

    // Init ALERTS
    const appAlerts = {
        waitingResponse:  `${ msgs.alert_waitingResponse || 'Waiting for ChatGPT response' }...`,
        login:            `${ msgs.alert_login || 'Please login' } @ `,
        checkCloudflare:  `${ msgs.alert_checkCloudflare || 'Please pass Cloudflare security check' } @ `,
        tooManyRequests:  `${ msgs.alert_tooManyRequests || 'API is flooded with too many requests' }.`,
        parseFailed:      `${ msgs.alert_parseFailed || 'Failed to parse response JSON' }.`,
        proxyNotWorking:  `${ msgs.mode_proxy || 'Proxy Mode' } ${ msgs.alert_notWorking || 'is not working' }.`,
        openAInotWorking: `OpenAI API ${ msgs.alert_notWorking || 'is not working' }.`,
        suggestProxy:     `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }`,
        suggestOpenAI:    `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOff || 'switching off' } ${ msgs.mode_proxy || 'Proxy Mode' }`
    }

    // Pre-load LOGO
    const appLogoImg = document.createElement('img') ; updateAppLogoSrc() 
    appLogoImg.onload = () => { appLogoImg.loaded = true ; updateTitleAnchor() }

    // Create/ID/classify/listenerize DDGPT container
    const appDiv = document.createElement('div') ; appDiv.id = 'ddgpt' ; appDiv.classList.add('fade-in')
    appDiv.addEventListener(inputEvents.down, event => { // to dismiss visible font size slider
        let elem = event.target
        while (elem && !(elem.id?.includes('font-size'))) // find font size elem parent to exclude handling down event
            elem = elem.parentNode
        if (!elem && appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')
    })

    // Stylize APP elems
    const appStyle = createStyle() ; updateAppStyle() ; 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 }'
                    + '#app-chatbar { width: 85.6% }',
          ssbStyles = '#ddgpt { position: sticky ; top: 14px }'
                    + '#ddgpt ~ * { display: none }' // hide sidebar contents
                    + 'body, div.site-wrapper { overflow: clip }' // replace `overflow: hidden` to allow stickiness
    updateTweaksStyle() ; document.head.append(tweaksStyle)

    // Create/stylize TOOLTIPs
    if (!isMobile) {
        var tooltipDiv = document.createElement('div') ; tooltipDiv.classList.add('btn-tooltip', 'no-user-select')
        document.head.append(createStyle('.btn-tooltip {'
            + 'background-color: rgba(0, 0, 0, 0.64) ; padding: 4px 6px ; border-radius: 6px ; border: 1px solid #d9d9e3 ;' // bubble style
            + 'font-size: 0.87em ; color: white ;' // font style
            + 'position: absolute ;' // for updateTooltip() calcs
            + 'box-shadow: 3px 5px 16px 0px rgb(0 0 0 / 21%) ;' // drop shadow
            + 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 9999 }' // visibility
        ))
    }
 
    // 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
    appFooter.append(footerContent)

    // APPEND DDGPT + footer to DDG
    const appElems = [appFooter, appDiv],
          hostContainer = document.querySelector(isMobile || isCentered ? '[data-area*="mainline"]'
                                                                        : '[class*="sidebar"]')
    appElems.forEach(elem => hostContainer.prepend(elem))
    appElems.toReversed().forEach((elem, idx) => // fade in staggered
        setTimeout(() => elem.classList.add('active'), idx * 550 - 200))

    // REPLACE hostContainer max-width w/ min-width for better UI
    if (!isMobile) { hostContainer.style.maxWidth = '' ; hostContainer.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(config.appName.toLowerCase(), 'i')
            const currentDate = (() => { // in YYYYMMDD format
                const today = new Date(), year = today.getFullYear(),
                      month = String(today.getMonth() + 1).padStart(2, '0'),
                      day = String(today.getDate()).padStart(2, '0')
                return year + month + day
            })()

            // Select random, active advertiser
            for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData))))
                if (details.campaigns.text) { chosenAdvertiser = advertiser ; break }

            // Fetch a random, active creative
            if (chosenAdvertiser) {
                const campaignsURL = 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/'
                                   + chosenAdvertiser + '/text/campaigns.json'
                get.json(campaignsURL, (err, campaignsData) => { if (err) return

                    // Select random, active campaign
                    for (const [campaignName, campaign] of shuffle(applyBoosts(Object.entries(campaignsData)))) {
                        const campaignIsActive = campaign.active && (!campaign.endDate || currentDate <= campaign.endDate)
                        if (!campaignIsActive) continue // to next campaign since campaign inactive

                        // Select random active group
                        for (const [groupName, adGroup] of shuffle(applyBoosts(Object.entries(campaign.adGroups)))) {

                            // Skip disqualified groups
                            if (/^self$/i.test(groupName) && !re_appName.test(campaignName) // self-group for other apps
                                || re_appName.test(campaignName) && !/^self$/i.test(groupName) // non-self group for this app
                                || adGroup.active == false // group explicitly disabled
                                || adGroup.targetBrowsers && // target browser(s) exist...
                                    !adGroup.targetBrowsers.some( // ...but doesn't match user's
                                        browser => new RegExp(browser, 'i').test(navigator.userAgent))
                                || adGroup.targetLocations && ( // target locale(s) exist...
                                    !config.userLocale || !adGroup.targetLocations.some( // ...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', config.appName.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
            }
    })

    // 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) {
                const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
                get.related(lastQuery).then(queries => show.related(queries))
                    .catch(err => { consoleErr(err.message)
                        if (get.related.status != 'done') api.tryNew(get.related) })
            }
    } else { appAlert('waitingResponse') ; get.reply(msgChain) }

    // Observe for DDG SCHEME CHANGES to update DDGPT scheme if auto-scheme mode 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) { scheme = newScheme ; updateAppLogoSrc() ; updateAppStyle() ; updateStars() }
    }

})()