Greasy Fork is available in English.
AI chat toolkit — export, Obsidian sync, Enter-as-newline, Caveman mode, Claude usage tracker, settings dashboard
// ==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,``));
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(/&/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}\``).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(/&/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(/&/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);
})();