ChatGPT Bubble Theme Pro

自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距、...+G)。文字颜色支持“自动/手动”,发送按钮箭头跟随“自动文字”;修复关闭、重置为默认、监听归拢;面板更紧凑,鼠标保持系统箭头。

目前為 2025-08-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ChatGPT Bubble Theme Pro
// @namespace    https://greasyfork.org/zh-CN/users/1503226-loom29
// @version      1.8.7
// @author       Ech0
// @description  自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距、...+G)。文字颜色支持“自动/手动”,发送按钮箭头跟随“自动文字”;修复关闭、重置为默认、监听归拢;面板更紧凑,鼠标保持系统箭头。
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
(function () {
  "use strict";

  // --- Mobile safe mode: avoid DOM moving on mobile to prevent white screen on mobile ---
  const IS_MOBILE = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) ||
                    (window.matchMedia && matchMedia("(max-width: 820px)").matches);
  const SAFE_MODE = !!IS_MOBILE; // On mobile: do not wrap/move DOM, only apply CSS

  /* ------------------- 主题(alpha=0.30,soft=10) ------------------- */
  const THEMES = {
    "海盐": { mode:"diffuse", angle:135, colors:["#89d0d2","#a8ced7","#88b9ce"], weights:[34,33,33], alpha:0.25, blur:10, glow:0, outlineColor:"#78afb0", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2d3d43", soft:10, autoText:false },
    "春日": { mode:"diffuse", angle:130, colors:["#dff0b7","#e5e5a4","#b0cda7"], weights:[34,33,33], alpha:0.45, blur:9, glow:0, outlineColor:"#b0cda7", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", autoText:false },
    "蜜桃": { mode:"diffuse", angle:125, colors:["#f0dcd9","#ebbeb3","#f0dcc9"], weights:[34,33,33], alpha:0.35, blur:8, glow:0, outlineColor:"#e6a08e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#47352e", autoText:false },
    "洋芋": { mode:"diffuse", angle:125, colors:["#9898c5","#b7cfe5","#b9b5c5"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#9189be", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#4d4860", autoText:false },
    "奶油": { mode:"diffuse", angle:125, colors:["#f0e6d0","#e2dfca","#ecd8c1"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#c8b493", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#5c5951", autoText:false },
    "湖畔": { mode:"diffuse", angle:135, colors:["#76b9d5","#a9d3d6","#34b7a1"], weights:[34,33,33], alpha:0.70, blur:10, glow:0, outlineColor:"#65b3b8", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#0f232a", autoText:false },
    "热异常": { mode:"linear", angle:180, colors:["#dcd6d6","#eab06d","#e95f28"], weights:[27,47,26], alpha:1, blur:9, glow:0, outlineColor:"#610505", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", autoText:true },
    "水泥": { mode:"diffuse", angle:135, colors:["#4d5b6f","#626e7a","#4f5863"], weights:[34,33,33], alpha:0.85, blur:10, glow:0, outlineColor:"#0d3f5e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "墨玉": { mode:"diffuse", angle:135, colors:["#3c5639","#274b37","#2a5026"], weights:[34,33,33], alpha:0.75, blur:10, glow:0, outlineColor:"#0d631f", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "红酒": { mode:"diffuse", angle:135, colors:["#640707","#2f0909","#570a29"], weights:[34,33,33], alpha:0.90, blur:10, glow:0, outlineColor:"#651406", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "纯黑": { mode:"solid", angle:135, colors:["#000000","#000000","#000000"], weights:[100,0,0], alpha:1.00, blur:10, glow:0, outlineColor:"#000000", outlineAlpha:0, outlineWidth:0, radius:16, padV:8, padH:12, textColor:"#ffffff", soft:10, autoText:true }
  };
  const DEFAULT_STYLE = clone(THEMES["蜜桃"]);

  /* ------------------- 配置 ------------------- */
  const DEFAULTS = {
    targets: { user:true, assistant:false, sendbtn:true },
    followOfSendBtn: "user",
    user: clone(THEMES["海盐"]),
    assistant: clone(THEMES["海盐"]),
    lastTheme: { user:"海盐", assistant:"海盐" },
    overrides: { user:{}, assistant:{} },
    customThemes: {},
    galleryOrder: Object.keys(THEMES),
    panel: { x:null, y:null, w:560, h:560 }
  };

  /* ------------------- 工具 ------------------- */
  function clone(x){ return JSON.parse(JSON.stringify(x)); }
  const clamp=(v,min,max)=>Math.min(max,Math.max(min,v));
  const pad2=(n)=>n.toString(16).padStart(2,"0");
  const hexToRgb=(hex)=>{ let h=(hex||"").replace("#","").trim(); if(h.length===3) h=h.split("").map(x=>x+x).join(""); const n=parseInt(h||"000000",16); return { r:(n>>16)&255, g:(n>>8)&255, b:n&255 }; };
  const rgba=(hex,a)=>{ const { r,g,b }=hexToRgb(hex); return `rgba(${r},${g},${b},${clamp(a,0,1)})`; };
  const load=()=>{ try{ const s=GM_getValue("ech0_theme_pro"); if(s) return Object.assign(clone(DEFAULTS), s);}catch{} return clone(DEFAULTS); };
  const save=(cfg)=>{ try{ GM_setValue("ech0_theme_pro", cfg); }catch{} };

  function hslToRgb(h,s,l){
    h=((h%360)+360)%360; s/=100; l/=100;
    const c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs((h/60)%2-1)), m=l-c/2;
    let r=0,g=0,b=0;
    if(h<60){ r=c; g=x; }
    else if(h<120){ r=x; g=c; }
    else if(h<180){ g=c; b=x; }
    else if(h<240){ g=x; b=c; }
    else if(h<300){ r=x; b=c; }
    else { r=c; b=x; }
    return { r:Math.round((r+m)*255), g:Math.round((g+m)*255), b:Math.round((b+m)*255) };
  }
  function parseColorToHex(s){
    if(!s) return null;
    s=s.trim();
    let m=s.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
    if(m){ let h=m[1]; if(h.length===3) h=h.split("").map(x=>x+x).join(""); return "#"+h.toLowerCase(); }
    m=s.match(/^rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
    if(m){ const r=clamp(Math.round(+m[1]),0,255), g=clamp(Math.round(+m[2]),0,255), b=clamp(Math.round(+m[3]),0,255); return "#"+pad2(r)+pad2(g)+pad2(b); }
    m=s.match(/^hsla?\s*\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/i);
    if(m){ const { r,g,b }=hslToRgb(+m[1],+m[2],+m[3]); return "#"+pad2(r)+pad2(g)+pad2(b); }
    return null;
  }
  function relLuma(hex){
    const { r,g,b }=hexToRgb(hex);
    const sr=[r,g,b].map(v=>v/255).map(v=>v<=0.04045? v/12.92 : Math.pow((v+0.055)/1.055,2.4));
    return 0.2126*sr[0]+0.7152*sr[1]+0.0722*sr[2];
  }
  function themeLuma(s){
    const w=s.weights||[1,1,1], sum=w.reduce((a,b)=>a+b,0)||1;
    const cols=(s.colors||[]).filter(Boolean); let L=0;
    cols.forEach((c,i)=>{ L+=relLuma(c)*(w[i]||0)/sum; });
    return L;
  }
  function pickAutoTextColor(s){ return themeLuma(s)>0.55 ? "#000000" : "#ffffff"; }

  /* ------------------- 背景 ------------------- */
  const SOFT=10;
  const MAX_SOFT=50;
  function makeBackground(s){
    const cols=(s.colors||[]).filter(Boolean);
    const c1=cols[0]||"#000000", c2=cols[1]||c1, c3=cols[2]||c2;
    const a1=rgba(c1,s.alpha), a2=rgba(c2,s.alpha), a3=rgba(c3,s.alpha);
    const w=(s.weights||[100,0,0]).map(n=>Math.max(0,+n||0));
    const sum=w.reduce((a,b)=>a+b,0);
    if(s.mode==="solid") return a1;
    const softBase=Math.max(0,Math.min((s.soft??SOFT),MAX_SOFT));
    if(sum<=0) return a1;

    if(s.mode==="diffuse"){
      const k1=w[0]/sum, k2=w[1]/sum, k3=w[2]/sum;
      const angle=typeof s.angle==="number"?s.angle:135;
      return [
        `radial-gradient(60% 60% at 0% 0%, ${a1} ${Math.round(k1*60)}%, transparent 60%)`,
        `radial-gradient(60% 60% at 100% 0%, ${a2} ${Math.round(k2*60)}%, transparent 60%)`,
        `radial-gradient(60% 60% at 0% 100%, ${a3} ${Math.round(k3*60)}%, transparent 60%)`,
        `linear-gradient(${angle}deg, ${a1}, ${a2}, ${a3})`
      ].join(",");
    }

    if(s.mode==="radial"){
      const r1=w[0]/sum*100, r2=(w[0]+w[1])/sum*100;
      const t1=Math.min(softBase,r1,(r2-r1)/2), t2=Math.min(softBase,(r2-r1)/2,100-r2);
      return `radial-gradient(120% 100% at 0% 0%,
        ${a1} 0%, ${a1} ${Math.max(0,r1-t1)}%,
        ${a2} ${Math.min(100,r1+t1)}%, ${a2} ${Math.max(0,r2-t2)}%,
        ${a3} ${Math.min(100,r2+t2)}%, ${a3} 100%)`;
    }

    const p1=w[0]*100/sum, p2=(w[0]+w[1])*100/sum;
    const t1=Math.min(softBase,p1,(p2-p1)/2,100-p1);
    const t2=Math.min(softBase,(p2-p1)/2,100-p2);
    const angle=typeof s.angle==="number"?s.angle:135;
    if(p1<=0 && p2<=0) return `linear-gradient(${angle}deg, ${a3} 0%, ${a3} 100%)`;
    if(p1>=100) return `linear-gradient(${angle}deg, ${a1} 0%, ${a1} 100%)`;
    return `linear-gradient(${angle}deg,
      ${a1} 0%, ${a1} ${Math.max(0,p1-t1)}%,
      ${a2} ${Math.min(100,p1+t1)}%, ${a2} ${Math.max(0,p2-t2)}%,
      ${a3} ${Math.min(100,p2+t2)}%, ${a3} 100%)`;
  }

  /* ------------------- 粘合器 ------------------- */
  const STYLE_ID="ech0-theme-style";
  const CANDIDATES=[":scope > .markdown",":scope .markdown",":scope .prose",":scope .whitespace-pre-wrap"];
  function getCandidateBlocks(el){ const nodes=Array.from(el.querySelectorAll(CANDIDATES.join(","))); return nodes.filter(n=>!n.closest(".ech0-bubble")); }
  function containsAll(c,nodes){ for(let i=0;i<nodes.length;i++){ if(!c.contains(nodes[i])) return false; } return true; }
  function findCommonContainer(msgEl,nodes){ if(!nodes.length) return null; for(let c=nodes[0].parentElement;c && c!==msgEl;c=c.parentElement){ if(containsAll(c,nodes)) return c; } return msgEl; }
  function wrapIntoBubble(container,nodes){
    if(!container) return;
    let shell=container.querySelector(":scope > .ech0-bubble");
    if(!shell){ shell=document.createElement("div"); shell.className="ech0-bubble"; container.insertBefore(shell,container.firstChild); }
    const tops=[];
    nodes.forEach(n=>{ let t=n; while(t.parentElement && t.parentElement!==container) t=t.parentElement; if(!tops.includes(t)) tops.push(t); });
    tops.sort((a,b)=>a.compareDocumentPosition(b)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1);
    tops.forEach(t=>{ if(t!==shell) shell.appendChild(t); });
  }
  function glueOneMessage(msgEl){
    if (SAFE_MODE) return; // mobile safe mode: do not move DOM
    const nodes=getCandidateBlocks(msgEl);
    if(!nodes.length) return;
    const c=findCommonContainer(msgEl,nodes);
    wrapIntoBubble(c,nodes);
  }
  function ensureShells(role){
    if (SAFE_MODE) return; // mobile safe mode: do not move DOM
    document.querySelectorAll(`[data-message-author-role="${role}"]`).forEach(glueOneMessage);
  }

  /* ------------------- 样式 ------------------- */
  const SEND_BTN_SEL=[
    'button[data-testid="send-button"]',
    'form button[type="submit"]',
    'button[aria-label="Send"]',
    '[data-testid="composer-send-button"] button'
  ].join(",");
  const makeGlow=(hex,str)=> str>0?`0 6px 18px ${rgba(hex,clamp(str*0.55,0,1))}, 0 0 18px ${rgba(hex,clamp(str*0.35,0,1))}`:"none";

  function cssForRole(role,s){
    const bg=makeBackground(s);
    const outline=rgba(s.outlineColor,s.outlineAlpha);
    const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`;
    const shadow=makeGlow(s.outlineColor,s.glow);
    const pfx=role==="user"?"user":"asst";
    const isLight=themeLuma(s)>0.55;
    const preBg=isLight?"rgba(0,0,0,.08)":"rgba(255,255,255,.12)";
    const preBorder=isLight?"1px solid rgba(0,0,0,.12)":"1px solid rgba(255,255,255,.16)";
    const text=s.autoText?pickAutoTextColor(s):(s.textColor||"#101418");

    return `
:root{ --ech0-${pfx}-text:${text}; }
[data-message-author-role="${role}"], [data-message-author-role="${role}"] > div,
[data-message-author-role="${role}"] .text-message, [data-message-author-role="${role}"] .relative{
  background:transparent!important; box-shadow:none!important; border-color:transparent!important;
}
[data-message-author-role="${role}"] .ech0-bubble{
  background:${bg}!important; color:var(--ech0-${pfx}-text)!important;
  border:${border}!important; border-radius:${s.radius}px!important;
  padding:${s.padV}px ${s.padH}px!important; box-shadow:${shadow}!important;
  -webkit-backdrop-filter:blur(${s.blur}px)!important; backdrop-filter:blur(${s.blur}px)!important;
}
/* 覆盖内层节点默认色,保留代码块配色 */
[data-message-author-role="${role}"] .ech0-bubble :not(pre):not(code){
  color:var(--ech0-${pfx}-text)!important;
  -webkit-text-fill-color:var(--ech0-${pfx}-text)!important;
}
[data-message-author-role="${role}"] .ech0-bubble pre{
  background:${preBg}!important; border:${preBorder}!important; border-radius:10px!important;
}`;
  }

  function applyStyle(){
    ensureShells("user");
    if(CFG.targets.assistant) ensureShells("assistant");

    let css=`
.ech0-bubble{ display:inline-block; width:fit-content; max-width:100%; background:transparent; background-clip:padding-box; }
#${PANEL_ID} .hd{ position:sticky; top:0; z-index:5; }
#${PANEL_ID} .previewRow{ position:relative; z-index:1; }
`;

    if(CFG.targets.user) css+=cssForRole("user",CFG.user);
    if(CFG.targets.assistant) css+=cssForRole("assistant",CFG.assistant);

    if(CFG.targets.sendbtn){
      const s=CFG.followOfSendBtn==="assistant"?CFG.assistant:CFG.user;
      const bg=makeBackground(s);
      const outline=rgba(s.outlineColor,s.outlineAlpha);
      const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`;
      const textColor = s.autoText ? pickAutoTextColor(s) : (s.textColor || "#101418");
      css+=`
${SEND_BTN_SEL}{
  background:${bg}!important; border:${border}!important; border-radius:9999px!important;
  -webkit-backdrop-filter:blur(${s.blur}px)!important; backdrop-filter:blur(${s.blur}px)!important;
  color:${textColor}!important;
}
/* 只设置颜色,不改变图标本身笔画,避免变粗 */
${SEND_BTN_SEL} svg{
  color:${textColor}!important;
  --send-stroke: 0.10px;
}
${SEND_BTN_SEL} svg *{
  filter:none!important;
  mix-blend-mode:normal!important;
  opacity:1!important;
  -webkit-mask-image:none!important;
  mask:none!important;

  fill:none!important;
  stroke:currentColor!important;
  stroke-linecap:round!important;
  stroke-linejoin:round!important;

  vector-effect:non-scaling-stroke!important;
  stroke-width:var(--send-stroke)!important;
  shape-rendering:geometricPrecision;
  paint-order:stroke fill markers;
}
`;
    }

    let node=document.getElementById(STYLE_ID);
    if(!node){ node=document.createElement("style"); node.id=STYLE_ID; document.head.appendChild(node); }
    node.textContent=css;
    updatePreview();
  }

  /* ------------------- 面板 ------------------- */
  const PANEL_ID="ech0-theme-panel";
  let CFG=load();

  function openPanel(){
    if(document.getElementById(PANEL_ID)) return;
    const w=document.createElement("div"); w.id=PANEL_ID;
    w.innerHTML=`
<div class="hd" id="ech0-hd"><strong>Bubble Theme Pro</strong><div class="sp"></div><button data-act="close" title="关闭">✕</button></div>

<div class="bar">
  <label><input type="checkbox" id="t-user"> 用户气泡</label>
  <label><input type="checkbox" id="t-assistant"> AI 气泡</label>
  <label><input type="checkbox" id="t-sendbtn"> 发送按钮</label>
  <span class="sep"></span>
  <label>发送按钮跟随
    <select id="followSel"><option value="user">用户</option><option value="assistant">AI</option></select>
  </label>
</div>

<div class="tabs">
  <button class="tab active" data-role="user">编辑:用户</button>
  <button class="tab" data-role="assistant">编辑:AI</button>
  <div class="sp"></div>
  <button class="chipEdit" id="editChipsBtn">编辑主题</button>
</div>

<div class="chipWall" id="chipWall"></div>

<div class="previewRow">
  <div class="previewLabel">预览:</div>
  <div class="previewWrap"><div id="ech0-preview" class="previewBubble">正在输入中.............</div></div>
</div>

<div class="grid">
  <div class="col">
    <div class="item"><label>背景类型</label>
      <select id="mode"><option value="linear">线性渐变</option><option value="radial">放射渐变</option><option value="diffuse">弥散光渐变</option><option value="solid">纯色</option></select>
    </div>
    <div class="item"><label>角度(线性)</label><div class="dual"><input type="range" id="angleR" min="0" max="360" step="1"><input type="number" id="angle" min="0" max="360" step="1"></div></div>

    <div class="item"><label>文字颜色</label>
      <div class="dualColor">
        <input type="text" id="textHex" class="hex" placeholder="#RRGGBB">
        <input type="color" id="textColor">
        <label class="inline"><input type="checkbox" id="autoText"> 自动</label>
      </div>
    </div>

    <div class="item"><label>颜色1</label><div class="dualColor"><input type="text" id="h1" class="hex" placeholder="#RRGGBB"><input type="color" id="c1"></div></div>
    <div class="item"><label>颜色2</label><div class="dualColor"><input type="text" id="h2" class="hex" placeholder="#RRGGBB"><input type="color" id="c2"></div></div>
    <div class="item"><label>颜色3</label><div class="dualColor"><input type="text" id="h3" class="hex" placeholder="#RRGGBB"><input type="color" id="c3"></div></div>

    <div class="item"><label>配重1/2/3(%)</label>
      <div class="weights">
        <div class="row"><input type="range" id="w1r" min="0" max="100" step="1"><input type="number" id="w1" min="0" max="100" step="1"></div>
        <div class="row"><input type="range" id="w2r" min="0" max="100" step="1"><input type="number" id="w2" min="0" max="100" step="1"></div>
        <div class="row"><input type="range" id="w3r" min="0" max="100" step="1" disabled><input type="number" id="w3" min="0" max="100" step="1" readonly></div>
      </div>
    </div>

    <div class="hint" id="weightHint">配重合计:0%(建议=100%)</div>
  </div>

  <div class="col">
    <div class="item"><label>过渡羽化(%)</label><div class="dual"><input type="range" id="softR" min="0" max="50" step="1"><input type="number" id="soft" min="0" max="50" step="1"></div></div>
    <div class="item"><label>透明度</label><div class="dual"><input type="range" id="alphaR" min="0" max="1" step="0.01"><input type="number" id="alpha" min="0" max="1" step="0.01"></div></div>
    <div class="item"><label>磨砂(blur)</label><div class="dual"><input type="range" id="blurR" min="0" max="30" step="1"><input type="number" id="blur" min="0" max="30" step="1"></div></div>
    <div class="item"><label>发光强度</label><div class="dual"><input type="range" id="glowR" min="0" max="1" step="0.01"><input type="number" id="glow" min="0" max="1" step="0.01"></div></div>

    <div class="item"><label>描边色</label>
      <div class="dualColor">
        <input type="text" id="outlineHex" class="hex" placeholder="#RRGGBB">
        <input type="color" id="outlineColor">
      </div>
    </div>

    <div class="item"><label>描边透明</label><div class="dual"><input type="range" id="outlineAlphaR" min="0" max="1" step="0.01"><input type="number" id="outlineAlpha" min="0" max="1" step="0.01"></div></div>
    <div class="item"><label>描边宽(px)</label><div class="dual"><input type="range" id="outlineWidthR" min="0" max="6" step="1"><input type="number" id="outlineWidth" min="0" max="6" step="1"></div></div>

    <div class="item"><label>圆角(px)</label><div class="dual"><input type="range" id="radiusR" min="0" max="40" step="1"><input type="number" id="radius" min="0" max="40" step="1"></div></div>
    <div class="item"><label>内边距-垂直</label><div class="dual"><input type="range" id="padVR" min="0" max="40" step="1"><input type="number" id="padV" min="0" max="40" step="1"></div></div>
    <div class="item"><label>内边距-水平</label><div class="dual"><input type="range" id="padHR" min="0" max="40" step="1"><input type="number" id="padH" min="0" max="40" step="1"></div></div>
  </div>
</div>

<div class="presets">
  <div class="left">
    <input id="presetName" placeholder="预设名称">
    <button data-act="savePreset">另存为</button>
  </div>
  <div class="right">
    <button data-act="save">保存</button>
    <button data-act="reset">重置默认</button>
  </div>
</div>
`;
    document.body.appendChild(w);

    // 关闭
    w.querySelector('[data-act="close"]').addEventListener("click",()=>w.remove());

    // 样式(紧凑 + 选中主题高亮)
    GM_addStyle(`
#${PANEL_ID}{
  position:fixed; z-index:999999; left:50%; top:60px; transform:translateX(-50%);
  width:${CFG.panel.w||560}px; height:${CFG.panel.h||560}px;
  background:#fff; color:#1b1f24; border:1px solid rgba(0,0,0,.12);
  border-radius:14px; box-shadow:0 16px 48px rgba(0,0,0,.16);
  overflow:auto; resize:both; font:13px/1.35 system-ui, Segoe UI, Arial; cursor:default;
}
#${PANEL_ID} .hd{
  position:sticky; top:0; z-index:5;
  display:flex; align-items:center; gap:8px; padding:8px 10px;
  user-select:none; background:#fff;
  border-bottom:1px solid rgba(0,0,0,.06); border-top-left-radius:14px; border-top-right-radius:14px; cursor:default;
}
#${PANEL_ID} .sp{ flex:1; }
#${PANEL_ID} .bar{ display:flex; align-items:center; gap:8px; padding:6px 10px; flex-wrap:wrap; cursor:default; }
#${PANEL_ID} .sep{ width:1px; height:16px; background:rgba(0,0,0,.08); margin:0 4px; }
#${PANEL_ID} .tabs{ display:flex; align-items:center; gap:6px; padding:6px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .tab{ border:1px solid rgba(0,0,0,.12); background:#f5f7fb; border-radius:8px; padding:5px 8px; cursor:default; }
#${PANEL_ID} .tab.active{ background:#e9eef9; }
#${PANEL_ID} .chipEdit{ border:1px solid rgba(0,0,0,.12); background:#fff; border-radius:999px; padding:5px 10px; cursor:default; }
#${PANEL_ID} .chipWall{ display:flex; flex-wrap:wrap; gap:6px; padding:0 10px 6px; border-bottom:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .chip{ display:flex; align-items:center; gap:6px; padding:5px 8px; border:1px solid rgba(0,0,0,.12); background:#f7f9fc; border-radius:999px; cursor:default; user-select:none; }
#${PANEL_ID} .chip .dot{ width:14px; height:14px; border-radius:999px; border:1px solid rgba(0,0,0,.12); background:linear-gradient(135deg,var(--c1),var(--c2),var(--c3)); }
#${PANEL_ID} .chip .x{ display:none; width:16px; height:16px; border-radius:50%; border:1px solid rgba(0,0,0,.25); background:#fff; line-height:14px; text-align:center; font-size:11px; }
#${PANEL_ID}.editing .chip .x{ display:inline-block; }
#${PANEL_ID} .chip.active{
  background:#e9eef9;
  border-color:rgba(59,130,246,.35);
  box-shadow: inset 0 0 0 2px rgba(59,130,246,.18);
}

#${PANEL_ID} .previewRow{ display:grid; grid-template-columns:56px 1fr; align-items:center; gap:8px; padding:6px 10px; }
#${PANEL_ID} .previewBubble{ display:inline-block; padding:6px 10px; border-radius:12px; border:1px solid rgba(0,0,0,.12); width:fit-content; max-width:100%; white-space:nowrap; }

#${PANEL_ID} .grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:6px 10px; }
#${PANEL_ID} .col{ display:flex; flex-direction:column; gap:8px; }
#${PANEL_ID} .item{ display:flex; align-items:center; gap:8px; cursor:default; }
#${PANEL_ID} label{ width:116px; color:#3b4250; }

#${PANEL_ID} .bar label{ width:auto!important; display:inline-flex; align-items:center; gap:8px; margin:0; white-space:nowrap; }
#${PANEL_ID} #followSel{ min-width:92px; padding-right:22px; }

#${PANEL_ID} .dual{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .dual input[type="range"]{ width:128px; }
#${PANEL_ID} .dual input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; padding:4px 6px; border-radius:7px; cursor:default; }

#${PANEL_ID} .dualColor{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .dualColor input.hex{ width:96px; height:26px; line-height:26px; font-size:13px; }
#${PANEL_ID} .dualColor .inline{ display:inline-flex; align-items:center; gap:4px; font-size:12px; color:#64748b; margin-left:6px; }
#${PANEL_ID} .dualColor input:disabled{ opacity:.6; }

#${PANEL_ID} input[type="color"]{ width:34px; height:24px; border:none; background:transparent; cursor:default; }
#${PANEL_ID} input[type="number"], #${PANEL_ID} select, #${PANEL_ID} input[type="text"]{
  font-size:13px; height:26px; line-height:26px; padding:4px 6px; border-radius:7px; border:1px solid rgba(0,0,0,.14); background:#fff; cursor:default;
}

#${PANEL_ID} input[type="range"]{ -webkit-appearance:none; appearance:none; background:transparent; height:16px; cursor:default; }
#${PANEL_ID} input[type="range"]::-webkit-slider-runnable-track{ height:3px; border-radius:2px; background:#d1d5db; }
#${PANEL_ID} input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:12px; height:12px; border-radius:50%; background:#4b5563; border:none; margin-top:-4.5px; }

#${PANEL_ID} .weights .row{ display:flex; align-items:center; gap:6px; margin-bottom:2px; }
#${PANEL_ID} .weights input[type="range"]{ width:128px; }
#${PANEL_ID} .weights input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; }

#${PANEL_ID} .hint{ padding-left:116px; color:#6b7280; }

#${PANEL_ID} .presets{ display:flex; justify-content:space-between; align-items:center; gap:8px; padding:8px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .presets .left{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .presets input{ width:140px; cursor:default; }
#${PANEL_ID} button{ background:#f5f7fb; border:1px solid rgba(0,0,0,.12); border-radius:8px; padding:5px 10px; cursor:default; }

#${PANEL_ID} #followSel, #${PANEL_ID} #mode{
  font-size:12px; height:26px; line-height:26px; padding:0 24px 0 8px; text-align:center; text-align-last:center; cursor:default;
}
#${PANEL_ID} #mode{ min-width:128px; }
#${PANEL_ID} #followSel option, #${PANEL_ID} #mode option{ text-align:center; }
`);

    if(CFG.panel.x!=null && CFG.panel.y!=null){ w.style.left=CFG.panel.x+"px"; w.style.top=CFG.panel.y+"px"; w.style.transform="none"; }
    (function enableDrag(){
      const hd=w.querySelector("#ech0-hd");
      let dragging=false,ox=0,oy=0,sx=0,sy=0;
      hd.addEventListener("mousedown",(e)=>{ dragging=true; sx=e.clientX; sy=e.clientY; const r=w.getBoundingClientRect(); ox=r.left; oy=r.top; e.preventDefault(); });
      window.addEventListener("mousemove",(e)=>{ if(!dragging) return; const nx=ox+(e.clientX-sx), ny=oy+(e.clientY-sy); w.style.left=Math.max(8,Math.min(window.innerWidth-8,nx))+"px"; w.style.top=Math.max(8,Math.min(window.innerHeight-8,ny))+"px"; w.style.transform="none"; });
      window.addEventListener("mouseup",()=>{ if(!dragging) return; dragging=false; const r=w.getBoundingClientRect(); CFG.panel.x=Math.round(r.left); CFG.panel.y=Math.round(r.top); CFG.panel.w=Math.round(r.width); CFG.panel.h=Math.round(r.height); save(CFG); });
      w.addEventListener("mouseup",()=>{ const r=w.getBoundingClientRect(); CFG.panel.w=Math.round(r.width); CFG.panel.h=Math.round(r.height); save(CFG); });
    })();

    const $=(s)=>w.querySelector(s);

    /* 顶部开关 */
    $("#t-user").checked=!!CFG.targets.user;
    $("#t-assistant").checked=!!CFG.targets.assistant;
    $("#t-sendbtn").checked=!!CFG.targets.sendbtn;
    $("#followSel").value=CFG.followOfSendBtn;
    ["t-user","t-assistant","t-sendbtn","followSel"].forEach(id=>{
      $("#"+id).addEventListener("change",()=>{
        CFG.targets.user=$("#t-user").checked;
        CFG.targets.assistant=$("#t-assistant").checked;
        CFG.targets.sendbtn=$("#t-sendbtn").checked;
        CFG.followOfSendBtn=$("#followSel").value;
        save(CFG); scheduleGlue();
      });
    });

    /* 主题墙 */
    let editMode=false, ACTIVE="user";
    function isBuiltin(name){ return name in THEMES; }
    function chipTheme(name){ return isBuiltin(name)?THEMES[name]:(CFG.customThemes[name]||THEMES["海盐"]); }
    function galleryNames(){
      const builtins=Object.keys(THEMES), customs=Object.keys(CFG.customThemes||{}), set=new Set([...builtins,...customs]);
      const arr=(CFG.galleryOrder||[]).filter(n=>set.has(n)); for(const n of set){ if(!arr.includes(n)) arr.push(n); }
      CFG.galleryOrder=arr; save(CFG); return arr;
    }
    function renderChips(){
      const wall=$("#chipWall"); wall.innerHTML="";
      const current = CFG.lastTheme[ACTIVE];
      galleryNames().forEach(name=>{
        const st=chipTheme(name), btn=document.createElement("div");
        btn.className="chip"; btn.draggable=true; btn.dataset.name=name;
        if(name===current) btn.classList.add("active");
        btn.innerHTML=`<span class="dot" style="--c1:${st.colors[0]};--c2:${st.colors[1]};--c3:${st.colors[2]}"></span><span class="nm">${name}</span>${isBuiltin(name)?"":'<span class="x" title="删除">×</span>'}`;
        wall.appendChild(btn);
      });
      wall.parentElement.classList.toggle("editing",editMode);
    }
    function pickThemeByName(name){
      CFG.lastTheme[ACTIVE]=name;
      const over=(CFG.overrides[ACTIVE]||{})[name];
      CFG[ACTIVE]=clone(over||chipTheme(name));
      fillForm(); save(CFG);
      renderChips();
    }
    $("#editChipsBtn").addEventListener("click",()=>{ editMode=!editMode; $("#editChipsBtn").textContent=editMode?"完成":"编辑主题"; renderChips(); });
    $("#chipWall").addEventListener("click",(e)=>{
      const chip=e.target.closest(".chip"); if(!chip) return;
      const name=chip.dataset.name;
      if(editMode){
        if(e.target.classList.contains("x")){
          if(isBuiltin(name)) return;
          if(confirm(`删除主题「${name}」?`)){ delete CFG.customThemes[name]; CFG.galleryOrder=CFG.galleryOrder.filter(n=>n!==name); save(CFG); renderChips(); }
          return;
        }
      }else{
        pickThemeByName(name);
      }
    });
    // 拖拽排序
    let dragName=null;
    $("#chipWall").addEventListener("dragstart",(e)=>{ const c=e.target.closest(".chip"); if(!c) return; dragName=c.dataset.name; e.dataTransfer.effectAllowed="move"; });
    $("#chipWall").addEventListener("dragover",(e)=>{ if(!dragName) return; e.preventDefault(); });
    $("#chipWall").addEventListener("drop",(e)=>{ e.preventDefault(); const c=e.target.closest(".chip"); if(!c || !dragName) return; const target=c.dataset.name; if(target===dragName) return;
      const arr=galleryNames(), a=arr.indexOf(dragName), b=arr.indexOf(target); arr.splice(b,0,arr.splice(a,1)[0]); CFG.galleryOrder=arr; save(CFG); dragName=null; renderChips();
    });
    $("#chipWall").addEventListener("dragend",()=>{ dragName=null; renderChips(); });
    renderChips();

    /* Tab */
    w.querySelectorAll(".tab").forEach(t=>{
      t.addEventListener("click",()=>{
        w.querySelectorAll(".tab").forEach(x=>x.classList.remove("active"));
        t.classList.add("active"); ACTIVE=t.dataset.role; fillForm(); renderChips();
      });
    });

    /* 绑定:HEX 与 取色器 */
    function bindHex(hexId,colorId){
      const hex=$(hexId), col=$(colorId);
      function setHex(){ hex.value=(col.value||"#000000").toLowerCase(); }
      function setCol(){ const p=parseColorToHex(hex.value); if(p){ col.value=p; hex.value=p; } }
      setHex();
      col.addEventListener("input",()=>{ setHex(); readForm(); scheduleGlue(); });
      hex.addEventListener("input",()=>{ setCol(); readForm(); scheduleGlue(); });
      hex.addEventListener("change",()=>{ setCol(); readForm(); scheduleGlue(); });
      hex.addEventListener("paste",()=>{ setTimeout(()=>{ setCol(); readForm(); scheduleGlue(); },0); });
    }
    function bindPair(rangeId,numId,onChange){
      const r=$(rangeId), n=$(numId);
      function sync(from){ if(from==="r"){ n.value=r.value; } else { r.value=n.value; } onChange&&onChange(+n.value); }
      r.addEventListener("input",()=>sync("r")); n.addEventListener("input",()=>sync("n")); sync("n");
    }

    /* 配重联动 */
    function applyWeightUI(w1,w2){
      const w3=clamp(100-w1-w2,0,100);
      $("#w1r").value=w1; $("#w1").value=w1; $("#w2r").value=w2; $("#w2").value=w2; $("#w3r").value=w3; $("#w3").value=w3;
      $("#weightHint").textContent=`配重合计:${w1+w2+w3}%(建议=100%)`;
    }
    function readWeightUIToModel(){
      let w1=+$("#w1").value||0, w2=+$("#w2").value||0;
      if(w1<0) w1=0; if(w1>100) w1=100; if(w2<0) w2=0; if(w1+w2>100) w2=100-w1;
      const w3=100-w1-w2; applyWeightUI(w1,w2);
      const s=CFG[ACTIVE]; s.weights=[w1,w2,w3]; return s.weights;
    }
    $("#w1r").addEventListener("input",()=>{ $("#w1").value=$("#w1r").value; readWeightUIToModel(); scheduleGlue(); });
    $("#w2r").addEventListener("input",()=>{ $("#w2").value=$("#w2r").value; readWeightUIToModel(); scheduleGlue(); });
    $("#w1").addEventListener("input",()=>{ readWeightUIToModel(); scheduleGlue(); });
    $("#w2").addEventListener("input",()=>{ readWeightUIToModel(); scheduleGlue(); });

    /* 表单填充/读取 */
    function fillForm(){
      const s=CFG[ACTIVE];
      $("#mode").value=s.mode; $("#angle").value=s.angle; $("#angleR").value=s.angle;

      $("#textColor").value=s.textColor||"#101418"; $("#textHex").value=$("#textColor").value.toLowerCase();
      $("#autoText").checked=!!s.autoText; const dis=!!s.autoText;
      $("#textHex").disabled=dis; $("#textColor").disabled=dis;
      $("#autoText").onchange=()=>{ const on=$("#autoText").checked; $("#textHex").disabled=on; $("#textColor").disabled=on; readForm(); scheduleGlue(); };

      $("#c1").value=s.colors[0]||"#000000"; $("#h1").value=$("#c1").value.toLowerCase();
      $("#c2").value=s.colors[1]||"#000000"; $("#h2").value=$("#c2").value.toLowerCase();
      $("#c3").value=s.colors[2]||"#000000"; $("#h3").value=$("#c3").value.toLowerCase();

      const w1=s.weights?.[0]??34, w2=s.weights?.[1]??33; applyWeightUI(w1,w2);

      $("#soft").value=s.soft??SOFT; $("#softR").value=$("#soft").value;
      $("#alpha").value=s.alpha; $("#alphaR").value=s.alpha;
      $("#blur").value=s.blur; $("#blurR").value=s.blur;
      $("#glow").value=s.glow; $("#glowR").value=s.glow;

      $("#outlineColor").value=s.outlineColor; $("#outlineHex").value=$("#outlineColor").value.toLowerCase();
      $("#outlineAlpha").value=s.outlineAlpha; $("#outlineAlphaR").value=s.outlineAlpha;
      $("#outlineWidth").value=s.outlineWidth; $("#outlineWidthR").value=s.outlineWidth;

      $("#radius").value=s.radius; $("#radiusR").value=s.radius;
      $("#padV").value=s.padV; $("#padVR").value=s.padV;
      $("#padH").value=s.padH; $("#padHR").value=s.padH;

      bindHex("#h1","#c1"); bindHex("#h2","#c2"); bindHex("#h3","#c3"); bindHex("#textHex","#textColor"); bindHex("#outlineHex","#outlineColor");
      bindPair("#angleR","#angle",()=>{ CFG[ACTIVE].angle=+$("#angle").value; });
      bindPair("#softR","#soft",()=>{ CFG[ACTIVE].soft=+$("#soft").value; });
      bindPair("#alphaR","#alpha",()=>{ CFG[ACTIVE].alpha=+$("#alpha").value; });
      bindPair("#blurR","#blur",()=>{ CFG[ACTIVE].blur=+$("#blur").value; });
      bindPair("#glowR","#glow",()=>{ CFG[ACTIVE].glow=+$("#glow").value; });
      bindPair("#outlineAlphaR","#outlineAlpha",()=>{ CFG[ACTIVE].outlineAlpha=+$("#outlineAlpha").value; });
      bindPair("#outlineWidthR","#outlineWidth",()=>{ CFG[ACTIVE].outlineWidth=+$("#outlineWidth").value; });
      bindPair("#radiusR","#radius",()=>{ CFG[ACTIVE].radius=+$("#radius").value; });
      bindPair("#padVR","#padV",()=>{ CFG[ACTIVE].padV=+$("#padV").value; });
      bindPair("#padHR","#padH",()=>{ CFG[ACTIVE].padH=+$("#padH").value; });

      scheduleGlue();
    }
    function readForm(){
      const s=CFG[ACTIVE];
      s.mode=$("#mode").value; s.angle=+$("#angle").value;
      s.autoText=$("#autoText").checked;
      const parsedText=parseColorToHex($("#textHex").value)||$("#textColor").value; s.textColor=parsedText||s.textColor||"#101418";
      s.colors=[$("#c1").value,$("#c2").value,$("#c3").value].filter(Boolean);
      s.weights=readWeightUIToModel();
      s.soft=+$("#soft").value; s.alpha=+$("#alpha").value; s.blur=+$("#blur").value; s.glow=+$("#glow").value;
      const parsedOutline=parseColorToHex($("#outlineHex").value)||$("#outlineColor").value; s.outlineColor=parsedOutline||"#000000";
      s.outlineAlpha=+$("#outlineAlpha").value; s.outlineWidth=+$("#outlineWidth").value;
      s.radius=+$("#radius").value; s.padV=+$("#padV").value; s.padH=+$("#padH").value;

      const tname=CFG.lastTheme[ACTIVE]; if(!CFG.overrides[ACTIVE]) CFG.overrides[ACTIVE]={};
      CFG.overrides[ACTIVE][tname]=clone(s);
      save(CFG);
    }

    w.querySelector(".grid").addEventListener("input",()=>{ readForm(); scheduleGlue(); });
    w.querySelector(".grid").addEventListener("change",()=>{ readForm(); scheduleGlue(); });

    w.querySelector('[data-act="save"]').addEventListener("click",()=>{ save(CFG); alert("已保存配置"); });
    w.querySelector('[data-act="savePreset"]').addEventListener("click",()=>{
      const name=$("#presetName").value.trim(); if(!name){ alert("请输入预设名称"); return; }
      if(THEMES[name] && !confirm("同名内置主题已存在,是否覆盖为自定义?")) return;
      if(!CFG.customThemes) CFG.customThemes={};
      CFG.customThemes[name]=clone(CFG[ACTIVE]); CFG.galleryOrder=[...new Set([name,...CFG.galleryOrder])];
      CFG.lastTheme[ACTIVE]=name; delete (CFG.overrides[ACTIVE]||{})[name]; save(CFG); renderChips(); pickThemeByName(name); alert("已另存为自定义主题");
    });
    w.querySelector('[data-act="reset"]').addEventListener("click",()=>{
      const name=CFG.lastTheme[ACTIVE];
      const source=isBuiltin(name)?THEMES[name]:CFG.customThemes[name];
      if(!source){ alert("未找到默认主题定义"); return; }
      if(CFG.overrides[ACTIVE]) delete CFG.overrides[ACTIVE][name];
      CFG[ACTIVE]=clone(source); save(CFG); fillForm(); scheduleGlue(); alert("已重置为默认");
    });

    fillForm();
  }

  function updatePreview(){
    const wp = document.getElementById("ech0-preview");
    if (!wp) return;

    const active = document.querySelector(`#${PANEL_ID} .tab.active`);
    const role = active ? active.dataset.role : "user";
    const s = role === "assistant" ? CFG.assistant : CFG.user;

    const bg = makeBackground(s);
    const outline = rgba(s.outlineColor, s.outlineAlpha);
    const border = (s.outlineAlpha <= 0 || s.outlineWidth <= 0)
      ? "none"
      : `${s.outlineWidth}px solid ${outline}`;
    const text = s.autoText ? (themeLuma(s)>0.55 ? "#000" : "#fff") : (s.textColor || "#111");

    wp.style.background = bg;
    wp.style.border = border;
    wp.style.borderRadius = s.radius + "px";
    wp.style.padding = `${s.padV}px ${s.padH}px`;
    wp.style.color = text;
    wp.style.webkitTextFillColor = text;
    wp.style.webkitBackdropFilter = `blur(${s.blur}px)`;
    wp.style.backdropFilter = `blur(${s.blur}px)`;
    wp.style.boxShadow = makeGlow(s.outlineColor, s.glow);
    wp.textContent = "正在输入中.............";
  }

  const togglePanel=()=>{ const p=document.getElementById(PANEL_ID); if(p){ p.remove(); } else { openPanel(); } };
  document.addEventListener("keydown",(e)=>{ if(e.altKey && e.key.toLowerCase()==="g"){ e.preventDefault(); togglePanel(); } });
  GM_registerMenuCommand("打开/关闭设置面板 (Alt+G)",togglePanel);

  applyStyle();
  let glueTicker=null;
  function scheduleGlue(){
    if(!SAFE_MODE){
      ensureShells("user");
      if(CFG.targets.assistant) ensureShells("assistant");
    }
    applyStyle();
    if(!SAFE_MODE){
      const deadline=Date.now()+3000;
      if(glueTicker) clearInterval(glueTicker);
      glueTicker=setInterval(()=>{
        ensureShells("user");
        if(CFG.targets.assistant) ensureShells("assistant");
        applyStyle();
        if(Date.now()>deadline){ clearInterval(glueTicker); glueTicker=null; }
      },120);
    }
  }
  if(!SAFE_MODE){
    const mo=new MutationObserver(scheduleGlue);
    mo.observe(document.querySelector("main")||document.documentElement,{ childList:true, subtree:true });
  }

})();