lance

AI chat toolkit — export, Obsidian sync, Enter-as-newline, Caveman mode, Claude usage tracker, settings dashboard

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         lance
// @namespace    https://github.com/SolRaze/lance
// @version      0.1.0
// @description  AI chat toolkit — export, Obsidian sync, Enter-as-newline, Caveman mode, Claude usage tracker, settings dashboard
// @author       SolRaze
// @homepageURL  https://github.com/SolRaze/lance
// @supportURL   https://github.com/SolRaze/lance/issues
// @license      MIT
// @include      *://chatgpt.com/*
// @include      *://grok.com/*
// @include      *://gemini.google.com/*
// @include      *://claude.ai/*
// @include      *://chat.deepseek.com/*
// @include      *://deepseek.com/*
// @include      *://yuanbao.tencent.com/*
// @noframes
// @run-at       document-idle
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      127.0.0.1
// @connect      claude.ai
// ==/UserScript==

(function () {
    'use strict';

    // ─── Platform ────────────────────────────────────────────────────────────────
    const host = window.location.hostname;
    const P =
        host.includes("chatgpt.com")         ? "chatGPT"  :
        host.includes("grok.com")            ? "grok"     :
        host.includes("gemini.google.com")   ? "gemini"   :
        host.includes("claude.ai")           ? "claude"   :
        host.includes("deepseek.com")        ? "deepseek" :
        host.includes("yuanbao.tencent.com") ? "yuanbao"  : "unknown";

    const qs  = (sel, root = document) => root.querySelector(sel);
    const qsa = (sel, root = document) => [...root.querySelectorAll(sel)];
    function mkEl(tag, opts = {}) {
        const el = document.createElement(tag);
        if (opts.html)      el.innerHTML   = opts.html;
        if (opts.text)      el.textContent = opts.text;
        if (opts.className) el.className   = opts.className;
        if (opts.style)     Object.assign(el.style, opts.style);
        return el;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    //  SETTINGS  — deep-merge on load so new keys survive script updates
    // ═══════════════════════════════════════════════════════════════════════════
    const DEFAULTS = {
        sites:         { chatGPT: true, grok: true, gemini: true, claude: true, deepseek: true, yuanbao: true },
        shortcuts:     { ctrl: true, meta: true, alt: false },
        obsFolder:     "Chat",
        obsTabCloseMs: 1500,
        caveman:       { enabled: false, level: 'ultra' },
        usageTracker:  true,   // Claude inline usage tracker
    };

    function deepMerge(defaults, saved) {
        const out = Object.assign({}, defaults);
        for (const k of Object.keys(defaults)) {
            if (saved[k] !== undefined) {
                if (typeof defaults[k] === 'object' && !Array.isArray(defaults[k]) && defaults[k] !== null)
                    out[k] = Object.assign({}, defaults[k], saved[k]);
                else
                    out[k] = saved[k];
            }
        }
        return out;
    }

    function loadCfg() {
        try {
            const s = GM_getValue("lance_cfg");
            if (s) return deepMerge(DEFAULTS, JSON.parse(s));
        } catch(_) {}
        return JSON.parse(JSON.stringify(DEFAULTS));
    }
    function saveCfg(c) { GM_setValue("lance_cfg", JSON.stringify(c)); }
    let CFG = loadCfg();

    // ═══════════════════════════════════════════════════════════════════════════
    //  CAVEMAN MODE
    // ═══════════════════════════════════════════════════════════════════════════
    const CAVEMAN_PROMPTS = {
        lite:  `[Caveman lite] Respond without filler or hedging. Keep full sentences and articles. Professional but tight. No pleasantries.\n\n---\n\n`,
        full:  `[Caveman full] Respond terse like smart caveman. Drop articles, fragments OK, short synonyms. Technical terms exact. Code blocks unchanged.\n\n---\n\n`,
        ultra: `[Caveman ultra] CAVEMAN ULTRA. Maximum compression. Short phrases. No filler. No intro/outro. No repetition. Keep all technical facts. Preserve code, commands, errors, paths, names, URLs, numbers, and API names exactly. Use compact bullets. Do not omit important warnings.\n\n---\n\n`,
    };

    function getChatInput() {
        if (P === "chatGPT")  return qs('#prompt-textarea');
        if (P === "claude")   return qs('div.ProseMirror') || qs('[contenteditable="true"][data-placeholder]');
        if (P === "gemini")   return qs('rich-textarea .ql-editor') || qs('div[contenteditable="true"]');
        if (P === "deepseek") return qs('textarea#chat-input') || qs('textarea');
        if (P === "grok")     return qs('textarea');
        if (P === "yuanbao")  return qs('textarea');
        return qs('textarea') || qs('div[contenteditable="true"]');
    }

    function getInputText(el) {
        return (el.tagName === 'TEXTAREA' ? el.value : (el.innerText || el.textContent || '')).trim();
    }

    function prependToInput(el, prefix) {
        el.focus();
        if (el.tagName === 'TEXTAREA') {
            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
            const cur = el.value;
            if (nativeSetter) nativeSetter.call(el, prefix + cur);
            else el.value = prefix + cur;
            el.selectionStart = el.selectionEnd = prefix.length;
            el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true }));
        } else {
            const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
            const firstText = walker.nextNode();
            const sel = window.getSelection();
            if (!sel) return;
            const range = document.createRange();
            if (firstText) range.setStart(firstText, 0);
            else range.setStart(el, 0);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
            const ok = document.execCommand('insertText', false, prefix);
            if (!ok) {
                try {
                    const dt = new DataTransfer();
                    dt.setData('text/plain', prefix);
                    el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
                } catch(_) {
                    el.textContent = prefix + (el.textContent || '');
                    el.dispatchEvent(new InputEvent('input', { bubbles: true }));
                }
            }
        }
    }

    function applyCavemanIfActive() {
        if (!CFG.caveman?.enabled) return false;
        const el = getChatInput();
        if (!el) return false;
        const cur = getInputText(el);
        if (!cur) return false;
        if (cur.startsWith('[Caveman')) return false;
        prependToInput(el, CAVEMAN_PROMPTS[CFG.caveman.level || 'ultra']);
        return true;
    }

    // ── Caveman button ────────────────────────────────────────────────────────
    let cavemanBox = null;

    function updateCavemanPill() {
        if (!cavemanBox) return;
        const on  = CFG.caveman?.enabled;
        const lvl = (CFG.caveman?.level || 'ultra').toUpperCase();
        const label = cavemanBox.querySelector('#lance-cave-label');
        if (label) label.textContent = on ? `◈ ${lvl}` : '◈ CAVE';
        cavemanBox.style.background = on ? 'rgba(255,255,255,0.92)' : 'rgba(24,24,27,0.9)';
        cavemanBox.style.color      = on ? '#111' : 'rgba(255,255,255,0.85)';
        cavemanBox.style.boxShadow  = on
            ? '0 4px 20px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.2)'
            : '0 4px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.07)';
    }

    function initCavemanPill() {
        if (qs('#lance-cave-box')) { updateCavemanPill(); return; }

        const box = mkEl('div', { className: 'ai-export-drag-box' });
        box.id = 'lance-cave-box';
        box.innerHTML = `<div style="display:flex;align-items:center;gap:7px;pointer-events:none;">
            <span id="lance-cave-label" style="font-size:13px;font-weight:700;letter-spacing:0.05em">◈ CAVE</span>
        </div>`;

        const menu = mkEl('div', { className: 'ai-export-menu-panel' });

        menu.appendChild(mkEl('div', { className: 'ai-export-section-label', text: 'Caveman Mode' }));

        const toggleBtn = mkEl('button', { className: 'ai-export-menu-item' });
        const updateToggleBtn = () => {
            const on = CFG.caveman?.enabled;
            toggleBtn.innerHTML = `<span style="flex:1">${on ? 'Enabled' : 'Disabled'}</span><span class="ai-export-badge">${on ? 'ON' : 'OFF'}</span>`;
        };
        updateToggleBtn();
        toggleBtn.onclick = e => {
            e.stopPropagation();
            if (!CFG.caveman) CFG.caveman = { enabled: false, level: 'ultra' };
            CFG.caveman.enabled = !CFG.caveman.enabled;
            saveCfg(CFG); updateCavemanPill(); updateToggleBtn();
        };
        menu.appendChild(toggleBtn);

        menu.appendChild(mkEl('div', { className: 'ai-export-menu-divider' }));
        menu.appendChild(mkEl('div', { className: 'ai-export-section-label', text: 'Level' }));

        [['lite','Lite','Tight prose, no filler'],['full','Full','Terse, fragments OK'],['ultra','Ultra','Max compression']].forEach(([val,name,desc]) => {
            const btn = mkEl('button', { className: 'ai-export-menu-item cave-lvl-btn' });
            const refresh = () => {
                const active = (CFG.caveman?.level || 'ultra') === val;
                btn.innerHTML = `<span style="flex:1">${name}<span style="display:block;font-size:10px;opacity:0.45;font-weight:400">${desc}</span></span><span class="ai-export-badge">${active ? '●' : ''}</span>`;
                btn.style.color = active ? '#fff' : '';
            };
            refresh(); btn._refresh = refresh;
            btn.onclick = e => {
                e.stopPropagation();
                if (!CFG.caveman) CFG.caveman = { enabled: false, level: 'ultra' };
                CFG.caveman.level = val;
                saveCfg(CFG); updateCavemanPill();
                menu.querySelectorAll('.cave-lvl-btn').forEach(b => b._refresh?.());
                menu.style.display = 'none';
            };
            menu.appendChild(btn);
        });

        box.appendChild(menu);
        document.body.appendChild(box);
        cavemanBox = box;

        const sx = GM_getValue('cx', window.innerWidth  - 160);
        const sy = GM_getValue('cy', window.innerHeight - 55);
        box.style.left = Math.max(0, Math.min(sx, window.innerWidth  - 120)) + 'px';
        box.style.top  = Math.max(0, Math.min(sy, window.innerHeight -  40)) + 'px';
        updateCavemanPill();

        let drag=false, moved=false, dX0, dY0, iL, iT;
        box.onmousedown = e => { drag=true; moved=false; dX0=e.clientX; dY0=e.clientY; iL=box.offsetLeft; iT=box.offsetTop; e.preventDefault(); };
        document.addEventListener('mousemove', e => {
            if (!drag) return;
            const dx=e.clientX-dX0, dy=e.clientY-dY0;
            if (Math.abs(dx)>3||Math.abs(dy)>3) moved=true;
            box.style.left=(iL+dx)+'px'; box.style.top=(iT+dy)+'px';
        });
        document.addEventListener('mouseup', () => {
            if (drag&&moved) { GM_setValue('cx',box.offsetLeft); GM_setValue('cy',box.offsetTop); }
            drag=false;
        });

        box.onclick = () => {
            if (moved) return;
            if (menu.style.display !== 'flex') {
                const rect=box.getBoundingClientRect(), isB=rect.top>window.innerHeight/2, isR=rect.left>window.innerWidth/2;
                menu.className = 'ai-export-menu-panel';
                menu.classList.add(isB ? isR ? 'pos-bottom-right':'pos-bottom-left' : isR ? 'pos-top-right':'pos-top-left');
                menu.querySelectorAll('.cave-lvl-btn').forEach(b => b._refresh?.());
                updateToggleBtn();
                menu.style.display = 'flex';
            } else {
                menu.style.display = 'none';
            }
        };
        document.addEventListener('click', e => { if (!cavemanBox?.contains(e.target)) menu.style.display = 'none'; });

        // Mouse-click send intercept — capture phase, preventDefault, re-fire after inject
        document.addEventListener('click', e => {
            if (!CFG.caveman?.enabled) return;
            const sb = findSubmit();
            if (!sb || !(e.target===sb || sb.contains(e.target))) return;
            const el = getChatInput();
            if (!el) return;
            const cur = getInputText(el);
            if (!cur || cur.startsWith('[Caveman')) return;
            e.preventDefault();
            e.stopImmediatePropagation();
            applyCavemanIfActive();
            setTimeout(() => sb.click(), 30);
        }, true);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    //  DEEPSEEK SCRAPER
    //  ds-virtual-list renders ~4 items at a time (172176px total / ~1400px each
    //  ≈ 121 messages). Unmounts items scrolled past. Must collect-while-scrolling.
    //  settle() observes document.body (not vl) — virtual list mutations happen
    //  on ds-virtual-list-items which may not be a direct child of vl.
    //  STEP sized to show 1 new virtual item per step (~1400px item height).
    //  stall limit high enough for 172k/1400px = ~123 steps needed.
    // ═══════════════════════════════════════════════════════════════════════════
    async function getDeepSeekContents() {
        const vl = qs('div.ds-virtual-list') ||
            (() => { let b=null,bH=0; qsa('div').forEach(el=>{if(el.scrollHeight>el.clientHeight+100&&el.scrollHeight>bH){bH=el.scrollHeight;b=el;}}); return b; })();
        if (!vl) { console.warn('[lance] DeepSeek: container not found'); return []; }
        console.log('[lance] DeepSeek: container scrollH=' + vl.scrollHeight + ', clientH=' + vl.clientHeight);

        // Observe document.body for virtual list re-renders (ds-virtual-list-items
        // is nested — body-level observation catches all mutations reliably)
        function settle(ms) {
            return new Promise(resolve => {
                const cap = ms || 500;
                let t = setTimeout(resolve, cap);
                const obs = new MutationObserver(() => {
                    clearTimeout(t);
                    t = setTimeout(() => { obs.disconnect(); resolve(); }, 150);
                });
                obs.observe(document.body, { childList: true, subtree: true });
                setTimeout(() => { obs.disconnect(); resolve(); }, cap);
            });
        }

        const seen = new WeakSet();
        const aMsgs = [], uMsgs = [];
        const seenUser = new Set();
        const BTN_SEL = 'div.ds-flex > div.ds-icon-button:nth-child(1)';
        const USR_SEL = 'div[class*="fbb737a4"]';

        async function collectVisible() {
            qsa(USR_SEL).forEach(el => {
                const t = el.textContent.trim();
                if (t && !seenUser.has(t)) { seenUser.add(t); uMsgs.push(t); }
            });
            for (const btn of qsa(BTN_SEL)) {
                if (seen.has(btn)) continue;
                seen.add(btn);
                btn.click();
                await new Promise(r => setTimeout(r, 350));
                try { const t = await navigator.clipboard.readText(); if (t) aMsgs.push(t); } catch(_) {}
            }
        }

        // Scroll to top, wait for top items to render
        vl.scrollTop = 0;
        await settle(700);
        await collectVisible();
        console.log('[lance] DeepSeek step 0: u=' + uMsgs.length + ' a=' + aMsgs.length);

        // Each virtual item ~1400px tall. Step = 1200px to ensure overlap (no gaps).
        // atBottom uses 200px tolerance — the _871cbca sentinel div at bottom
        // prevents scrollTop from ever reaching scrollHeight-clientHeight exactly.
        const STEP = 1200;
        let prev = -1, stalls = 0, step = 0;

        while (true) {
            const maxScroll = vl.scrollHeight - vl.clientHeight;
            const atBottom  = vl.scrollTop >= maxScroll - 200;
            if (atBottom) break;

            vl.scrollTop += STEP;
            step++;
            await settle(step < 5 ? 600 : 500);
            await collectVisible();

            const total = uMsgs.length + aMsgs.length;
            console.log('[lance] DeepSeek step ' + step + ' scrollTop=' + Math.round(vl.scrollTop)
                + '/' + vl.scrollHeight + ' u=' + uMsgs.length + ' a=' + aMsgs.length);

            if (total === prev) {
                stalls++;
                // If stalled but not at bottom yet, jump forward aggressively
                if (stalls >= 15) {
                    const remaining = maxScroll - vl.scrollTop;
                    if (remaining > 500) {
                        console.log('[lance] DeepSeek: stall — jumping +' + Math.round(remaining/2) + 'px');
                        vl.scrollTop += remaining / 2;
                        stalls = 0;
                        await settle(800);
                        await collectVisible();
                    } else {
                        console.warn('[lance] DeepSeek: stall limit near bottom');
                        break;
                    }
                }
            } else {
                stalls = 0; prev = total;
            }
        }

        // Final collect at bottom
        vl.scrollTop = vl.scrollHeight;
        await settle(600);
        await collectVisible();
        console.log('[lance] DeepSeek: final u=' + uMsgs.length + ' a=' + aMsgs.length);

        const result = [];
        const pairs = Math.min(uMsgs.length, aMsgs.length);
        for (let i = 0; i < pairs; i++) {
            result.push({ role: 'user',      text: uMsgs[i] });
            result.push({ role: 'assistant', text: aMsgs[i] });
        }
        for (let i = pairs; i < aMsgs.length; i++) result.push({ role: 'assistant', text: aMsgs[i] });
        console.log('[lance] DeepSeek: done — ' + result.length + ' messages (' + pairs + ' pairs)');
        return result;
    }

    // ─── DeepSeek pair grouping ──────────────────────────────────────────────────
    // DeepSeek can emit multiple assistant turns per user turn (thinking + answer).
    // Group by user: each user message + ALL following assistant messages = one pair.
    // Returns [{q, a}] where a = all assistant texts joined with \n\n
    function groupDeepSeekPairs(items) {
        const pairs = [];
        let currentQ = null, currentA = [];
        for (const item of items) {
            if (item.role === 'user') {
                if (currentQ !== null) pairs.push({ q: currentQ, a: currentA.join('\n\n') });
                currentQ = item.text; currentA = [];
            } else if (item.role === 'assistant') {
                if (currentQ === null) currentQ = ''; // assistant-first edge case
                currentA.push(item.text);
            }
        }
        if (currentQ !== null && currentA.length) pairs.push({ q: currentQ, a: currentA.join('\n\n') });
        return pairs;
    }

    // ─── Filename ────────────────────────────────────────────────────────────────
    function makeFilename(title, turnCount) {
        const d = new Date();
        return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}${String(turnCount).padStart(4,'0')}_${title}`;
    }
    function sanitize(t) { return (t||document.title||"Export").trim().replace(/[\/\\\?\%\*\:\|"<>\.]/g,"_"); }
    function getTitle() {
        if (P==="chatGPT")  return sanitize(qs("#history a[data-active]")?.textContent);
        if (P==="gemini")   return sanitize(qs("conversations-list div.selected")?.textContent||document.title.replace(/ - Google Gemini$/,'').trim().slice(0,30));
        if (P==="deepseek") {
            const byZ=qsa('[style*="z-index"],div').find(el=>getComputedStyle(el).zIndex==="12");
            return sanitize(byZ?.textContent||qs('div[class*="chat-item--active"] span,li[class*="active"] .title,a[class*="active"] span')?.textContent);
        }
        if (P==="yuanbao") return sanitize(qs("span.agent-dialogue__content--common__header__name__title")?.textContent);
        return sanitize(document.title);
    }

    // ─── HTML → Markdown ─────────────────────────────────────────────────────────
    function toMd(html) {
        const doc=new DOMParser().parseFromString(html,"text/html");
        const isGemini=P==="gemini",isGrok=P==="grok",isChatGPT=P==="chatGPT",isClaude=P==="claude",isDS=P==="deepseek";
        if(!isGemini) qsa("span.katex-html",doc).forEach(e=>e.remove());
        qsa("mrow",doc).forEach(e=>e.remove());
        qsa('annotation[encoding="application/x-tex"]',doc).forEach(e=>e.replaceWith(e.closest(".katex-display")?`\n$$\n${e.textContent.trim()}\n$$\n`:`$${e.textContent.trim()}$`));
        const rp=(el,txt)=>el.parentNode.replaceChild(document.createTextNode(txt),el);
        qsa("strong,b",doc).forEach(e=>rp(e,`**${e.textContent}**`));
        qsa("em,i",doc).forEach(e=>rp(e,`*${e.textContent}*`));
        qsa("p code",doc).forEach(e=>rp(e,`\`${e.textContent}\``));
        qsa("a",doc).forEach(e=>rp(e,`[${e.textContent}](${e.href})`));
        qsa("img",doc).forEach(e=>rp(e,`![${e.alt}](${e.src})`));
        if(isChatGPT){qsa("pre",doc).forEach(pre=>{const type=qs("div>div:first-child",pre)?.textContent||"";const code=qs("div>div:nth-child(3)>code",pre)?.textContent||pre.textContent;pre.innerHTML=`\n\`\`\`${type}\n${code}\n\`\`\`\n`;});}
        else if(isGrok){qsa("div.not-prose",doc).forEach(d=>{const type=qs("div>div>span",d)?.textContent||"";const code=qs("div>div:nth-child(3)>code",d)?.textContent||d.textContent;d.innerHTML=`\n\`\`\`${type}\n${code}\n\`\`\`\n`;});}
        else if(isGemini){qsa("code-block",doc).forEach(d=>{const type=qs("div>div>span",d)?.textContent||"";const code=qs("div>div:nth-child(2)>div>pre",d)?.textContent||d.textContent;d.innerHTML=`\n\`\`\`${type}\n${code}\n\`\`\`\n`;});}
        else if(isClaude){qsa("pre",doc).forEach(pre=>{const code=qs("code",pre);const type=code?Array.from(code.classList).find(c=>c.startsWith("language-"))?.replace("language-","")||"":"";pre.innerHTML=`\n\`\`\`${type}\n${code?code.textContent:pre.textContent}\n\`\`\`\n`;});}
        else if(isDS){qsa("pre",doc).forEach(pre=>{const code=qs("code",pre);let type=code?Array.from(code.classList).find(c=>c.startsWith("language-"))?.replace("language-","")||"":"";if(!type)type=qs('span.code-lang,span[class*="lang"],div[class*="code-header"] span',pre.closest("div"))?.textContent.trim()||"";pre.innerHTML=`\n\`\`\`${type}\n${code?code.textContent:pre.textContent}\n\`\`\`\n`;});qsa('div[class*="think"],details.think,div.ds-think',doc).forEach(e=>rp(e,`\n> **[Thinking]**\n${e.textContent.trim().split("\n").map(l=>`> ${l}`).join("\n")}\n`));}
        qsa("ul",doc).forEach(ul=>rp(ul,"\n"+qsa(":scope>li",ul).map(li=>`- ${li.textContent.trim()}`).join("\n")));
        qsa("ol",doc).forEach(ol=>rp(ol,"\n"+qsa(":scope>li",ol).map((li,i)=>`${i+1}. ${li.textContent.trim()}`).join("\n")));
        for(let i=1;i<=6;i++) qsa(`h${i}`,doc).forEach(h=>rp(h,`\n${"#".repeat(i)} ${h.textContent}\n`));
        qsa("p",doc).forEach(p=>rp(p,`\n${p.textContent}\n`));
        return doc.body.innerHTML.replace(/<[^>]*>/g,"").replace(/&amp;/g,"&").trim();
    }

    // ─── Attachments ─────────────────────────────────────────────────────────────
    function extractAttachments(msgEl){const seen=new Set(),out=[];qsa("img[src]",msgEl).forEach(img=>{const src=img.src||"";if(src&&!seen.has(src)&&!src.includes("avatar")&&!src.includes("icon")&&src!==window.location.href){seen.add(src);out.push({name:img.alt||"image",type:"image",src});}});qsa('[data-testid*="file-thumbnail"],[class*="FileAttachment"],[class*="file-name"],[class*="attachment-name"]',msgEl).forEach(el=>{const name=(el.querySelector('[class*="name"],span,p')||el).textContent.trim();if(name&&name.length<200&&!seen.has(name)){seen.add(name);out.push({name,type:"file",src:null});}});return out;}
    function renderAttachmentsMd(a){if(!a.length)return "";return "\n**Attachments:**\n"+a.map(x=>x.type==="image"?`![${x.name}](${x.src})`:`- \`${x.name}\``).join("\n")+"\n";}

    // ─── getElements ─────────────────────────────────────────────────────────────
    function getElements(){const res=[];if(P==="chatGPT")res.push(...qsa("article"));else if(P==="grok")res.push(...qsa("div.message-bubble"));else if(P==="gemini"){const q=qsa("user-query-content"),r=qsa("model-response");q.forEach((x,i)=>{res.push(x);if(r[i])res.push(r[i]);});}else if(P==="claude")res.push(...qsa('[data-testid="user-message"],.font-claude-response'));else if(P==="yuanbao")res.push(...qsa("div.agent-chat__list__item"));return res;}

    // ─── File export ─────────────────────────────────────────────────────────────
    async function fileExport(fmt){
        let c="",m="text/plain",title,fname;
        if(P==="deepseek"){
            const items=await getDeepSeekContents();if(!items.length)return;
            title=getTitle();const pl=groupDeepSeekPairs(items);
            fname=makeFilename(title,pl.length);
            if(fmt==="json"){c=JSON.stringify(pl,null,2);m="application/json";}
            else if(fmt==="csv"){c="Q,A\n"+pl.map(p=>`"${p.q.replace(/"/g,'""')}","${p.a.replace(/"/g,'""')}"`).join("\n");m="text/csv";}
            else if(fmt==="html"){c=`<html><body style="font-family:sans-serif;max-width:800px;margin:auto;padding:30px;line-height:1.7;">${pl.map(p=>`<div style="background:#f4f4f5;padding:15px;border-radius:12px;margin:20px 0;"><b>Q:</b> ${p.q}</div><div><b>A:</b> ${p.a}</div><hr/>`).join("")}</body></html>`;m="text/html";}
            else if(fmt==="md"){c=pl.map(p=>`\n# Q:\n${p.q}\n\n# A:\n${p.a}\n\n---\n`).join("");m="text/markdown";}
            else{c=pl.map(p=>`\nQ:\n${p.q}\n\nA:\n${p.a}\n\n---\n`).join("");}
        } else {
            const res=getElements();if(!res.length)return;
            title=getTitle();fname=makeFilename(title,Math.floor(res.length/2));
            const md=el=>toMd(el.innerHTML),txt=el=>el.textContent.trim();
            if(fmt==="json"){c=JSON.stringify(res.reduce((a,x,i)=>{if(i%2===0&&res[i+1])a.push({q:md(x),a:md(res[i+1])});return a;},[]),null,2);m="application/json";}
            else if(fmt==="csv"){c="Q,A\n"+res.reduce((a,x,i)=>{if(i%2===0&&res[i+1])a+=`"${md(x).replace(/"/g,'""')}","${md(res[i+1]).replace(/"/g,'""')}"\n`;return a;},"");m="text/csv";}
            else if(fmt==="html"){c=`<html><body style="font-family:sans-serif;max-width:800px;margin:auto;padding:30px;line-height:1.7;">${res.reduce((a,x,i)=>{if(i%2===0&&res[i+1])a+=`<div style="background:#f4f4f5;padding:15px;border-radius:12px;margin:20px 0;"><b>Q:</b> ${x.innerHTML}</div><div><b>A:</b> ${res[i+1].innerHTML}</div><hr/>`;return a;},"")}</body></html>`;m="text/html";}
            else if(fmt==="md"){c=res.reduce((a,x,i)=>{if(i%2===0&&res[i+1])a+=`\n# Q:\n${md(x)}\n\n# A:\n${md(res[i+1])}\n\n---\n`;return a;},"");m="text/markdown";}
            else{c=res.reduce((a,x,i)=>{if(i%2===0&&res[i+1])a+=`\nQ:\n${txt(x)}\n\nA:\n${txt(res[i+1])}\n\n---\n`;return a;},"");}
        }
        const u=URL.createObjectURL(new Blob([c.replace(/&amp;/g,"&")],{type:m}));
        const a=Object.assign(document.createElement("a"),{href:u,download:`${fname}.${fmt}`});
        document.body.appendChild(a);a.click();
        setTimeout(()=>{document.body.removeChild(a);URL.revokeObjectURL(u);},0);
    }

    // ─── Obsidian export ──────────────────────────────────────────────────────────
    async function obsidianExport(){
        let pairs_data=[], title, pairCount;

        if(P==="deepseek"){
            const items=await getDeepSeekContents();
            if(!items.length){alert("No conversation found!");return;}
            title=getTitle();
            pairs_data=groupDeepSeekPairs(items);
            pairCount=pairs_data.length;
        } else {
            const res=getElements();
            if(!res.length){alert("No conversation found!");return;}
            title=getTitle();
            pairCount=Math.floor(res.length/2);
            for(let i=0;i<res.length-1;i+=2){
                if(!res[i+1]) break;
                const att=extractAttachments(res[i]);
                const qText=toMd(res[i].innerHTML)+(att.length?renderAttachmentsMd(att):'');
                const aText=toMd(res[i+1].innerHTML);
                pairs_data.push({q:qText,a:aText});
            }
        }

        if(!pairs_data.length){alert("No conversation found!");return;}

        // Clean minimal Obsidian body — no emoji headers, just Q/A blocks with dividers
        let body='';
        pairs_data.forEach((p,i)=>{
            body += `## User\n\n${p.q.trim()}\n\n## Assistant\n\n${p.a.trim()}\n\n---\n\n`;
        });

        const fname=makeFilename(title,pairCount);
        const yaml=["---",
            `title: "${title}"`,
            `date: "${new Date().toISOString()}"`,
            `source: ${P}`,
            `url: "${document.URL}"`,
            `turns: ${pairCount}`,
            "tags:",
            "  - chat",
            `  - ${P}`,
            "---","",""].join("\n");

        GM_setClipboard(yaml+body.replace(/&amp;/g,"&"));
        const folder=encodeURIComponent(CFG.obsFolder+"/"+P);
        const obsUrl=`obsidian://new?file=${folder}%2F${encodeURIComponent(fname)}&clipboard`;
        GM_xmlhttpRequest({
            method:'POST',url:'http://127.0.0.1:27184/obsidian',
            headers:{'Content-Type':'application/json'},
            data:JSON.stringify({uri:obsUrl}),timeout:1500,
            onload(r){try{if(JSON.parse(r.responseText).ok)return;}catch(_){}_obsidianTabFallback(obsUrl);},
            onerror(){_obsidianTabFallback(obsUrl);},
            ontimeout(){_obsidianTabFallback(obsUrl);},
        });
    }
    function _obsidianTabFallback(u){const tab=GM_openInTab(u,{active:false,insert:true});if(CFG.obsTabCloseMs>0&&tab&&typeof tab.close==="function")setTimeout(()=>tab.close(),CFG.obsTabCloseMs);}

    // ═══════════════════════════════════════════════════════════════════════════
    //  ENTER-AS-NEWLINE
    // ═══════════════════════════════════════════════════════════════════════════
    function getEventTarget(e){return e.composedPath?e.composedPath()[0]||e.target:e.target;}
    function isComposing(e){return e.isComposing||e.keyCode===229;}
    function isEditableTarget(t){return /INPUT|TEXTAREA|SELECT/.test(t.tagName)||(t.getAttribute&&t.getAttribute("contenteditable")==="true");}
    function isChatGPTTarget(t){return t.id==="prompt-textarea"||t.closest("#prompt-textarea")||(t.getAttribute&&t.getAttribute("contenteditable")==="true");}
    function isSendShortcut(e){if(e.key!=="Enter")return false;const sc=CFG.shortcuts;return(sc.ctrl&&e.ctrlKey&&!e.altKey&&!e.metaKey)||(sc.alt&&e.altKey&&!e.ctrlKey&&!e.metaKey)||(sc.meta&&e.metaKey&&!e.ctrlKey&&!e.altKey);}
    function isPotentialSend(e){if(e.key!=="Enter")return false;return(e.ctrlKey&&!e.altKey&&!e.metaKey&&!e.shiftKey)||(e.altKey&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey)||(e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey);}
    function findSubmit(){
        if(P==="chatGPT")return qs('button[data-testid="send-button"]');
        if(P==="gemini") return qs('button[aria-label*="Send"],button[aria-label*="发送"],button[aria-label*="傳送"]');
        if(P==="deepseek"){const bc=qs(".bf38813a");if(!bc)return null;const btns=qsa('.ds-icon-button[role="button"]',bc);for(let i=btns.length-1;i>=0;i--){const b=btns[i];if(b.getAttribute("aria-disabled")!=="true"&&!b.classList.contains("ds-icon-button--disabled"))return b;}return null;}
        if(P==="claude") return qs('button[aria-label*="Send"]');
        if(P==="grok")   return qs('button[type="submit"]');
        return null;
    }
    window.addEventListener("keydown",e=>{
        if(isComposing(e))return;const t=getEventTarget(e);
        if(P==="chatGPT"){
            if(e.key==="Enter"&&!e.ctrlKey&&!e.shiftKey&&!e.metaKey&&!e.altKey&&isChatGPTTarget(t)){e.stopPropagation();e.preventDefault();const ev=new KeyboardEvent("keydown",{key:"Enter",code:"Enter",shiftKey:true,bubbles:true,cancelable:true});t.dispatchEvent(ev);if(!ev.defaultPrevented)document.execCommand("insertParagraph");return;}
            if(isSendShortcut(e)&&isChatGPTTarget(t)){applyCavemanIfActive();const sb=findSubmit();if(sb&&!sb.disabled){e.preventDefault();e.stopPropagation();sb.click();}return;}
            if(isPotentialSend(e)&&isChatGPTTarget(t)){e.preventDefault();e.stopPropagation();}
            return;
        }
        if(e.key==="Enter"&&!e.ctrlKey&&!e.shiftKey&&!e.metaKey&&!e.altKey&&isEditableTarget(t)){
            e.preventDefault();e.stopPropagation();
            if(t.tagName==="TEXTAREA"){const s=t.selectionStart,v=t.value;t.value=v.substring(0,s)+"\n"+v.substring(t.selectionEnd);t.selectionStart=t.selectionEnd=s+1;t.dispatchEvent(new Event("input",{bubbles:true}));}
            else{const ev=new KeyboardEvent("keydown",{key:"Enter",code:"Enter",shiftKey:true,bubbles:true,cancelable:true});t.dispatchEvent(ev);if(!ev.defaultPrevented)document.execCommand("insertParagraph");}
            return;
        }
        if(isSendShortcut(e)&&isEditableTarget(t)){applyCavemanIfActive();const sb=findSubmit();if(sb&&!sb.disabled){e.preventDefault();e.stopPropagation();sb.click();}return;}
        if(isPotentialSend(e)&&isEditableTarget(t)){e.stopPropagation();}
    },true);
    window.addEventListener("keypress",e=>{
        if(P==="chatGPT"||isComposing(e))return;
        if(e.key==="Enter"&&!e.ctrlKey&&!e.shiftKey&&!e.metaKey&&!e.altKey){const t=getEventTarget(e);if(isEditableTarget(t))e.stopPropagation();}
        if(isPotentialSend(e)){const t=getEventTarget(e);if(isEditableTarget(t))e.stopPropagation();}
    },true);

    // ═══════════════════════════════════════════════════════════════════════════
    //  CLAUDE USAGE TRACKER  (ported from Claude Inline Usage Tracker v2.7)
    //  Only active on claude.ai. Toggle via CFG.usageTracker.
    //  Original: https://greasyfork.org/scripts/567949
    // ═══════════════════════════════════════════════════════════════════════════
    const UT = (() => {
        if (P !== 'claude') return { init(){} };

        const ID='lance-cut',SID='lance-cut-style',API='/api/organizations';
        const POLL=60_000,HOVER_REFRESH=30_000,MIN_GAP=15_000,WARN=60,DANGER=80;
        const A='lance-cut-anchor',H='lance-cut-hover';
        const ROWS=[['five_hour','Current Session'],['seven_day','Weekly Limit (All)'],['seven_day_opus','Weekly Limit (Opus)']];
        const S={org:null,inflight:null,last:null,lastAt:0,anchor:null,ui:null,poll:0,sched:0,mo:null};
        const clamp=v=>(v=+v||0)<0?0:v>100?100:v;
        const fmt=iso=>{if(!iso)return'N/A';const m=Math.round((new Date(iso).getTime()-Date.now())/60000);if(m<1)return'Resetting soon';if(m<60)return`In ${m} min`;const h=(m/60)|0;return h<24?`In ${h} hr`:`In ${(h/24)|0} days`;};
        const jget=u=>fetch(u,{credentials:'include'}).then(r=>{if(!r.ok)throw new Error(r.status);return r.json();});

        async function orgId(){if(S.org)return S.org;const orgs=await jget(API);return(S.org=orgs?.[0]?.uuid??null);}
        function getUsage(force){
            const now=Date.now();
            if(!force&&now-S.lastAt<MIN_GAP)return Promise.resolve(S.last);
            if(S.inflight)return S.inflight;
            return(S.inflight=(async()=>{try{const id=await orgId();if(!id)return S.last;const d=await jget(`${API}/${id}/usage`);if(d){S.last=d;S.lastAt=Date.now();}return S.last;}catch(e){S.org=null;return S.last;}finally{S.inflight=null;}})());
        }

        function injectStyle(){
            if(document.getElementById(SID))return;
            const s=document.createElement('style');s.id=SID;
            s.textContent=`
#${ID}{position:absolute;inset:auto 16px -15px;z-index:30;font-family:var(--font-ui,system-ui,-apple-system,sans-serif);color:hsl(var(--text-100))}
#${ID} .t{height:12px;display:flex;align-items:center;cursor:pointer}
#${ID} .b{width:100%;height:3px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden;transition:height .16s}
#${ID} .t:hover .b{height:4px}
#${ID} .f{height:100%;width:0%;background:hsl(var(--brand-000));transition:width .25s}
#${ID} .fw{background:hsl(var(--warning-100))}
#${ID} .fd{background:hsl(var(--danger-100))}
#${ID} .p{position:absolute;bottom:14px;left:0;right:0;background:hsl(var(--bg-000));border-radius:16px;display:flex;flex-direction:column;gap:10px;padding:12px 14px 10px;box-shadow:0 .25rem 1.25rem hsl(var(--always-black)/3.5%),0 0 0 .5px hsla(var(--border-300)/.15);opacity:0;visibility:hidden;pointer-events:none;transform:translateY(8px);transition:opacity .16s,transform .16s,visibility 0s linear .16s}
#${ID} .t:hover + .p{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .16s,transform .16s}
#${ID} .hh{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;margin-bottom:6px;font-size:13px}
#${ID} .l{font-weight:550;color:hsl(var(--text-100))}
#${ID} .m{font-size:12px;font-weight:430;color:hsl(var(--text-500));white-space:nowrap}
#${ID} .k{width:100%;height:6px;background:hsla(var(--border-300)/.12);border-radius:999px;overflow:hidden}
.${A}{transition:background-color .2s,box-shadow .2s,border-color .2s}
.${A}.${H}{background-color:transparent!important;box-shadow:none!important;border-color:transparent!important}
.${A}>:not(#${ID}){transition:opacity .2s}
.${A}.${H}>:not(#${ID}){opacity:0!important;pointer-events:none!important}`;
            document.head.appendChild(s);
        }

        function clsFor(p){return p>DANGER?'f fd':p>WARN?'f fw':'f';}
        function setFill(el,p){const sp=''+p;if(el.dataset.p!==sp){el.dataset.p=sp;el.style.width=sp+'%';const c=clsFor(p);if(el.className!==c)el.className=c;}}

        function buildUI(){
            const root=document.createElement('div');root.id=ID;
            root.innerHTML=`<div class="t"><div class="b"><div class="f" data-role="tf"></div></div></div><div class="p">${ROWS.map(([,label],i)=>`<div class="r" data-i="${i}"><div class="hh"><span class="l">${label}</span><span class="m" data-role="m"></span></div><div class="k"><div class="f" data-role="f"></div></div></div>`).join('')}</div>`;
            const tf=root.querySelector('[data-role="tf"]');
            const rEls=[...root.querySelectorAll('.r')];
            const metas=rEls.map(r=>r.querySelector('[data-role="m"]'));
            const fills=rEls.map(r=>r.querySelector('[data-role="f"]'));
            root.addEventListener('pointerenter',()=>{S.anchor&&S.anchor.classList.add(H);if(Date.now()-S.lastAt>HOVER_REFRESH)doRefresh(1);},{passive:true});
            root.addEventListener('pointerleave',()=>{S.anchor&&S.anchor.classList.remove(H);},{passive:true});
            return{root,tf,rEls,metas,fills};
        }

        function render(d){
            if(!S.ui||!d)return;
            setFill(S.ui.tf,clamp(d?.five_hour?.utilization));
            for(let i=0;i<ROWS.length;i++){const key=ROWS[i][0];const b=d?.[key];const row=S.ui.rEls[i];if(!b){row.hidden=true;continue;}row.hidden=false;const p=clamp(b.utilization);setFill(S.ui.fills[i],p);const t=`${p}% · ${fmt(b.resets_at)}`;const m=S.ui.metas[i];if(m.dataset.t!==t){m.dataset.t=t;m.textContent=t;}}
        }

        async function doRefresh(force){if(!S.ui||(!force&&document.hidden))return;render(await getUsage(!!force));}

        function findAnchor(){
            const ed=document.querySelector('[contenteditable="true"].tiptap');if(!ed)return null;
            const fs=ed.closest('fieldset');if(!fs)return null;
            return fs.querySelector('div[class*="bg-bg-000"][class*="rounded-[20px]"]')||fs;
        }

        function attach(){
            if(!CFG.usageTracker){document.getElementById(ID)?.remove();return;}
            const a=findAnchor();if(!a)return;
            const existing=document.getElementById(ID);
            if(a===S.anchor&&existing&&a.contains(existing))return;
            existing?.remove();
            a.classList.add(A);
            if(getComputedStyle(a).position==='static')a.style.position='relative';
            S.anchor=a;S.ui=buildUI();
            a.insertBefore(S.ui.root,a.firstChild);
            doRefresh(1);
        }

        function schedAttach(){if(S.sched)return;const cb=()=>{S.sched=0;attach();};S.sched=window.requestIdleCallback?requestIdleCallback(cb,{timeout:800}):requestAnimationFrame(cb);}
        function startPoll(){stopPoll();const tick=()=>{if(document.hidden){S.poll=0;return;}doRefresh(0);S.poll=setTimeout(tick,POLL);};S.poll=setTimeout(tick,POLL);}
        function stopPoll(){S.poll&&clearTimeout(S.poll);S.poll=0;}

        return {
            init(){
                injectStyle();
                const patch=m=>{const o=history[m];history[m]=function(){const r=o.apply(this,arguments);schedAttach();return r;};};
                patch('pushState');patch('replaceState');
                addEventListener('popstate',schedAttach,{passive:true});
                addEventListener('hashchange',schedAttach,{passive:true});
                let t=0;
                S.mo=new MutationObserver(()=>{if(t)return;t=setTimeout(()=>{t=0;schedAttach();},200);});
                S.mo.observe(document.body,{childList:true,subtree:true});
                document.addEventListener('visibilitychange',()=>{if(document.hidden)stopPoll();else{schedAttach();doRefresh(1);startPoll();}},{passive:true});
                addEventListener('focus',()=>!document.hidden&&doRefresh(1),{passive:true});
                schedAttach();startPoll();
            },
            refresh(){ schedAttach(); },
        };
    })();

    // ═══════════════════════════════════════════════════════════════════════════
    //  SETTINGS DASHBOARD
    // ═══════════════════════════════════════════════════════════════════════════
    function openDashboard(){
        const existing=qs('#lance-dashboard');
        if(existing){existing.remove();qs('#lance-overlay')?.remove();return;}

        const bg="#18181b",bg3="#27272c",fg="#e4e4e8",fg2="rgba(228,228,232,0.5)",bd="rgba(255,255,255,0.07)",wht="#ffffff";

        const ov=document.createElement('div');ov.id='lance-overlay';
        Object.assign(ov.style,{position:'fixed',inset:'0',background:'rgba(0,0,0,0.6)',zIndex:'2147483645',backdropFilter:'blur(2px)'});
        ov.onclick=()=>{ov.remove();dlg.remove();};
        document.body.appendChild(ov);

        const dlg=document.createElement('div');dlg.id='lance-dashboard';
        Object.assign(dlg.style,{position:'fixed',top:'50%',left:'50%',transform:'translate(-50%,-50%)',background:bg,color:fg,border:`1px solid ${bd}`,borderRadius:'14px',padding:'20px 24px 24px',width:'360px',maxWidth:'94vw',maxHeight:'88vh',overflowY:'auto',zIndex:'2147483646',fontFamily:'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',fontSize:'13px',lineHeight:'1.5',boxShadow:'0 24px 64px rgba(0,0,0,0.6),0 0 0 1px rgba(255,255,255,0.05)',scrollbarWidth:'thin',scrollbarColor:`${bg3} transparent`});

        const rowEl=(label,control)=>{const d=document.createElement('div');Object.assign(d.style,{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'9px 0',borderBottom:`1px solid ${bd}`});const la=document.createElement('span');la.textContent=label;la.style.color=fg;d.appendChild(la);if(control)d.appendChild(control);return d;};

        const toggle=(val,onChange)=>{
            const lbl=document.createElement('label');Object.assign(lbl.style,{position:'relative',display:'inline-block',width:'34px',height:'18px',flexShrink:'0'});
            const inp=document.createElement('input');inp.type='checkbox';inp.checked=val;Object.assign(inp.style,{opacity:'0',width:'0',height:'0',position:'absolute'});
            const sl=document.createElement('span');Object.assign(sl.style,{position:'absolute',inset:'0',borderRadius:'18px',cursor:'pointer',background:val?wht:'rgba(255,255,255,0.12)',transition:'background 0.18s',border:'1px solid rgba(255,255,255,0.1)'});
            const dot=document.createElement('span');Object.assign(dot.style,{position:'absolute',height:'12px',width:'12px',left:val?'18px':'3px',bottom:'2px',background:val?'#111':'rgba(255,255,255,0.4)',borderRadius:'50%',transition:'left 0.18s,background 0.18s'});
            sl.appendChild(dot);
            inp.onchange=()=>{const v=inp.checked;sl.style.background=v?wht:'rgba(255,255,255,0.12)';dot.style.left=v?'18px':'3px';dot.style.background=v?'#111':'rgba(255,255,255,0.4)';onChange(v);};
            lbl.appendChild(inp);lbl.appendChild(sl);return lbl;
        };

        const section=t=>{const d=document.createElement('div');Object.assign(d.style,{fontSize:'10px',fontWeight:'700',letterSpacing:'0.1em',textTransform:'uppercase',color:fg2,padding:'18px 0 6px'});d.textContent=t;return d;};

        const textInput=(val,onInput,opts={})=>{
            const inp=document.createElement('input');inp.value=val;if(opts.type)inp.type=opts.type;if(opts.min)inp.min=opts.min;if(opts.max)inp.max=opts.max;if(opts.step)inp.step=opts.step;
            Object.assign(inp.style,{background:bg3,border:`1px solid rgba(255,255,255,0.1)`,borderRadius:'6px',color:fg,padding:'5px 9px',fontSize:'12px',width:opts.width||'100%',boxSizing:'border-box',textAlign:opts.align||'left',outline:'none'});
            inp.addEventListener('focus',()=>{inp.style.borderColor='rgba(255,255,255,0.25)';});
            inp.addEventListener('blur',()=>{inp.style.borderColor='rgba(255,255,255,0.1)';});
            inp.oninput=()=>onInput(inp.value);return inp;
        };

        // Header
        const hdr=document.createElement('div');Object.assign(hdr.style,{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'2px'});
        const htitle=document.createElement('div');Object.assign(htitle.style,{display:'flex',alignItems:'baseline',gap:'8px'});
        const hname=document.createElement('span');hname.textContent='lance';Object.assign(hname.style,{fontSize:'17px',fontWeight:'700',color:wht});
        const hver=document.createElement('span');hver.textContent='v0.1.0';Object.assign(hver.style,{fontSize:'10px',color:fg2});
        htitle.appendChild(hname);htitle.appendChild(hver);
        const closeBtn=document.createElement('button');closeBtn.textContent='✕';Object.assign(closeBtn.style,{background:'none',border:'none',color:fg2,cursor:'pointer',fontSize:'16px',padding:'0',lineHeight:'1',transition:'color 0.1s'});
        closeBtn.addEventListener('mouseenter',()=>{closeBtn.style.color=wht;});closeBtn.addEventListener('mouseleave',()=>{closeBtn.style.color=fg2;});
        closeBtn.onclick=()=>{dlg.remove();ov.remove();};
        hdr.appendChild(htitle);hdr.appendChild(closeBtn);dlg.appendChild(hdr);
        const sub=document.createElement('div');sub.textContent='All changes save instantly.';Object.assign(sub.style,{fontSize:'11px',color:fg2,marginBottom:'4px'});dlg.appendChild(sub);

        // ── Sites — collapsible dropdown ──
        dlg.appendChild(section('Export button — sites'));
        const SITE_LABELS={chatGPT:'ChatGPT',grok:'Grok',gemini:'Gemini',claude:'Claude',deepseek:'DeepSeek',yuanbao:'Yuanbao'};

        // Dropdown toggle button
        const sitesDropBtn=document.createElement('button');
        Object.assign(sitesDropBtn.style,{display:'flex',justifyContent:'space-between',alignItems:'center',width:'100%',padding:'9px 0',background:'none',border:'none',borderBottom:`1px solid ${bd}`,color:fg,fontSize:'13px',cursor:'pointer',outline:'none'});
        const sitesDropLabel=document.createElement('span');
        const countOn=()=>Object.values(CFG.sites).filter(Boolean).length;
        sitesDropLabel.textContent=`${countOn()} / ${Object.keys(SITE_LABELS).length} sites enabled`;
        const sitesArrow=document.createElement('span');sitesArrow.textContent='▾';Object.assign(sitesArrow.style,{fontSize:'11px',color:fg2,transition:'transform 0.15s'});
        sitesDropBtn.appendChild(sitesDropLabel);sitesDropBtn.appendChild(sitesArrow);
        dlg.appendChild(sitesDropBtn);

        // Collapsible sites list
        const sitesPanel=document.createElement('div');
        Object.assign(sitesPanel.style,{overflow:'hidden',maxHeight:'0',transition:'max-height 0.2s ease'});
        let sitesOpen=false;
        sitesDropBtn.onclick=()=>{
            sitesOpen=!sitesOpen;
            sitesPanel.style.maxHeight=sitesOpen?'300px':'0';
            sitesArrow.style.transform=sitesOpen?'rotate(180deg)':'rotate(0deg)';
        };
        Object.entries(SITE_LABELS).forEach(([key,label])=>{
            const t=toggle(CFG.sites[key]??true,v=>{CFG.sites[key]=v;saveCfg(CFG);sitesDropLabel.textContent=`${countOn()} / ${Object.keys(SITE_LABELS).length} sites enabled`;});
            sitesPanel.appendChild(rowEl(label,t));
        });
        dlg.appendChild(sitesPanel);

        // ── Keyboard ──
        dlg.appendChild(section('Send shortcut (+ Enter)'));
        [['ctrl','Ctrl + Enter'],['meta','Cmd / Win + Enter'],['alt','Alt / Option + Enter']].forEach(([key,label])=>{
            dlg.appendChild(rowEl(label,toggle(CFG.shortcuts[key]??DEFAULTS.shortcuts[key],v=>{CFG.shortcuts[key]=v;saveCfg(CFG);})));
        });

        // ── Obsidian ──
        dlg.appendChild(section('Obsidian'));

        // Relay status
        const relayRow=document.createElement('div');Object.assign(relayRow.style,{padding:'9px 0',borderBottom:`1px solid ${bd}`,display:'flex',justifyContent:'space-between',alignItems:'center'});
        const relayLbl=document.createElement('span');relayLbl.textContent='lance-relay';relayLbl.style.color=fg;
        const relayStatus=document.createElement('span');relayStatus.textContent='checking…';Object.assign(relayStatus.style,{fontSize:'11px',color:fg2});
        relayRow.appendChild(relayLbl);relayRow.appendChild(relayStatus);dlg.appendChild(relayRow);
        GM_xmlhttpRequest({method:'GET',url:'http://127.0.0.1:27184/ping',timeout:1200,
            onload(r){try{if(JSON.parse(r.responseText).ok){relayStatus.textContent='● online';relayStatus.style.color='rgba(255,255,255,0.75)';return;}}catch(_){}relayStatus.textContent='● offline';relayStatus.style.color='rgba(255,255,255,0.25)';},
            onerror(){relayStatus.textContent='● offline';relayStatus.style.color='rgba(255,255,255,0.25)';},
            ontimeout(){relayStatus.textContent='● offline';relayStatus.style.color='rgba(255,255,255,0.25)';}});

        const folderWrap=document.createElement('div');Object.assign(folderWrap.style,{padding:'9px 0',borderBottom:`1px solid ${bd}`});
        const folderLbl=document.createElement('div');folderLbl.textContent='Vault folder';Object.assign(folderLbl.style,{marginBottom:'6px',color:fg,fontSize:'12px'});
        folderWrap.appendChild(folderLbl);folderWrap.appendChild(textInput(CFG.obsFolder,v=>{CFG.obsFolder=v.trim()||"Chat";saveCfg(CFG);}));
        dlg.appendChild(folderWrap);
        dlg.appendChild(rowEl('Fallback tab close (ms, 0=off)',textInput(String(CFG.obsTabCloseMs),v=>{CFG.obsTabCloseMs=Math.max(0,parseInt(v)||0);saveCfg(CFG);},{type:'number',min:'0',max:'10000',step:'100',width:'72px',align:'right'})));

        // ── Caveman ──
        dlg.appendChild(section('Caveman mode'));
        dlg.appendChild(rowEl('Enable',toggle(CFG.caveman?.enabled??false,v=>{if(!CFG.caveman)CFG.caveman={enabled:false,level:'ultra'};CFG.caveman.enabled=v;saveCfg(CFG);updateCavemanPill();})));
        const lvlRow=document.createElement('div');Object.assign(lvlRow.style,{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'9px 0',borderBottom:`1px solid ${bd}`});
        const lvlLbl=document.createElement('span');lvlLbl.textContent='Level';lvlLbl.style.color=fg;
        const lvlSel=document.createElement('select');Object.assign(lvlSel.style,{background:bg3,border:'1px solid rgba(255,255,255,0.1)',borderRadius:'6px',color:fg,padding:'4px 8px',fontSize:'12px',outline:'none'});
        ['lite','full','ultra'].forEach(l=>{const opt=document.createElement('option');opt.value=l;opt.textContent=l.charAt(0).toUpperCase()+l.slice(1);if((CFG.caveman?.level||'ultra')===l)opt.selected=true;lvlSel.appendChild(opt);});
        lvlSel.onchange=()=>{if(!CFG.caveman)CFG.caveman={enabled:false,level:'ultra'};CFG.caveman.level=lvlSel.value;saveCfg(CFG);updateCavemanPill();};
        lvlRow.appendChild(lvlLbl);lvlRow.appendChild(lvlSel);dlg.appendChild(lvlRow);

        // ── Claude Usage Tracker (only shown on claude.ai) ──
        if (P === 'claude') {
            dlg.appendChild(section('Claude usage tracker'));
            dlg.appendChild(rowEl('Show inline usage bar',toggle(CFG.usageTracker??true,v=>{CFG.usageTracker=v;saveCfg(CFG);UT.refresh();})));
        }

        const note=document.createElement('p');note.textContent='Cave button: click → menu → toggle or select level.';Object.assign(note.style,{margin:'12px 0 0',fontSize:'10px',color:fg2,textAlign:'center'});dlg.appendChild(note);
        document.body.appendChild(dlg);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    //  STYLES
    // ═══════════════════════════════════════════════════════════════════════════
    GM_addStyle(`
        .ai-export-drag-box{position:fixed;z-index:2147483646;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(24,24,27,0.9);backdrop-filter:blur(14px);color:rgba(255,255,255,0.85);border-radius:100px;box-shadow:0 4px 24px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.07);cursor:move;user-select:none;padding:9px 18px;font-family:system-ui;font-size:13px;font-weight:600;transition:transform 0.15s,box-shadow 0.15s;white-space:nowrap;}
        .ai-export-drag-box:hover{transform:scale(1.04);color:#fff;box-shadow:0 6px 30px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.12);}
        .ai-export-menu-panel{position:absolute;width:max-content;min-width:185px;background:#18181b;border:1px solid rgba(255,255,255,0.07);border-radius:12px;padding:4px;display:none;flex-direction:column;gap:1px;box-shadow:0 12px 40px rgba(0,0,0,0.6);}
        .pos-bottom-right{bottom:calc(100% + 12px);right:0;transform-origin:bottom right;animation:aiPopUp .2s cubic-bezier(.16,1,.3,1);}
        .pos-bottom-left{bottom:calc(100% + 12px);left:0;transform-origin:bottom left;animation:aiPopUp .2s cubic-bezier(.16,1,.3,1);}
        .pos-top-right{top:calc(100% + 12px);right:0;transform-origin:top right;animation:aiPopDown .2s cubic-bezier(.16,1,.3,1);}
        .pos-top-left{top:calc(100% + 12px);left:0;transform-origin:top left;animation:aiPopDown .2s cubic-bezier(.16,1,.3,1);}
        @keyframes aiPopUp{0%{opacity:0;transform:scale(.94) translateY(6px)}100%{opacity:1;transform:scale(1) translateY(0)}}
        @keyframes aiPopDown{0%{opacity:0;transform:scale(.94) translateY(-6px)}100%{opacity:1;transform:scale(1) translateY(0)}}
        .ai-export-menu-item{display:flex;align-items:center;padding:9px 12px;background:transparent;border:none;border-radius:8px;text-align:left;cursor:pointer;color:rgba(228,228,232,0.75);font-size:12px;font-weight:500;transition:background .1s,color .1s,transform .08s;width:100%;white-space:nowrap;letter-spacing:0.01em;}
        .ai-export-menu-item:hover{background:rgba(255,255,255,0.07);color:#fff;}
        .ai-export-menu-item:active,.ai-export-menu-item.clicked{transform:scale(.95);opacity:.6;}
        .ai-export-menu-divider{height:1px;background:rgba(255,255,255,0.06);margin:2px 6px;}
        .ai-export-section-label{font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:rgba(255,255,255,0.25);padding:7px 12px 2px;}
        .ai-export-badge{margin-left:auto;font-size:9px;font-weight:700;letter-spacing:.04em;font-family:monospace;color:rgba(255,255,255,0.2);}
    `);

    // ─── Export button UI ─────────────────────────────────────────────────────
    function init(){
        if(CFG.sites[P]===false){qs('.ai-export-drag-box[id="lance-export-box"]')?.remove();return;}
        if(qs('#lance-export-box'))return;

        const box=mkEl("div",{className:"ai-export-drag-box"});
        box.id='lance-export-box';
        box.innerHTML=`<div style="display:flex;align-items:center;gap:7px;pointer-events:none;"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg><span>Export</span></div>`;

        const menu=mkEl("div",{className:"ai-export-menu-panel"});
        const addLabel=t=>menu.appendChild(Object.assign(document.createElement('div'),{className:'ai-export-section-label',textContent:t}));
        const addDiv=()=>menu.appendChild(mkEl("div",{className:"ai-export-menu-divider"}));
        const addBtn=(icon,label,badge,fn)=>{
            const btn=mkEl("button",{className:'ai-export-menu-item'});
            btn.innerHTML=`<span style="font-family:monospace;font-size:10px;width:16px;text-align:center;opacity:.45;flex-shrink:0">${icon}</span><span style="margin-left:4px">${label}</span><span class="ai-export-badge">${badge}</span>`;
            btn.onclick=e=>{e.stopPropagation();btn.classList.add('clicked');setTimeout(()=>{btn.classList.remove('clicked');menu.style.display='none';fn();},160);};
            menu.appendChild(btn);
        };

        addLabel("Download");
        addBtn('#','Markdown','.MD',()=>fileExport('md'));
        addBtn('{}','JSON','.JSON',()=>fileExport('json'));
        addBtn(',','CSV','.CSV',()=>fileExport('csv'));
        addBtn('T','Plain text','.TXT',()=>fileExport('txt'));
        addBtn('<>','HTML','.HTML',()=>fileExport('html'));
        addDiv();
        addLabel("Integrations");
        addBtn('◆','Obsidian','.MD',()=>obsidianExport());
        addDiv();
        addBtn('⚙','Settings','',()=>openDashboard());

        box.appendChild(menu);document.body.appendChild(box);

        const sX=GM_getValue('x',window.innerWidth-160),sY=GM_getValue('y',window.innerHeight-100);
        box.style.left=Math.max(0,Math.min(sX,window.innerWidth-120))+'px';
        box.style.top=Math.max(0,Math.min(sY,window.innerHeight-60))+'px';

        let drag=false,moved=false,sX0,sY0,iL,iT;
        box.onmousedown=e=>{drag=true;moved=false;sX0=e.clientX;sY0=e.clientY;iL=box.offsetLeft;iT=box.offsetTop;e.preventDefault();};
        document.onmousemove=e=>{if(!drag)return;const dx=e.clientX-sX0,dy=e.clientY-sY0;if(Math.abs(dx)>3||Math.abs(dy)>3)moved=true;box.style.left=(iL+dx)+'px';box.style.top=(iT+dy)+'px';};
        document.onmouseup=()=>{if(drag&&moved){GM_setValue('x',box.offsetLeft);GM_setValue('y',box.offsetTop);}drag=false;};
        box.onclick=()=>{
            if(moved)return;
            if(menu.style.display!=='flex'){
                const rect=box.getBoundingClientRect(),isB=rect.top>window.innerHeight/2,isR=rect.left>window.innerWidth/2;
                menu.className='ai-export-menu-panel';
                menu.classList.add(isB?isR?'pos-bottom-right':'pos-bottom-left':isR?'pos-top-right':'pos-top-left');
                menu.style.display='flex';
            } else menu.style.display='none';
        };
        document.addEventListener("click",e=>{if(!box.contains(e.target))menu.style.display='none';});
    }

    GM_registerMenuCommand("⚙ Settings",openDashboard);

    if(typeof trustedTypes!=="undefined"&&trustedTypes.defaultPolicy===null)
        trustedTypes.createPolicy("default",{createHTML:s=>s,createScriptURL:s=>s,createScript:s=>s});

    setTimeout(()=>{init();initCavemanPill();UT.init();},1000);
    setInterval(()=>{init();initCavemanPill();updateCavemanPill();},3000);
})();