Greasy Fork is available in English.
Customizes Character.ai chat interface with lots of bubble styles, custom bubble options, themed input box for a more aesthetic experience, custom fonts, custom backgrounds, custom avatars/names, and a some other useful options like message drafts and hide edited marks.
// ==UserScript==
// @name •˚✦ C.ai Customizer ✦˚•
// @version 1.3.2.10
// @description Customizes Character.ai chat interface with lots of bubble styles, custom bubble options, themed input box for a more aesthetic experience, custom fonts, custom backgrounds, custom avatars/names, and a some other useful options like message drafts and hide edited marks.
// @author @redsus.vn (modded by Ciel_145 & Claude)
// @match https://character.ai/
// @match *://character.ai/*
// @match *://*.character.ai/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// @run-at document-start
// @namespace https://greasyfork.org/users/1515583
// ==/UserScript==
(function () {
'use strict';
// ─── COLORIS CSS INJECTION───
GM_addStyle(`
.clr-picker{display:none;flex-wrap:wrap;position:absolute;width:200px;z-index:1000;border-radius:10px;background-color:#fff;justify-content:flex-end;direction:ltr;box-shadow:0 0 5px rgba(0,0,0,.05),0 5px 20px rgba(0,0,0,.1);-moz-user-select:none;-webkit-user-select:none;user-select:none}.clr-picker.clr-open,.clr-picker[data-inline=true]{display:flex}.clr-picker[data-inline=true]{position:relative}.clr-gradient{position:relative;width:100%;height:100px;margin-bottom:15px;border-radius:3px 3px 0 0;background-image:linear-gradient(rgba(0,0,0,0),#000),linear-gradient(90deg,#fff,currentColor);cursor:pointer}.clr-marker{position:absolute;width:12px;height:12px;margin:-6px 0 0 -6px;border:1px solid #fff;border-radius:50%;background-color:currentColor;cursor:pointer}.clr-picker input[type=range]::-webkit-slider-runnable-track{width:100%;height:16px}.clr-picker input[type=range]::-webkit-slider-thumb{width:16px;height:16px;-webkit-appearance:none}.clr-picker input[type=range]::-moz-range-track{width:100%;height:16px;border:0}.clr-picker input[type=range]::-moz-range-thumb{width:16px;height:16px;border:0}.clr-hue{background-image:linear-gradient(to right,red 0,#ff0 16.66%,#0f0 33.33%,#0ff 50%,#00f 66.66%,#f0f 83.33%,red 100%)}.clr-alpha,.clr-hue{position:relative;width:calc(100% - 40px);height:8px;margin:5px 20px;border-radius:4px}.clr-alpha span{display:block;height:100%;width:100%;border-radius:inherit;background-image:linear-gradient(90deg,rgba(0,0,0,0),currentColor)}.clr-alpha input[type=range],.clr-hue input[type=range]{position:absolute;width:calc(100% + 32px);height:16px;left:-16px;top:-4px;margin:0;background-color:transparent;opacity:0;cursor:pointer;appearance:none;-webkit-appearance:none}.clr-alpha div,.clr-hue div{position:absolute;width:16px;height:16px;left:0;top:50%;margin-left:-8px;transform:translateY(-50%);border:2px solid #fff;border-radius:50%;background-color:currentColor;box-shadow:0 0 1px #888;pointer-events:none}.clr-alpha div:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border-radius:50%;background-color:currentColor}.clr-format{display:none;order:1;width:calc(100% - 40px);margin:0 20px 20px}.clr-segmented{display:flex;position:relative;width:100%;margin:0;padding:0;border:1px solid #ddd;border-radius:15px;box-sizing:border-box;color:#999;font-size:12px}.clr-segmented input,.clr-segmented legend{position:absolute;width:100%;height:100%;margin:0;padding:0;border:0;left:0;top:0;opacity:0;pointer-events:none}.clr-segmented label{flex-grow:1;margin:0;padding:4px 0;font-size:inherit;font-weight:400;line-height:initial;text-align:center;cursor:pointer}.clr-segmented label:first-of-type{border-radius:10px 0 0 10px}.clr-segmented label:last-of-type{border-radius:0 10px 10px 0}.clr-segmented input:checked+label{color:#fff;background-color:#666}.clr-swatches{order:2;width:calc(100% - 32px);margin:0 16px}.clr-swatches div{display:flex;flex-wrap:wrap;padding-bottom:12px;justify-content:center}.clr-swatches button{position:relative;width:20px;height:20px;margin:0 4px 6px 4px;padding:0;border:0;border-radius:50%;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;cursor:pointer}.clr-swatches button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}input.clr-color{order:1;width:calc(100% - 80px);height:32px;margin:15px 20px 20px auto;padding:0 10px;border:1px solid #ddd;border-radius:16px;color:#444;background-color:#fff;font-family:sans-serif;font-size:14px;text-align:center;box-shadow:none}input.clr-color:focus{outline:0;border:1px solid #1e90ff}.clr-clear,.clr-close{display:none;order:2;height:24px;margin:0 20px 20px;padding:0 20px;border:0;border-radius:12px;color:#fff;background-color:#666;font-family:inherit;font-size:12px;font-weight:400;cursor:pointer}.clr-close{display:block;margin:0 20px 20px auto}.clr-preview{position:relative;width:32px;height:32px;margin:15px 0 20px 20px;border-radius:50%;overflow:hidden}.clr-preview:after,.clr-preview:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border:1px solid #fff;border-radius:50%}.clr-preview:after{border:0;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.clr-preview button{position:absolute;width:100%;height:100%;z-index:1;margin:0;padding:0;border:0;border-radius:50%;outline-offset:-2px;background-color:transparent;text-indent:-9999px;cursor:pointer;overflow:hidden}.clr-alpha div,.clr-color,.clr-hue div,.clr-marker{box-sizing:border-box}.clr-field{display:inline-block;position:relative;color:transparent}.clr-field input{margin:0;direction:ltr}.clr-field.clr-rtl input{text-align:right}.clr-field button{position:absolute;width:30px;height:100%;right:0;top:50%;transform:translateY(-50%);margin:0;padding:0;border:0;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;pointer-events:none}.clr-field.clr-rtl button{right:auto;left:0}.clr-field button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 1px rgba(0,0,0,.5)}.clr-alpha,.clr-alpha div,.clr-field button,.clr-preview:before,.clr-swatches button{background-image:repeating-linear-gradient(45deg,#aaa 25%,transparent 25%,transparent 75%,#aaa 75%,#aaa),repeating-linear-gradient(45deg,#aaa 25%,#fff 25%,#fff 75%,#aaa 75%,#aaa);background-position:0 0,4px 4px;background-size:8px 8px}.clr-marker:focus{outline:0}.clr-keyboard-nav .clr-alpha input:focus+div,.clr-keyboard-nav .clr-hue input:focus+div,.clr-keyboard-nav .clr-marker:focus,.clr-keyboard-nav .clr-segmented input:focus+label{outline:0;box-shadow:0 0 0 2px #1e90ff,0 0 2px 2px #fff}.clr-picker[data-alpha=false] .clr-alpha{display:none}.clr-picker[data-minimal=true]{padding-top:16px}.clr-picker[data-minimal=true] .clr-alpha,.clr-picker[data-minimal=true] .clr-color,.clr-picker[data-minimal=true] .clr-gradient,.clr-picker[data-minimal=true] .clr-hue,.clr-picker[data-minimal=true] .clr-preview{display:none}.clr-dark{background-color:#444}.clr-dark .clr-segmented{border-color:#777}.clr-dark .clr-swatches button:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}.clr-dark input.clr-color{color:#fff;border-color:#777;background-color:#555}.clr-dark input.clr-color:focus{border-color:#1e90ff}.clr-dark .clr-preview:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.5)}.clr-dark .clr-alpha,.clr-dark .clr-alpha div,.clr-dark .clr-preview:before,.clr-dark .clr-swatches button{background-image:repeating-linear-gradient(45deg,#666 25%,transparent 25%,transparent 75%,#888 75%,#888),repeating-linear-gradient(45deg,#888 25%,#444 25%,#444 75%,#888 75%,#888)}.clr-picker.clr-polaroid{border-radius:6px;box-shadow:0 0 5px rgba(0,0,0,.1),0 5px 30px rgba(0,0,0,.2)}.clr-picker.clr-polaroid:before{content:'';display:block;position:absolute;width:16px;height:10px;left:20px;top:-10px;border:solid transparent;border-width:0 8px 10px 8px;border-bottom-color:currentColor;box-sizing:border-box;color:#fff;filter:drop-shadow(0 -4px 3px rgba(0,0,0,.1));pointer-events:none}.clr-picker.clr-polaroid.clr-dark:before{color:#444}.clr-picker.clr-polaroid.clr-left:before{left:auto;right:20px}.clr-picker.clr-polaroid.clr-top:before{top:auto;bottom:-10px;transform:rotateZ(180deg)}.clr-polaroid .clr-gradient{width:calc(100% - 20px);height:120px;margin:10px;border-radius:3px}.clr-polaroid .clr-alpha,.clr-polaroid .clr-hue{width:calc(100% - 30px);height:10px;margin:6px 15px;border-radius:5px}.clr-polaroid .clr-alpha div,.clr-polaroid .clr-hue div{box-shadow:0 0 5px rgba(0,0,0,.2)}.clr-polaroid .clr-format{width:calc(100% - 20px);margin:0 10px 15px}.clr-polaroid .clr-swatches{width:calc(100% - 12px);margin:0 6px}.clr-polaroid .clr-swatches div{padding-bottom:10px}.clr-polaroid .clr-swatches button{width:22px;height:22px}.clr-polaroid input.clr-color{width:calc(100% - 60px);margin:10px 10px 15px auto}.clr-polaroid .clr-clear{margin:0 10px 15px 10px}.clr-polaroid .clr-close{margin:0 10px 15px auto}.clr-polaroid .clr-preview{margin:10px 0 15px 10px}.clr-picker.clr-large{width:275px}.clr-large .clr-gradient{height:150px}.clr-large .clr-swatches button{width:22px;height:22px}.clr-picker.clr-pill{width:380px;padding-left:180px;box-sizing:border-box}.clr-pill .clr-gradient{position:absolute;width:180px;height:100%;left:0;top:0;margin-bottom:0;border-radius:3px 0 0 3px}.clr-pill .clr-hue{margin-top:20px}
`);
// ─── COLORIS JS INJECTION ───
!function(u,p,s,c){var d,f,h,i,b,y,v,m,g,l,w,k,L,E,a,n,r=p.createElement("canvas").getContext("2d"),x={r:0,g:0,b:0,h:0,s:0,v:0,a:1},A={},C={el:"[data-coloris]",parent:"body",theme:"default",themeMode:"light",rtl:!1,wrap:!0,margin:2,format:"hex",formatToggle:!1,swatches:[],swatchesOnly:!1,alpha:!0,forceAlpha:!1,focusInput:!0,selectInput:!1,inline:!1,defaultColor:"#000000",clearButton:!1,clearLabel:"Clear",closeButton:!1,closeLabel:"Close",onChange:function(){return c},a11y:{open:"Open color picker",close:"Close color picker",clear:"Clear the selected color",marker:"Saturation: {s}. Brightness: {v}.",hueSlider:"Hue slider",alphaSlider:"Opacity slider",input:"Color value field",format:"Color format",swatch:"Color swatch",instruction:"Saturation and brightness selector. Use up, down, left and right arrow keys to select."}},o={},S="",T={},B=!1;function M(t){if("object"==typeof t)for(var e in t)switch(e){case"el":D(t.el),!1!==t.wrap&&R(t.el);break;case"parent":(d=t.parent instanceof HTMLElement?t.parent:p.querySelector(t.parent))&&(d.appendChild(f),C.parent=t.parent,d===p.body&&(d=c));break;case"themeMode":C.themeMode=t.themeMode,"auto"===t.themeMode&&u.matchMedia&&u.matchMedia("(prefers-color-scheme: dark)").matches&&(C.themeMode="dark");case"theme":t.theme&&(C.theme=t.theme),f.className="clr-picker clr-"+C.theme+" clr-"+C.themeMode,C.inline&&j();break;case"rtl":C.rtl=!!t.rtl,Array.from(p.getElementsByClassName("clr-field")).forEach(function(e){return e.classList.toggle("clr-rtl",C.rtl)});break;case"margin":t.margin*=1,C.margin=(isNaN(t.margin)?C:t).margin;break;case"wrap":t.el&&t.wrap&&R(t.el);break;case"formatToggle":C.formatToggle=!!t.formatToggle,V("clr-format").style.display=C.formatToggle?"block":"none",C.formatToggle&&(C.format="auto");break;case"swatches":Array.isArray(t.swatches)&&function(){var e=V("clr-swatches"),l=p.createElement("div");e.textContent="",t.swatches.forEach(function(e,t){var a=p.createElement("button");a.setAttribute("type","button"),a.setAttribute("id","clr-swatch-"+t),a.setAttribute("aria-labelledby","clr-swatch-label clr-swatch-"+t),a.style.color=e,a.textContent=e,l.appendChild(a)}),t.swatches.length&&e.appendChild(l),C.swatches=t.swatches.slice()}();break;case"swatchesOnly":C.swatchesOnly=!!t.swatchesOnly,f.setAttribute("data-minimal",C.swatchesOnly);break;case"alpha":C.alpha=!!t.alpha,f.setAttribute("data-alpha",C.alpha);break;case"inline":C.inline=!!t.inline,f.setAttribute("data-inline",C.inline),C.inline&&(l=t.defaultColor||C.defaultColor,E=Y(l),j(),U(l));break;case"clearButton":"object"==typeof t.clearButton&&(t.clearButton.label&&(C.clearLabel=t.clearButton.label,v.innerHTML=C.clearLabel),t.clearButton=t.clearButton.show),C.clearButton=!!t.clearButton,v.style.display=C.clearButton?"block":"none";break;case"clearLabel":C.clearLabel=t.clearLabel,v.innerHTML=C.clearLabel;break;case"closeButton":C.closeButton=!!t.closeButton,C.closeButton?f.insertBefore(m,b):b.appendChild(m);break;case"closeLabel":C.closeLabel=t.closeLabel,m.innerHTML=C.closeLabel;break;case"a11y":var a,l,r=t.a11y,n=!1;if("object"==typeof r)for(var o in r)r[o]&&C.a11y[o]&&(C.a11y[o]=r[o],n=!0);n&&(a=V("clr-open-label"),l=V("clr-swatch-label"),a.innerHTML=C.a11y.open,l.innerHTML=C.a11y.swatch,m.setAttribute("aria-label",C.a11y.close),v.setAttribute("aria-label",C.a11y.clear),g.setAttribute("aria-label",C.a11y.hueSlider),w.setAttribute("aria-label",C.a11y.alphaSlider),y.setAttribute("aria-label",C.a11y.input),h.setAttribute("aria-label",C.a11y.instruction));break;default:C[e]=t[e]}}function H(e,t){"string"==typeof e&&"object"==typeof t&&(o[e]=t,B=!0)}function N(e){delete o[e],0===Object.keys(o).length&&(B=!1,e===S&&O())}function t(l){if(B){var e,r=["el","wrap","rtl","inline","defaultColor","a11y"];for(e in o)if("break"===function(e){var t=o[e];if(l.matches(e)){for(var a in S=e,T={},r.forEach(function(e){return delete t[e]}),t)T[a]=Array.isArray(C[a])?C[a].slice():C[a];return M(t),"break"}}(e))break}}function O(){0<Object.keys(T).length&&(M(T),S="",T={})}function D(e){e instanceof HTMLElement&&(e=[e]),Array.isArray(e)?e.forEach(function(e){Z(e,"click",I),Z(e,"input",q)}):(Z(p,"click",e,I),Z(p,"input",e,q))}function I(e){C.inline||(t(e.target),L=e.target,a=L.value,E=Y(a),f.classList.add("clr-open"),j(),U(a),(C.focusInput||C.selectInput)&&(y.focus({preventScroll:!0}),y.setSelectionRange(L.selectionStart,L.selectionEnd)),C.selectInput&&y.select(),(n||C.swatchesOnly)&&Q().shift().focus(),L.dispatchEvent(new Event("open",{bubbles:!1})))}function j(){var e,t,a,l,r=d,n=u.scrollY,o=f.offsetWidth,c=f.offsetHeight,i={left:!1,top:!1},s={x:0,y:0};r&&(a=u.getComputedStyle(r),e=parseFloat(a.marginTop),l=parseFloat(a.borderTopWidth),(s=r.getBoundingClientRect()).y+=l+n),C.inline||(a=(t=L.getBoundingClientRect()).x,l=n+t.y+t.height+C.margin,r?(a-=s.x,l-=s.y,a+o>r.clientWidth&&(a+=t.width-o,i.left=!0),l+c>r.clientHeight-e&&c+C.margin<=t.top-(s.y-n)&&(l-=t.height+c+2*C.margin,i.top=!0),l+=r.scrollTop):(a+o>p.documentElement.clientWidth&&(a+=t.width-o,i.left=!0),l+c-n>p.documentElement.clientHeight&&c+C.margin<=t.top&&(l=n+t.y-c-C.margin,i.top=!0)),f.classList.toggle("clr-left",i.left),f.classList.toggle("clr-top",i.top),f.style.left=a+"px",f.style.top=l+"px",s.x+=f.offsetLeft,s.y+=f.offsetTop),A={width:h.offsetWidth,height:h.offsetHeight,x:h.offsetLeft+s.x,y:h.offsetTop+s.y}}function R(e){e instanceof HTMLElement?W(e):(Array.isArray(e)?e:p.querySelectorAll(e)).forEach(W)}function W(e){var t,a,l=e.parentNode;l.classList.contains("clr-field")||(t=p.createElement("div"),a="clr-field",(C.rtl||e.classList.contains("clr-rtl"))&&(a+=" clr-rtl"),t.innerHTML='<button type="button" aria-labelledby="clr-open-label"></button>',l.insertBefore(t,e),t.className=a,t.style.color=e.value,t.appendChild(e))}function q(e){var t=e.target.parentNode;t.classList.contains("clr-field")&&(t.style.color=e.target.value)}function F(e){var t;L&&!C.inline&&(t=L,e&&(L=c,a!==t.value&&(t.value=a,t.dispatchEvent(new Event("input",{bubbles:!0})))),setTimeout(function(){a!==t.value&&t.dispatchEvent(new Event("change",{bubbles:!0}))}),f.classList.remove("clr-open"),B&&O(),t.dispatchEvent(new Event("close",{bubbles:!1})),C.focusInput&&t.focus({preventScroll:!0}),L=c)}function U(e){var t=function(e){r.fillStyle="#000",r.fillStyle=e,e=(e=/^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i.exec(r.fillStyle))?{r:+e[3],g:+e[4],b:+e[5],a:+e[6]}:(e=r.fillStyle.replace("#","").match(/.{2}/g).map(function(e){return parseInt(e,16)}),{r:e[0],g:e[1],b:e[2],a:1});return e}(e),e=function(e){var t=e.r/255,a=e.g/255,l=e.b/255,r=s.max(t,a,l),n=s.min(t,a,l),o=r-n,c=r,i=0,n=0;o&&(r===t&&(i=(a-l)/o),r===a&&(i=2+(l-t)/o),r===l&&(i=4+(t-a)/o),r&&(n=o/r));return{h:(i=s.floor(60*i))<0?i+360:i,s:s.round(100*n),v:s.round(100*c),a:e.a}}(t);G(e.s,e.v),z(t,e),g.value=e.h,f.style.color="hsl("+e.h+", 100%, 50%)",l.style.left=e.h/360*100+"%",i.style.left=A.width*e.s/100+"px",i.style.top=A.height-A.height*e.v/100+"px",w.value=100*e.a,k.style.left=100*e.a+"%"}function Y(e){e=e.substring(0,3).toLowerCase();return"rgb"===e||"hsl"===e?e:"hex"}function P(e){e=e!==c?e:y.value,L&&(L.value=e,L.dispatchEvent(new Event("input",{bubbles:!0}))),C.onChange&&C.onChange.call(u,e,L),p.dispatchEvent(new CustomEvent("coloris:pick",{detail:{color:e,currentEl:L}}))}function X(e,t){e={h:+g.value,s:e/A.width*100,v:100-t/A.height*100,a:w.value/100},t=function(e){var t=e.s/100,a=e.v/100,l=t*a,r=e.h/60,n=l*(1-s.abs(r%2-1)),o=a-l;l+=o,n+=o;t=s.floor(r)%6,a=[l,n,o,o,n,l][t],r=[n,l,l,n,o,o][t],t=[o,o,n,l,l,n][t];return{r:s.round(255*a),g:s.round(255*r),b:s.round(255*t),a:e.a}}(e);G(e.s,e.v),z(t,e),P()}function G(e,t){var a=C.a11y.marker;e=+e.toFixed(1),t=+t.toFixed(1),a=(a=a.replace("{s}",e)).replace("{v}",t),i.setAttribute("aria-label",a)}function K(e){var t={pageX:((a=e).changedTouches?a.changedTouches[0]:a).pageX,pageY:(a.changedTouches?a.changedTouches[0]:a).pageY},a=t.pageX-A.x,t=t.pageY-A.y;d&&(t+=d.scrollTop),$(a,t),e.preventDefault(),e.stopPropagation()}function $(e,t){e=e<0?0:e>A.width?A.width:e,t=t<0?0:t>A.height?A.height:t,i.style.left=e+"px",i.style.top=t+"px",X(e,t),i.focus()}function z(e,t){void 0===t&&(t={});var a,l,r=C.format;for(a in e=void 0===e?{}:e)x[a]=e[a];for(l in t)x[l]=t[l];var n,o=function(e){var t=e.r.toString(16),a=e.g.toString(16),l=e.b.toString(16),r="";e.r<16&&(t="0"+t);e.g<16&&(a="0"+a);e.b<16&&(l="0"+l);C.alpha&&(e.a<1||C.forceAlpha)&&(e=255*e.a|0,r=e.toString(16),e<16&&(r="0"+r));return"#"+t+a+l+r}(x),c=o.substring(0,7);switch(i.style.color=c,k.parentNode.style.color=c,k.style.color=o,b.style.color=o,h.style.display="none",h.offsetHeight,h.style.display="",k.nextElementSibling.style.display="none",k.nextElementSibling.offsetHeight,k.nextElementSibling.style.display="","mixed"===r?r=1===x.a?"hex":"rgb":"auto"===r&&(r=E),r){case"hex":y.value=o;break;case"rgb":y.value=(n=x,!C.alpha||1===n.a&&!C.forceAlpha?"rgb("+n.r+", "+n.g+", "+n.b+")":"rgba("+n.r+", "+n.g+", "+n.b+", "+n.a+")");break;case"hsl":y.value=(n=function(e){var t,a=e.v/100,l=a*(1-e.s/100/2);0<l&&l<1&&(t=s.round((a-l)/s.min(l,1-l)*100));return{h:e.h,s:t||0,l:s.round(100*l),a:e.a}}(x),!C.alpha||1===n.a&&!C.forceAlpha?"hsl("+n.h+", "+n.s+"%, "+n.l+"%)":"hsla("+n.h+", "+n.s+"%, "+n.l+"%, "+n.a+")")}p.querySelector('.clr-format [value="'+r+'"]').checked=!0}function e(){var e=+g.value,t=+i.style.left.replace("px",""),a=+i.style.top.replace("px","");f.style.color="hsl("+e+", 100%, 50%)",l.style.left=e/360*100+"%",X(t,a)}function J(){var e=w.value/100;k.style.left=100*e+"%",z({a:e}),P()}function Q(){return Array.from(f.querySelectorAll("input, button")).filter(function(e){return!!e.offsetWidth})}function V(e){return p.getElementById(e)}function Z(e,t,a,l){var r=Element.prototype.matches||Element.prototype.msMatchesSelector;"string"==typeof a?e.addEventListener(t,function(e){r.call(e.target,a)&&l.call(e.target,e)}):(l=a,e.addEventListener(t,l))}function _(e,t){t=t!==c?t:[],"loading"!==p.readyState?e.apply(void 0,t):p.addEventListener("DOMContentLoaded",function(){e.apply(void 0,t)})}NodeList!==c&&NodeList.prototype&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=Array.prototype.forEach),u.Coloris=function(){var r={set:M,wrap:R,close:F,setInstance:H,removeInstance:N,updatePosition:j,ready:_};function e(e){_(function(){e&&("string"==typeof e?D:M)(e)})}for(var t in r)!function(l){e[l]=function(){for(var e=arguments.length,t=new Array(e),a=0;a<e;a++)t[a]=arguments[a];_(r[l],t)}}(t);return e}(),_(function(){d=c,(f=p.createElement("div")).setAttribute("id","clr-picker"),f.className="clr-picker",f.innerHTML='<input id="clr-color-value" name="clr-color-value" class="clr-color" type="text" value="" spellcheck="false" aria-label="'+C.a11y.input+'"><div id="clr-color-area" class="clr-gradient" role="application" aria-label="'+C.a11y.instruction+'"><div id="clr-color-marker" class="clr-marker" tabindex="0"></div></div><div class="clr-hue"><input id="clr-hue-slider" name="clr-hue-slider" type="range" min="0" max="360" step="1" aria-label="'+C.a11y.hueSlider+'"><div id="clr-hue-marker"></div></div><div class="clr-alpha"><input id="clr-alpha-slider" name="clr-alpha-slider" type="range" min="0" max="100" step="1" aria-label="'+C.a11y.alphaSlider+'"><div id="clr-alpha-marker"></div><span></span></div><div id="clr-format" class="clr-format"><fieldset class="clr-segmented"><legend>'+C.a11y.format+'</legend><input id="clr-f1" type="radio" name="clr-format" value="hex"><label for="clr-f1">Hex</label><input id="clr-f2" type="radio" name="clr-format" value="rgb"><label for="clr-f2">RGB</label><input id="clr-f3" type="radio" name="clr-format" value="hsl"><label for="clr-f3">HSL</label><span></span></fieldset></div><div id="clr-swatches" class="clr-swatches"></div><button type="button" id="clr-clear" class="clr-clear" aria-label="'+C.a11y.clear+'">'+C.clearLabel+'</button><div id="clr-color-preview" class="clr-preview"><button type="button" id="clr-close" class="clr-close" aria-label="'+C.a11y.close+'">'+C.closeLabel+'</button></div><span id="clr-open-label" hidden>'+C.a11y.open+'</span><span id="clr-swatch-label" hidden>'+C.a11y.swatch+"</span>",p.body.appendChild(f),h=V("clr-color-area"),i=V("clr-color-marker"),v=V("clr-clear"),m=V("clr-close"),b=V("clr-color-preview"),y=V("clr-color-value"),g=V("clr-hue-slider"),l=V("clr-hue-marker"),w=V("clr-alpha-slider"),k=V("clr-alpha-marker"),D(C.el),R(C.el),Z(f,"mousedown",function(e){f.classList.remove("clr-keyboard-nav"),e.stopPropagation()}),Z(h,"mousedown",function(e){Z(p,"mousemove",K)}),Z(h,"contextmenu",function(e){e.preventDefault()}),Z(h,"touchstart",function(e){p.addEventListener("touchmove",K,{passive:!1})}),Z(i,"mousedown",function(e){Z(p,"mousemove",K)}),Z(i,"touchstart",function(e){p.addEventListener("touchmove",K,{passive:!1})}),Z(y,"change",function(e){var t=y.value;(L||C.inline)&&P(""===t?t:U(t))}),Z(v,"click",function(e){P(""),F()}),Z(m,"click",function(e){P(),F()}),Z(V("clr-format"),"click",".clr-format input",function(e){E=e.target.value,z(),P()}),Z(f,"click",".clr-swatches button",function(e){U(e.target.textContent),P(),C.swatchesOnly&&F()}),Z(p,"mouseup",function(e){p.removeEventListener("mousemove",K)}),Z(p,"touchend",function(e){p.removeEventListener("touchmove",K)}),Z(p,"mousedown",function(e){n=!1,f.classList.remove("clr-keyboard-nav"),F()}),Z(p,"keydown",function(e){var t,a=e.key,l=e.target,r=e.shiftKey;"Escape"!==a?"Enter"!==a||"BUTTON"===l.tagName?(["Tab","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(a)&&(n=!0,f.classList.add("clr-keyboard-nav")),"Tab"===a&&l.matches(".clr-picker *")&&(a=(t=Q()).shift(),t=t.pop(),r&&l===a?(t.focus(),e.preventDefault()):r||l!==t||(a.focus(),e.preventDefault()))):F():F(!0)}),Z(p,"click",".clr-field button",function(e){B&&O(),e.target.nextElementSibling.dispatchEvent(new Event("click",{bubbles:!0}))}),Z(i,"keydown",function(e){var t={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]};Object.keys(t).includes(e.key)&&(!function(e,t){$(+i.style.left.replace("px","")+e,+i.style.top.replace("px","")+t)}.apply(void 0,t[e.key]),e.preventDefault())}),Z(h,"click",K),Z(g,"input",e),Z(w,"input",J)})}(window,document,Math);
// ─── FLOATING CUSTOMIZE BUTTON ────────────────────────────────────────────
function initCustomizeButton() {
if (document.getElementById('cai-customize-toggle')) return;
// Add CSS for the floating button
GM_addStyle(`
#cai-customize-toggle {
width: 48px; height: 48px; border-radius: 50%;
background-color: #18181b; color: #e4e4e7;
border: 1px solid #3f3f46; box-shadow: 0 4px 12px rgba(0,0,0,0.5);
cursor: grab; display: flex; align-items: center; justify-content: center;
font-size: 22px; transition: background-color 0.2s, transform 0.1s;
position: fixed; bottom: 80px; right: 20px; /* Positioned above the translate button */
z-index: 999999; user-select: none; touch-action: none;
}
#cai-customize-toggle:active { cursor: grabbing; transform: scale(0.95); }
#cai-customize-toggle:hover { background-color: #27272a; }
`);
// Create the button element
const toggleBtn = document.createElement('button');
toggleBtn.id = 'cai-customize-toggle';
toggleBtn.title = 'Drag to move, Click to Customize';
toggleBtn.innerHTML = '🎨'; // You can change this emoji to a gear ⚙️ or anything else
document.body.appendChild(toggleBtn);
// Restore saved position, if exists
const savedPos = GM_getValue('cai_btn_pos', null);
if (savedPos) {
try {
const pos = JSON.parse(savedPos);
toggleBtn.style.bottom = 'auto';
toggleBtn.style.right = 'auto';
toggleBtn.style.left = pos.left + 'px';
toggleBtn.style.top = pos.top + 'px';
} catch (e) {}
}
// Drag and drop logic (Mouse + Touch)
let isDragging = false, dragHasMoved = false, startX, startY, initialLeft, initialTop;
function startDrag(e) {
isDragging = true; dragHasMoved = false;
startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
startY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
const r = toggleBtn.getBoundingClientRect();
initialLeft = r.left; initialTop = r.top;
toggleBtn.style.bottom = 'auto'; toggleBtn.style.right = 'auto';
toggleBtn.style.left = initialLeft + 'px'; toggleBtn.style.top = initialTop + 'px';
document.addEventListener('mousemove', onDragMove, { passive: false });
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('touchend', onDragEnd);
}
function onDragMove(e) {
if (!isDragging) return;
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
const dx = clientX - startX, dy = clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragHasMoved = true;
toggleBtn.style.left = (initialLeft + dx) + 'px';
toggleBtn.style.top = (initialTop + dy) + 'px';
if (dragHasMoved && e.type.includes('touch')) e.preventDefault();
}
function onDragEnd() {
isDragging = false;
if (dragHasMoved) {
const r = toggleBtn.getBoundingClientRect();
GM_setValue('cai_btn_pos', JSON.stringify({ left: r.left, top: r.top }));
}
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
document.removeEventListener('touchmove', onDragMove);
document.removeEventListener('touchend', onDragEnd);
}
toggleBtn.addEventListener('mousedown', startDrag);
toggleBtn.addEventListener('touchstart', startDrag, { passive: false });
// Click logic
toggleBtn.addEventListener('click', (e) => {
if (dragHasMoved) { e.preventDefault(); e.stopPropagation(); return; }
buildModal();
});
}
const nameElCache = new WeakMap();
const STORAGE_KEY = 'cai_bubble_styles_v3';
const PRESETS_KEY = 'cai_saved_presets_v1';
function loadPresets() { try { return JSON.parse(GM_getValue(PRESETS_KEY, '[]')); } catch { return []; } }
function savePresets(arr) { GM_setValue(PRESETS_KEY, JSON.stringify(arr)); }
const DRAFT_ENABLED_KEY = 'cai_msg_draft_enabled';
function isDraftEnabled() { return GM_getValue(DRAFT_ENABLED_KEY, true) === true; }
const HIDE_DISTRACTORS_KEY = 'cai_hide_distractors_enabled';
function isHideDistractorsEnabled() { return GM_getValue(HIDE_DISTRACTORS_KEY, true) === true; }
// ─── EARLY HIDE-DISTRACTORS CSS (se aplica antes de que el JS corra) ───
if (isHideDistractorsEnabled()) {
document.documentElement.classList.add('cai-early-hide-distractors');
}
GM_addStyle(`
.cai-early-hide-distractors [class*="bg-warning"],
.cai-early-hide-distractors a[href="/feed"],
.cai-early-hide-distractors a[href="/labs"],
.cai-early-hide-distractors a[href="https://labs.character.ai"] {
display: none !important;
}
`);
function getDraftStorageKey(botId) { return 'cai_msg_draft_' + botId; }
function saveDraft(botId, text) {
GM_setValue(getDraftStorageKey(botId), text || '');
}
function loadDraft(botId) { return GM_getValue(getDraftStorageKey(botId), ''); }
// ─── GLOBAL SELECTORS ───
// FIX: Wrapped selectors in :is() to fix CSS comma-separated descendant issues.
// This ensures text color targets all elements inside AI bubbles properly.
const BUBBLE_TARGET = '[data-testid="completed-message"], [data-testid="active-message"], [data-testid="generating-message"], [data-testid="streaming-message"], [data-testid="false"], .group\\/message .bg-surface-elevated';
const selMsg = `:is(${BUBBLE_TARGET})`;
const selAIWrapper = `:is(${BUBBLE_TARGET})`; // Baseline AI styling. User styles will override due to higher specificity.
const selUser = `.flex-row-reverse :is(${BUBBLE_TARGET})`;
const selInputBox = '.flex.grow:has(#chat-input-textarea)';
const selOuterBtns = '.w-full.flex.justify-center.items-center > button';
const selThemeTargets = `:is(${selUser}, ${selInputBox}, ${selOuterBtns})`;
const selRow = `:is(#chat-messages > div:has(${BUBBLE_TARGET}) > div:has(${BUBBLE_TARGET}), #chat-messages .swiper-slide-active > div:has(${BUBBLE_TARGET}) > div:has(${BUBBLE_TARGET}) > div:has(${BUBBLE_TARGET}))`;
const selActionsBar = `#chat-messages .swiper-slide-active > div:has(${BUBBLE_TARGET}) > div.absolute`;
const selRowUser = `${selRow}.flex-row-reverse`;
const selRowAI = `${selRow}:not(.flex-row-reverse)`;
const DEFAULT_FONT_SIZE = 16;
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 32;
function clampFontSize(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_FONT_SIZE;
return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, v));
}
// ─── LINE HEIGHT ───
const DEFAULT_LINE_HEIGHT = 1.5;
const MIN_LINE_HEIGHT = 1.0;
const MAX_LINE_HEIGHT = 2.2;
function clampLineHeight(v) {
v = parseFloat(v);
if (isNaN(v)) v = DEFAULT_LINE_HEIGHT;
return Math.min(MAX_LINE_HEIGHT, Math.max(MIN_LINE_HEIGHT, Math.round(v * 10) / 10));
}
// ─── LETTER SPACING ───
const DEFAULT_LETTER_SPACING = 0;
const MIN_LETTER_SPACING = -4;
const MAX_LETTER_SPACING = 8;
function clampLetterSpacing(v) {
v = parseFloat(v);
if (isNaN(v)) v = DEFAULT_LETTER_SPACING;
return Math.min(MAX_LETTER_SPACING, Math.max(MIN_LETTER_SPACING, Math.round(v * 10) / 10));
}
// ─── TEXT ALIGN ───
const DEFAULT_TEXT_ALIGN = 'left';
function clampTextAlign(v) {
return ['left','center','right'].includes(v) ? v : DEFAULT_TEXT_ALIGN;
}
// ─── MESSAGE SPACING ───
const DEFAULT_MSG_SPACING = 0;
const MIN_MSG_SPACING = -10;
const MAX_MSG_SPACING = 40;
function clampMsgSpacing(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_MSG_SPACING;
return Math.min(MAX_MSG_SPACING, Math.max(MIN_MSG_SPACING, v));
}
// ─── AVATAR SIZE (0=24px, 1=36px, 2=48px, 3=64px) ───
const AVATAR_SIZES = [24, 36, 48, 64];
const DEFAULT_AVATAR_IDX = 0;
function clampAvatarIdx(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_AVATAR_IDX;
return Math.min(3, Math.max(0, v));
}
// ─── BUBBLE BORDER ───
const DEFAULT_BORDER_WIDTH = 2;
const MIN_BORDER_WIDTH = 0;
const MAX_BORDER_WIDTH = 10;
function clampBorderWidth(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_BORDER_WIDTH;
return Math.min(MAX_BORDER_WIDTH, Math.max(MIN_BORDER_WIDTH, v));
}
// ─── BUBBLE SHADOW ───
const DEFAULT_SHADOW_SPREAD = 0;
const DEFAULT_SHADOW_BLUR = 10;
const DEFAULT_SHADOW_OFFSET_X = 0;
const DEFAULT_SHADOW_OFFSET_Y = 4;
function clampShadowSpread(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_SHADOW_SPREAD;
return Math.min(50, Math.max(-30, v));
}
function clampShadowBlur(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_SHADOW_BLUR;
return Math.min(40, Math.max(0, v));
}
function clampShadowOffset(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = 0;
return Math.min(20, Math.max(-20, v));
}
// ─── BUBBLE INNER SHADOW ───
const DEFAULT_INNER_SHADOW_SPREAD = 0;
const DEFAULT_INNER_SHADOW_BLUR = 10;
const DEFAULT_INNER_SHADOW_OFFSET_X = 0;
const DEFAULT_INNER_SHADOW_OFFSET_Y = 4;
function clampInnerShadowSpread(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_INNER_SHADOW_SPREAD;
return Math.min(50, Math.max(-30, v));
}
function clampInnerShadowBlur(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_INNER_SHADOW_BLUR;
return Math.min(40, Math.max(0, v));
}
function clampInnerShadowOffset(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = 0;
return Math.min(20, Math.max(-20, v));
}
// ─── BUBBLE CORNERS ───
const DEFAULT_CORNER_RADIUS = 16;
const MIN_CORNER_RADIUS = 0;
const MAX_CORNER_RADIUS = 50;
function clampCornerRadius(v) {
v = parseInt(v, 10);
if (isNaN(v)) v = DEFAULT_CORNER_RADIUS;
return Math.min(MAX_CORNER_RADIUS, Math.max(MIN_CORNER_RADIUS, v));
}
// ─── BUBBLE DEFINITIONS ───
const BUBBLES = [
{ id:0, name:'Default', cat:'Default', isDefault:true, colorLock:false, font:'', aiBg:'#242529', aiTx:'#e3e3e4', uBg:'#2d2e34', uTx:'#e3e3e4', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); }' },
{ id:111, name:'Default Light', cat:'Default', isDefault:true, colorLock:false, font:'', aiBg:'#e6e6ea', aiTx:'#44454a', uBg:'#dcdce1', uTx:'#44454a', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); }' },
// ── CAT 1: CLASSICS
{ id:1, name:'iOS Light', cat:'1. The Classics', font:'Onest', aiBg:'#e5e5ea', aiTx:'#000000', uBg:'#0b93f6', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 18px 18px 18px 4px; } .cai-user { border-radius: 18px 18px 4px 18px; }' },
{ id:2, name:'iOS Dark Blue', cat:'1. The Classics', font:'Onest', aiBg:'#0b93f6', aiTx:'#ffffff', uBg:'#e5e5ea', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 18px 18px 18px 4px; } .cai-user { border-radius: 18px 18px 4px 18px; }' },
{ id:3, name:'WhatsApp', cat:'1. The Classics', font:'Onest', aiBg:'#ffffff', aiTx:'#000000', uBg:'#dcf8c6', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); box-shadow: 0 1px 1px rgba(0,0,0,0.1); position: relative; overflow: visible !important; } .cai-ai { border-radius: 0 8px 8px 8px; } .cai-ai::before { content:""; position:absolute; top:0; right:calc(100%); width:0; height:0; border-top: 10px solid var(--cai-bg); border-left: 10px solid transparent; } .cai-user { border-radius: 8px 0 8px 8px; } .cai-user::before { content:""; position:absolute; top:0; left:calc(100% - 10px); width:0; height:0; border-top: 10px solid var(--cai-bg); border-right: 10px solid transparent; }' },
{ id:4, name:'Android/SMS', cat:'1. The Classics', font:'Onest', aiBg:'#ffffff', aiTx:'#000000', uBg:'#0084ff', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); box-shadow: 0 1px 2px rgba(0,0,0,0.1); } .cai-ai { border-radius: 12px 12px 12px 0; } .cai-user { border-radius: 18px; }' },
{ id:5, name:'Messenger', cat:'1. The Classics', font:'Onest', aiBg:'#f1f0f0', aiTx:'#000000', uBg:'#0084ff', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 20px; border: 1px solid #ddd; } .cai-user { border-radius: 18px; }' },
{ id:6, name:'Dark Mode', cat:'1. The Classics', font:'Onest', aiBg:'#333333', aiTx:'#ffffff', uBg:'#555555', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 18px 18px 18px 4px; } .cai-user { border-radius: 18px 18px 4px 18px; }' },
{ id:7, name:'Wireframe', cat:'1. The Classics', font:'Onest', aiBg:'#f1f0f0', aiTx:'#000000', uBg:'#ffffff', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 20px; border: 1px solid #ddd; }' },
{ id:8, name:'WeChat Green', cat:'1. The Classics', font:'Onest', aiBg:'#ffffff', aiTx:'#000000', uBg:'#95ec69', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 4px; position: relative; overflow: visible !important; } .cai-ai::before { content:""; position:absolute; top:12px; right:100%; width:0; height:0; border-style: solid; border-width: 5px 6px 5px 0; border-color: transparent var(--cai-bg) transparent transparent; } .cai-user::before { content:""; position:absolute; top:12px; left:100%; width:0; height:0; border-style: solid; border-width: 5px 0 5px 6px; border-color: transparent transparent transparent var(--cai-bg); }' },
{ id:9, name:'Discord Blurple', cat:'1. The Classics', font:'Onest', aiBg:'#5865F2', aiTx:'#ffffff', uBg:'#5865F2', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 8px; }' },
{ id:10, name:'Dating App Pink', cat:'1. The Classics', font:'Onest', aiBg:'#FF3366', aiTx:'#ffffff', uBg:'#FF3366', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 20px 20px 0 20px; } .cai-user { border-radius: 20px 20px 20px 0; }' },
// ── CAT 2: GRADIENTS
{ id:11, name:'Sunset', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #ff9a9e, #fecfef) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #fecfef, #ff9a9e) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:12, name:'Ocean', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #a1c4fd, #c2e9fb) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #c2e9fb, #a1c4fd) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:13, name:'Mango', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #f6d365, #fda085) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #fda085, #f6d365) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:14, name:'Mint', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #84fab0, #8fd3f4) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #8fd3f4, #84fab0) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:15, name:'Lavender', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #a18cd1, #fbc2eb) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #fbc2eb, #a18cd1) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:16, name:'Cyber', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #ff0844, #ffb199) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #ffb199, #ff0844) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:17, name:'Twilight', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #30cfd0, #330867) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #330867, #30cfd0) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:18, name:'Breeze', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #4facfe, #00f2fe) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #00f2fe, #4facfe) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:19, name:'Forest', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #43e97b, #38f9d7) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #38f9d7, #43e97b) !important; border-radius: 18px 18px 4px 18px; }' },
{ id:20, name:'Lemonade', cat:'2. Gorgeous Gradients', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { color: var(--cai-tx) !important; } .cai-msg * { color: var(--cai-tx) !important; } .cai-ai { background: linear-gradient(135deg, #fa709a, #fee140) !important; border-radius: 18px 18px 18px 4px; } .cai-user { background: linear-gradient(135deg, #fee140, #fa709a) !important; border-radius: 18px 18px 4px 18px; }' },
// ── CAT 3: SHADOWS & NEUMORPHISM
{ id:21, name:'Neumorphic Light', cat:'3. Shadows & Neumorphism', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: #e0e5ec !important; color: var(--cai-tx) !important; border-radius: 16px; box-shadow: 5px 5px 10px #b8bcc2, -5px -5px 10px #ffffff !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:22, name:'Neumorphic Dark', cat:'3. Shadows & Neumorphism', colorLock:true, font:'Onest', aiTx:'#dddddd', uTx:'#dddddd', css:'.cai-msg { background: #2b2b2b !important; color: var(--cai-tx) !important; border-radius: 16px; box-shadow: 4px 4px 8px #1a1a1a, -4px -4px 8px #3c3c3c !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:23, name:'Neumorphic Inset', cat:'3. Shadows & Neumorphism', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: #e0e5ec !important; color: var(--cai-tx) !important; border-radius: 16px; box-shadow: inset 4px 4px 8px #b8bcc2, inset -4px -4px 8px #ffffff !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:24, name:'Floating White', cat:'3. Shadows & Neumorphism', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; box-shadow: 0 10px 20px rgba(0,0,0,0.15) !important; transform: translateY(-2px); }' },
{ id:25, name:'Sharp Shadow Light', cat:'3. Shadows & Neumorphism', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 2px solid #000; border-radius: 12px; box-shadow: 4px 4px 0 #000 !important; }' },
{ id:26, name:'Sharp Shadow Gold', cat:'3. Shadows & Neumorphism', colorLock:false, font:'Onest', aiBg:'#ffd700', aiTx:'#000000', uBg:'#ffd700', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 2px solid #000; border-radius: 12px; box-shadow: 4px 4px 0 #000 !important; }' },
{ id:27, name:'Clay Pink', cat:'3. Shadows & Neumorphism', colorLock:true, font:'Onest', aiTx:'#000000', uTx:'#000000', css:'.cai-msg { background: #ffb6c1 !important; color: var(--cai-tx) !important; border-radius: 24px; box-shadow: inset -4px -4px 8px rgba(0,0,0,0.1), inset 4px 4px 8px rgba(255,255,255,0.8) !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:28, name:'Clay Blue', cat:'3. Shadows & Neumorphism', colorLock:true, font:'Onest', aiTx:'#000000', uTx:'#000000', css:'.cai-msg { background: #87ceeb !important; color: var(--cai-tx) !important; border-radius: 24px; box-shadow: inset -4px -4px 8px rgba(0,0,0,0.1), inset 4px 4px 8px rgba(255,255,255,0.8) !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:29, name:'Left Connected', cat:'3. Shadows & Neumorphism', colorLock:false, font:'Onest', aiBg:'#f3f4f6', aiTx:'#333333', uBg:'#f3f4f6', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 0 16px 16px 16px; border-left: 4px solid #3b82f6; box-shadow: 2px 2px 10px rgba(0,0,0,0.05); } .cai-user { border-radius: 16px 0 16px 16px; border-right: 4px solid #ef4444; box-shadow: -2px 2px 10px rgba(0,0,0,0.05); }' },
{ id:30, name:'Right Connected', cat:'3. Shadows & Neumorphism', colorLock:false, font:'Onest', aiBg:'#f3f4f6', aiTx:'#333333', uBg:'#f3f4f6', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 16px 0 16px 16px; border-right: 4px solid #ef4444; box-shadow: -2px 2px 10px rgba(0,0,0,0.05); } .cai-user { border-radius: 0 16px 16px 16px; border-left: 4px solid #3b82f6; box-shadow: 2px 2px 10px rgba(0,0,0,0.05); }' },
// ── CAT 4: KAWAII SOFT & SWEET
{ id:31, name:'Cotton Candy', cat:'4. Kawaii Soft & Sweet', colorLock:true, font:'Onest', aiTx:'#4c1d95', uTx:'#4c1d95', css:'.cai-msg { background: linear-gradient(135deg, #fbcfe8 0%, #bfdbfe 100%) !important; color: var(--cai-tx) !important; border-radius: 24px; box-shadow: 0 4px 15px rgba(191,219,254,0.5) !important; font-weight: 500; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:32, name:'Strawberry Milk', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#fdf2f8', aiTx:'#831843', uBg:'#fdf2f8', uTx:'#831843', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 3px solid #f9a8d4; font-weight: 500; } .cai-ai { border-radius: 24px 24px 24px 8px; } .cai-user { border-radius: 24px 24px 8px 24px; }' },
{ id:33, name:'Blushing Cheeks', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 24px; position: relative; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05); } .cai-msg::before, .cai-msg::after { content: ""; position: absolute; bottom: -15px; width: 50px; height: 50px; background: #f9a8d4; border-radius: 50%; filter: blur(12px); opacity: 0.6; z-index: 0; pointer-events: none; } .cai-msg::before { left: -15px; } .cai-msg::after { right: -15px; }' },
{ id:34, name:'Squishy Mochi', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#dcfce7', aiTx:'#14532d', uBg:'#dcfce7', uTx:'#14532d', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 30px; box-shadow: inset -4px -4px 10px rgba(20,83,45,0.1), inset 4px 4px 10px rgba(255,255,255,0.9) !important; font-weight: 500; }' },
{ id:35, name:'Lemon Dashed', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#fef9c3', aiTx:'#854d0e', uBg:'#fef9c3', uTx:'#854d0e', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 20px; border: 2px dashed #fde047; }' },
{ id:36, name:'Scalloped Lace', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#e0e7ff', aiTx:'#312e81', uBg:'#e0e7ff', uTx:'#312e81', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 0 0 16px 16px; margin-top: 12px; position: relative; overflow: visible !important; } .cai-msg::before { content:""; position:absolute; top:-10px; left:0; right:0; height:10px; background-image: radial-gradient(circle at 50% 100%, var(--cai-bg) 6px, transparent 6px); background-size: 12px 10px; background-repeat: repeat-x; }' },
{ id:37, name:'Bubblegum Gloss', cat:'4. Kawaii Soft & Sweet', colorLock:true, font:'Onest', aiTx:'#831843', uTx:'#831843', css:'.cai-msg { background: #fbcfe8 !important; color: var(--cai-tx) !important; border-radius: 30px; box-shadow: inset 0 8px 10px rgba(255,255,255,0.8), 0 4px 10px rgba(249,168,212,0.4) !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:38, name:'Caramel Pudding', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#fef08a', aiTx:'#713f12', uBg:'#fef08a', uTx:'#713f12', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 12px 12px 24px 24px; border-top: 14px solid #78350f; position: relative; overflow: visible !important; } .cai-msg::before { content:""; position:absolute; top:-14px; left:16px; width:12px; height:22px; background:#78350f; border-radius:10px; } .cai-msg::after { content:""; position:absolute; top:-14px; right:24px; width:14px; height:28px; background:#78350f; border-radius:10px; }' },
{ id:39, name:'Fluffy Cloud', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 24px; margin-top: 16px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); position: relative; overflow: visible !important; } .cai-msg::before, .cai-msg::after { content: ""; position: absolute; background: var(--cai-bg); border-radius: 50%; z-index: -1; } .cai-msg::before { top: -14px; left: 24px; width: 32px; height: 32px; } .cai-msg::after { top: -18px; left: 50px; width: 44px; height: 44px; }' },
{ id:40, name:'Sweet Confetti', cat:'4. Kawaii Soft & Sweet', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 20px; background-image: radial-gradient(circle at 15px 15px, #f9a8d4 3px, transparent 4px), radial-gradient(circle at 45px 30px, #818cf8 3px, transparent 4px), radial-gradient(circle at 75px 12px, #fde047 3px, transparent 4px), radial-gradient(circle at 105px 25px, #6ee7b7 3px, transparent 4px), radial-gradient(circle at 135px 15px, #fca5a5 3px, transparent 4px); background-size: 150px 45px; border: 2px solid #f3f4f6; }' },
// ── CAT 5: BADGES & ADD-ONS
{ id:41, name:'Royal Crown', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 1px solid #ffd700; margin-top:15px; position:relative; overflow: visible !important; } .cai-msg::after { content:"👑"; position:absolute; top:-18px; right:-10px; font-size:24px; }' },
{ id:42, name:'Heart Ribbon', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 1px solid #ff69b4; position:relative; overflow: visible !important; } .cai-msg::after { content:"💖"; position:absolute; bottom:-10px; right:-10px; font-size:20px; }' },
{ id:43, name:'Unread Dot', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#e5e5ea', aiTx:'#333333', uBg:'#e5e5ea', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; position:relative; overflow: visible !important; } .cai-msg::after { content:""; position:absolute; top:-5px; right:-5px; width:12px; height:12px; background:red; border-radius:50%; border:2px solid #f3f4f6; }' },
{ id:44, name:'Quoted Message', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; overflow:hidden; position:relative; z-index:1; } .cai-msg::before { content:"\\""; position:absolute; top:-10px; left:5px; font-size:60px; color:rgba(0,0,0,0.05); font-family:serif; z-index:-1; }' },
{ id:45, name:'Pinned Note', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#fffbd5', aiTx:'#333333', uBg:'#fffbd5', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 4px; box-shadow: 2px 2px 5px rgba(0,0,0,0.1); position:relative; margin-top:15px; overflow: visible !important; } .cai-msg::before { content:"📌"; position:absolute; top:-12px; left:50%; transform:translateX(-50%); font-size:20px; }' },
{ id:46, name:'VIP Star', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#1a1a1a', aiTx:'#ffffff', uBg:'#1a1a1a', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; position:relative; overflow: visible !important; } .cai-msg::after { content:"✨"; position:absolute; top:-10px; left:-10px; font-size:20px; }' },
{ id:47, name:'Spring Flower', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#e0f7fa', aiTx:'#006064', uBg:'#e0f7fa', uTx:'#006064', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; position:relative; overflow: visible !important; } .cai-msg::after { content:"🌸"; position:absolute; bottom:-10px; left:-10px; font-size:24px; }' },
{ id:48, name:'Sparkles', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 2px dashed #a855f7; position:relative; overflow: visible !important; } .cai-msg::before { content:"✦"; position:absolute; top:-8px; right:-5px; color:#a855f7; font-size:18px;} .cai-msg::after { content:"✦"; position:absolute; bottom:-8px; left:-5px; color:#a855f7; font-size:18px;}' },
{ id:49, name:'Music Note', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#fdf2f8', aiTx:'#831843', uBg:'#fdf2f8', uTx:'#831843', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; position:relative; overflow: visible !important; } .cai-msg::after { content:"🎵"; position:absolute; top:-12px; right:10px; font-size:18px; animation: caiBounce 2s infinite; } @keyframes caiBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }' },
{ id:50, name:'Sleeping Zzz', cat:'5. Badges & Add-ons', colorLock:false, font:'Onest', aiBg:'#1e1b4b', aiTx:'#c7d2fe', uBg:'#1e1b4b', uTx:'#c7d2fe', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; position:relative; overflow: visible !important; } .cai-msg::after { content:"💤"; position:absolute; top:-15px; right:-5px; font-size:16px; color:#fff;}' },
// ── CAT 6: PATTERNS
{ id:51, name:'Polka Dots', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: radial-gradient(#cbd5e1 15%, transparent 16%) 0 0, radial-gradient(#cbd5e1 15%, transparent 16%) 8px 8px !important; background-size: 16px 16px !important; background-color: #f8fafc !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:52, name:'Candy Stripes', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#991b1b', uTx:'#991b1b', css:'.cai-msg { background: repeating-linear-gradient(45deg, #fecaca, #fecaca 10px, #fff 10px, #fff 20px) !important; color: var(--cai-tx) !important; border-radius: 16px; font-weight: 500;} .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:53, name:'Graph Paper', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px), linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px) !important; background-size: 10px 10px !important; background-color: #fff !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:54, name:'Sunburst', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: repeating-conic-gradient(#e2e8f0 0% 25%, #fff 0% 50%) !important; background-size: 20px 20px !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:55, name:'Blue Chevron', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#1e3a8a', uTx:'#1e3a8a', css:'.cai-msg { background: repeating-linear-gradient(-45deg, #bfdbfe, #bfdbfe 5px, #fff 5px, #fff 10px) !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:56, name:'Yellow Zigzag', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#854d0e', uTx:'#854d0e', css:'.cai-msg { background: repeating-linear-gradient(0deg, #fef08a, #fef08a 2px, #fff 2px, #fff 6px) !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:57, name:'Purple Waves', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#4c1d95', uTx:'#4c1d95', css:'.cai-msg { background: radial-gradient(circle at 100% 50%, transparent 20%, #e9d5ff 21%, #e9d5ff 34%, transparent 35%, transparent), radial-gradient(circle at 0% 50%, transparent 20%, #e9d5ff 21%, #e9d5ff 34%, transparent 35%, transparent) 0 -25px !important; background-size: 30px 40px !important; background-color: #fff !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:58, name:'TV Static Noise', cat:'6. Fun Patterns', colorLock:true, font:'VT323', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: url("data:image/svg+xml,%3Csvg viewBox=\'0 0 200 200\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noiseFilter\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noiseFilter)\'/%3E%3C/svg%3E") !important; color: var(--cai-tx) !important; border-radius: 16px; text-shadow: 1px 1px 2px #000; background-color: #444 !important;} .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:59, name:'Tartan Plaid', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: repeating-linear-gradient(90deg, transparent, transparent 20px, rgba(255,0,0,0.1) 20px, rgba(255,0,0,0.1) 22px), repeating-linear-gradient(0deg, transparent, transparent 20px, rgba(0,0,255,0.1) 20px, rgba(0,0,255,0.1) 22px) !important; background-color: #fff !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:60, name:'Honey Dots', cat:'6. Fun Patterns', colorLock:true, font:'Onest', aiTx:'#b45309', uTx:'#b45309', css:'.cai-msg { background: radial-gradient(circle, transparent 20%, #fff 20%, #fff 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #fff 20%, #fff 80%, transparent 80%, transparent) 25px 25px, linear-gradient(#fcd34d 4px, transparent 4px) 0 -2px, linear-gradient(90deg, #fcd34d 4px, #fff 4px) !important; background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px !important; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
// ── CAT 7: SHAPES & OUTLINES
{ id:61, name:'Comic Book', cat:'7. Shapes & Outlines', font:'Comic Neue', aiBg:'#ffffff', aiTx:'#000000', uBg:'#ffffff', uTx:'#000000', spName:'Shadow', spDef:'#ff3366', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 3px solid #000; border-radius: 16px; box-shadow: 3px 3px 0 var(--cai-sp) !important; }' },
{ id:62, name:'Dashed Border', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'transparent', aiTx:'#333333', uBg:'transparent', uTx:'#333333', spName:'Border Color', spDef:'#888888', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 2px dashed var(--cai-sp); border-radius: 16px; }' },
{ id:63, name:'Dotted Border', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', spName:'Dot Color', spDef:'#3b82f6', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 3px dotted var(--cai-sp); border-radius: 16px; }' },
{ id:64, name:'Double Line', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', spName:'Border Color', spDef:'#10b981', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border: 4px double var(--cai-sp); border-radius: 16px; }' },
{ id:65, name:'Leaf Cut', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#34d399', aiTx:'#ffffff', uBg:'#34d399', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); } .cai-ai { border-radius: 0 24px 0 24px; } .cai-user { border-radius: 24px 0 24px 0; }' },
{ id:66, name:'Pill Shape', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#f472b6', aiTx:'#ffffff', uBg:'#f472b6', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 50px; text-align: center; }' },
{ id:67, name:'Sharp Box', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#333333', aiTx:'#ffffff', uBg:'#333333', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 0; }' },
{ id:68, name:'Movie Ticket', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 8px; mask-image: radial-gradient(circle at 100% 50%, transparent 6px, black 7px), radial-gradient(circle at 0% 50%, transparent 6px, black 7px); mask-size: 51% 100%; mask-position: right, left; mask-repeat: no-repeat; -webkit-mask-image: radial-gradient(circle at 100% 50%, transparent 6px, black 7px), radial-gradient(circle at 0% 50%, transparent 6px, black 7px); -webkit-mask-size: 51% 100%; -webkit-mask-position: right, left; -webkit-mask-repeat: no-repeat; border-left: 2px dashed #ccc; border-right: 2px dashed #ccc; }' },
{ id:69, name:'Folded Paper Corner', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#fbbf24', aiTx:'#000000', uBg:'#fbbf24', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 8px; position: relative; } .cai-ai { border-top-left-radius: 0; } .cai-ai::before { content:""; position:absolute; top:0; left:0; width:0; height:0; border-width: 0 0 20px 20px; border-style: solid; border-color: transparent transparent var(--cai-bg) rgba(0,0,0,0.15); box-shadow: 2px 2px 2px rgba(0,0,0,0.1); border-bottom-right-radius: 4px; } .cai-user { border-top-right-radius: 0; } .cai-user::before { content:""; position:absolute; top:0; right:0; width:0; height:0; border-width: 0 20px 20px 0; border-style: solid; border-color: transparent transparent var(--cai-bg) rgba(0,0,0,0.15); box-shadow: -2px 2px 2px rgba(0,0,0,0.1); border-bottom-left-radius: 4px; }' },
{ id:70, name:'Hexagon Cut', cat:'7. Shapes & Outlines', font:'Onest', aiBg:'#8b5cf6', aiTx:'#ffffff', uBg:'#8b5cf6', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); clip-path: polygon(10% 0, 100% 0, 100% 80%, 90% 100%, 0 100%, 0 20%); border-radius: 4px; }' },
// ── CAT 8: MATERIALS & TEXTURES
{ id:71, name:'Frosted Glass', cat:'8. Materials & Textures', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: rgba(255,255,255,0.4) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; border: 1px solid rgba(255,255,255,0.5); color: var(--cai-tx) !important; border-radius: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:72, name:'Tinted Dark Glass', cat:'8. Materials & Textures', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: rgba(0,0,0,0.6) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; color: var(--cai-tx) !important; border-radius: 16px; border: 1px solid rgba(255,255,255,0.1); } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:73, name:'Metallic Silver', cat:'8. Materials & Textures', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: linear-gradient(to bottom, #f5f5f5, #e0e0e0) !important; color: var(--cai-tx) !important; border-radius: 16px; border: 1px solid #ccc; box-shadow: inset 0 1px 0 #fff; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:74, name:'Gold Foil', cat:'8. Materials & Textures', colorLock:true, font:'Onest', aiTx:'#4a3300', uTx:'#4a3300', css:'.cai-msg { background: linear-gradient(135deg, #bf953f, #fcf6ba, #b38728, #fbf5b7, #aa771c) !important; color: var(--cai-tx) !important; border-radius: 16px; font-weight: 600; box-shadow: 0 4px 10px rgba(170,119,28,0.3) !important; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:75, name:'Wooden Board', cat:'8. Materials & Textures', font:'Comic Neue', colorLock:true, aiTx:'#5c3a21', uTx:'#5c3a21', css:'.cai-msg { background: #deb887 !important; color: var(--cai-tx) !important; border-radius: 16px; background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 20px) !important; border: 2px solid #8b4513; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:76, name:'Chalkboard', cat:'8. Materials & Textures', font:'Comic Neue', colorLock:true, aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: #2f4f4f !important; color: var(--cai-tx) !important; border-radius: 8px; border: 4px solid #8b4513; box-shadow: inset 0 0 10px rgba(0,0,0,0.5); } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:77, name:'Notepad Paper', cat:'8. Materials & Textures', font:'Onest', colorLock:true, aiTx:'#000000', uTx:'#000000', css:'.cai-msg { background: #fffacd !important; color: var(--cai-tx) !important; border-radius: 4px; background-image: linear-gradient(#87ceeb 1px, transparent 1px) !important; background-size: 100% 20px !important; line-height: 20px; padding-top: 14px; box-shadow: 2px 2px 5px rgba(0,0,0,0.1); border-left: 2px solid #ff7f50; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:78, name:'Brown Leather', cat:'8. Materials & Textures', font:'Onest', aiBg:'#8b4513', aiTx:'#ffffff', uBg:'#8b4513', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 12px; border: 2px dashed #deb887; box-shadow: 0 0 0 4px var(--cai-bg); margin: 4px; }' },
{ id:79, name:'Blue Denim', cat:'8. Materials & Textures', font:'Onest', aiBg:'#1e3a8a', aiTx:'#ffffff', uBg:'#1e3a8a', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 12px; border: 2px dashed #f59e0b; box-shadow: 0 0 0 4px var(--cai-bg); margin: 4px; }' },
{ id:80, name:'Holographic Foil', cat:'8. Materials & Textures', font:'Onest', colorLock:true, aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3) !important; background-size: 400% 400% !important; animation: caiHolo 5s ease infinite; color: var(--cai-tx) !important; border-radius: 16px; font-weight: bold; text-shadow: 1px 1px 2px #000; } .cai-msg * { color: var(--cai-tx) !important; }' },
// ── CAT 9: SCI-FI & RETRO
{ id:81, name:'8-Bit Box', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#000000', aiTx:'#ffffff', uBg:'#000000', uTx:'#ffffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); font-size: 18px; border: 4px solid #fff; box-shadow: inset -4px -4px 0 #888, inset 4px 4px 0 #ccc; border-radius: 0; }' },
{ id:82, name:'Cyberpunk Polygon', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#fdf100', aiTx:'#000000', uBg:'#fdf100', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); font-weight: bold; clip-path: polygon(0 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%); border-left: 4px solid #00f0ff; }' },
{ id:83, name:'Vaporwave', cat:'9. Sci-Fi & Retro', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: linear-gradient(180deg, #ff71ce 0%, #01cdfe 100%) !important; color: var(--cai-tx) !important; border-radius: 0; border: 2px solid #05ffa1; text-shadow: 2px 2px #b967ff; font-family: "Times New Roman", serif !important; } .cai-msg * { color: var(--cai-tx) !important; font-family: "Times New Roman", serif !important; }' },
{ id:84, name:'Terminal Green', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#000000', aiTx:'#00ff00', uBg:'#000000', uTx:'#00ff00', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); font-size: 18px; border-radius: 4px; border: 1px solid #0f0; }' },
{ id:85, name:'Retro Arcade', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#111827', aiTx:'#fde047', uBg:'#111827', uTx:'#fde047', spName:'Bóng', spDef:'#ec4899', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); font-size: 18px; border: 2px solid var(--cai-sp); box-shadow: 4px 4px 0 var(--cai-sp) !important; border-radius: 8px; text-transform: uppercase; }' },
{ id:86, name:'Matrix Code', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#000000', aiTx:'#00ff00', uBg:'#000000', uTx:'#00ff00', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); overflow: hidden; position: relative; border-radius: 8px; } .cai-msg::before { content:"1010101010"; position:absolute; top:0; left:0; color:rgba(0,255,0,0.1); font-size:40px; word-wrap:break-word; line-height:0.8; z-index:0; } .cai-msg * { position:relative; z-index:1; }' },
{ id:87, name:'CRT Scanlines', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#222222', aiTx:'#88ff88', uBg:'#222222', uTx:'#88ff88', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); font-size:18px; border-radius: 10px; background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.2) 2px, rgba(0,0,0,0.2) 4px); box-shadow: inset 0 0 10px #000; }' },
{ id:88, name:'Neon Magenta', cat:'9. Sci-Fi & Retro', font:'Onest', aiBg:'#111111', aiTx:'#ffffff', uBg:'#111111', uTx:'#ffffff', spName:'Neon', spDef:'#ff00ff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 2px solid var(--cai-sp); box-shadow: 0 0 10px var(--cai-sp), inset 0 0 10px var(--cai-sp) !important; text-shadow: 0 0 5px var(--cai-sp); }' },
{ id:89, name:'Neon Cyan', cat:'9. Sci-Fi & Retro', font:'Onest', aiBg:'#111111', aiTx:'#ffffff', uBg:'#111111', uTx:'#ffffff', spName:'Neon', spDef:'#00ffff', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 2px solid var(--cai-sp); box-shadow: 0 0 10px var(--cai-sp), inset 0 0 10px var(--cai-sp) !important; text-shadow: 0 0 5px var(--cai-sp); }' },
{ id:90, name:'Windows 95 Classic', cat:'9. Sci-Fi & Retro', font:'VT323', aiBg:'#c0c0c0', aiTx:'#000000', uBg:'#c0c0c0', uTx:'#000000', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 0; border-top: 2px solid #fff; border-left: 2px solid #fff; border-right: 2px solid #888; border-bottom: 2px solid #888; font-size: 18px; }' },
// ── CAT 10: VIP & ANIMATIONS
{ id:91, name:'Cosmic Space', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#e2e8f0', uTx:'#e2e8f0', css:'.cai-msg { background: #0f172a !important; color: var(--cai-tx) !important; border-radius: 16px; background-image: radial-gradient(white, rgba(255,255,255,.2) 2px, transparent 4px), radial-gradient(white, rgba(255,255,255,.15) 1px, transparent 3px) !important; background-size: 30px 30px, 20px 20px !important; background-position: 0 0, 15px 15px !important; border: 1px solid #334155; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:92, name:'Aurora Borealis', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: linear-gradient(270deg, #10b981, #3b82f6, #8b5cf6) !important; background-size: 600% 600% !important; animation: caiAurora 6s ease infinite; color: var(--cai-tx) !important; border-radius: 16px; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:93, name:'Pulse Glow', cat:'10. VIP & Animations', font:'Onest', aiBg:'#c084fc', aiTx:'#ffffff', uBg:'#c084fc', uTx:'#ffffff', spName:'Glow', spDef:'#c084fc', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; animation: caiPulse 2s infinite; } @keyframes caiPulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--cai-sp) 70%, transparent); } 70% { box-shadow: 0 0 0 10px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }' },
{ id:94, name:'Shake on Hover', cat:'10. VIP & Animations', font:'Onest', aiBg:'#fcd34d', aiTx:'#78350f', uBg:'#fcd34d', uTx:'#78350f', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; transition: transform 0.2s; } .cai-msg:hover { animation: caiShake 0.5s cubic-bezier(.36,.07,.19,.97) both; } @keyframes caiShake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } }' },
{ id:95, name:'Rainbow Text', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'transparent', uTx:'transparent', css:'.cai-msg { background: #111 !important; border-radius: 16px; font-weight: bold; border: 1px solid #333; } .cai-msg p, .cai-msg span, .cai-msg div, .cai-msg textarea { background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet) !important; -webkit-background-clip: text !important; background-clip: text !important; -webkit-text-fill-color: transparent !important; color: transparent !important; }' },
{ id:96, name:'Color Shifting', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: #3b82f6 !important; color: var(--cai-tx) !important; border-radius: 16px; animation: caiColorShift 5s infinite alternate; } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:97, name:'Neon Pulse Border', cat:'10. VIP & Animations', font:'Onest', aiBg:'#000000', aiTx:'#ffffff', uBg:'#000000', uTx:'#ffffff', spName:'Pulse', spDef:'#a855f7', css:'.cai-msg { background: var(--cai-bg); color: var(--cai-tx); border-radius: 16px; border: 2px solid var(--cai-sp); box-shadow: 0 0 10px var(--cai-sp), inset 0 0 10px var(--cai-sp) !important; animation: caiSmoothNeon 2s ease-in-out infinite alternate; text-shadow: 0 0 5px #fff, 0 0 10px var(--cai-sp); } @keyframes caiSmoothNeon { 0% { box-shadow: 0 0 5px var(--cai-sp), inset 0 0 5px var(--cai-sp); border-color: var(--cai-sp); } 100% { box-shadow: 0 0 20px color-mix(in srgb, var(--cai-sp) 80%, white), 0 0 10px color-mix(in srgb, var(--cai-sp) 80%, white), inset 0 0 10px color-mix(in srgb, var(--cai-sp) 80%, white); border-color: color-mix(in srgb, var(--cai-sp) 80%, white); color: #fff; text-shadow: 0 0 8px #fff, 0 0 15px color-mix(in srgb, var(--cai-sp) 80%, white); } }' },
{ id:98, name:'Liquid Gold', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#ffffff', uTx:'#ffffff', css:'.cai-msg { background: linear-gradient(135deg, #f6d365 0%, #fda085 100%) !important; color: var(--cai-tx) !important; border-radius: 16px; position: relative; overflow: hidden; border: none; } .cai-msg::after { content:""; position:absolute; top:0; left:-100%; width:50%; height:100%; background: linear-gradient(to right, transparent, rgba(255,255,255,0.4), transparent); transform: skewX(-20deg); animation: caiShine 3s infinite; } @keyframes caiShine { 0% { left: -100%; } 20% { left: 200%; } 100% { left: 200%; } } .cai-msg * { color: var(--cai-tx) !important; }' },
{ id:99, name:'Party Confetti', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#333333', uTx:'#333333', css:'.cai-msg { background: #fff !important; color: var(--cai-tx) !important; border-radius: 16px; position: relative; overflow: hidden; } .cai-msg::before { content:""; position:absolute; top:0; left:0; right:0; bottom:0; background-image: radial-gradient(circle, #ff0000 2px, transparent 2px), radial-gradient(circle, #00ff00 2px, transparent 2px), radial-gradient(circle, #0000ff 2px, transparent 2px) !important; background-size: 20px 20px !important; background-position: 0 0, 10px 10px, 5px 15px !important; opacity: 0.3; z-index: 0; } .cai-msg * { color: var(--cai-tx) !important; position: relative; z-index: 1; }' },
{ id:100, name:'Ultimate Diamond VIP', cat:'10. VIP & Animations', colorLock:true, font:'Onest', aiTx:'#ffd700', uTx:'#ffd700', css:'.cai-msg { background: linear-gradient(45deg, #111, #333) !important; color: var(--cai-tx) !important; border-radius: 20px; border: 2px solid #ffd700; box-shadow: 0 0 15px #ffd700 !important; text-transform: uppercase; font-weight: 900; letter-spacing: 1px; position: relative; overflow: visible !important; } .cai-msg::before { content:"💎"; position:absolute; top:-15px; left:-10px; font-size:24px; animation: caiFloat 2s infinite; } .cai-msg::after { content:"👑"; position:absolute; bottom:-15px; right:-10px; font-size:24px; animation: caiFloat 2s infinite reverse; } @keyframes caiFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } .cai-msg * { color: var(--cai-tx) !important; }' },
// ── CAT 11: EMOJI BG (iOS Style)
{ id:101, name:'Peaches', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'🍑', font:'Onest', aiBg:'#fff5f5', aiTx:'#9a3412', uBg:'#fff5f5', uTx:'#9a3412', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #ffedd5; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:102, name:'Cherry Blossoms', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'🌸', font:'Onest', aiBg:'#fdf2f8', aiTx:'#831843', uBg:'#fdf2f8', uTx:'#831843', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #fce7f3; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:103, name:'Strawberries', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'🍓', font:'Onest', aiBg:'#fef2f2', aiTx:'#7f1d1d', uBg:'#fef2f2', uTx:'#7f1d1d', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #fee2e2; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:104, name:'Puppies', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'🐶', font:'Onest', aiBg:'#fffbeb', aiTx:'#78350f', uBg:'#fffbeb', uTx:'#78350f', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #fef3c7; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:105, name:'Sparkles', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'✨', font:'Onest', aiBg:'#fafafa', aiTx:'#111111', uBg:'#fafafa', uTx:'#111111', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #f3f4f6; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:106, name:'Avocados', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'🥑', font:'Onest', aiBg:'#f0fdf4', aiTx:'#14532d', uBg:'#f0fdf4', uTx:'#14532d', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #dcfce7; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:107, name:'Ghosts', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'👻', font:'Onest', aiBg:'#f8fafc', aiTx:'#334155', uBg:'#f8fafc', uTx:'#334155', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #e2e8f0; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:108, name:'Clouds', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'☁️', font:'Onest', aiBg:'#eff6ff', aiTx:'#1e3a8a', uBg:'#eff6ff', uTx:'#1e3a8a', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #dbeafe; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:109, name:'Money Stack', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'💸', font:'Onest', aiBg:'#ecfdf5', aiTx:'#064e3b', uBg:'#ecfdf5', uTx:'#064e3b', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #d1fae5; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
{ id:110, name:'Dark Skulls', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'💀', font:'Onest', aiBg:'#0f172a', aiTx:'#f8fafc', uBg:'#0f172a', uTx:'#f8fafc', css:'.cai-msg { color: var(--cai-tx); border-radius: 18px; border: 1px solid #334155; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }' },
];
function isChatPage() {
return /\/chat\//.test(location.pathname);
}
// ─── STORAGE ───
function getCharKey() {
const match = location.pathname.match(/\/chat\/([^\/]+)/);
return match ? 'cai_style_' + match[1] : 'cai_style_global';
}
function loadAllStyles() {
try { return JSON.parse(GM_getValue(STORAGE_KEY, '{}')); } catch { return {}; }
}
function saveStyle(data) {
const all = loadAllStyles();
all[getCharKey()] = data;
GM_setValue(STORAGE_KEY, JSON.stringify(all));
}
function loadStyle() {
return loadAllStyles()[getCharKey()] || null;
}
// ─── BASE KEYFRAMES & UTILS ───
function injectBaseCSS() {
if (document.getElementById('cai-base-css')) return;
const s = document.createElement('style');
s.id = 'cai-base-css';
// We dynamically inject BUBBLE_TARGET exclusions here to ensure generating bubbles aren't blacked out.
// We also separate Light and Dark Mode logic so Popups and Context menus remain readable in Light Mode.
s.textContent = `
@keyframes caiHolo{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
@keyframes caiAurora{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
@keyframes caiColorShift{0%{filter:hue-rotate(0deg)}100%{filter:hue-rotate(360deg)}}
body.cai-transparent-bg { background-color: transparent !important; }
body.cai-transparent-bg #root { background-color: transparent !important; }
#chat-header, #chat-header [class*="bg-"] { background-color: rgba(255, 255, 255, 0.15) !important; }
:is(html.dark, body.dark) #chat-header, :is(html.dark, body.dark) #chat-header [class*="bg-"] { background-color: rgba(24, 24, 27, 0.15) !important; }
aside [class*="bg-primary-foreground"] { background-color: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important; }
:is(html.dark, body.dark) aside [class*="bg-primary-foreground"] { background-color: rgba(24, 24, 27, 0.15) !important; }
#chat-details, div[role="dialog"][class*="w-96"][class*="h-dvh"] { background-color: rgba(255, 255, 255, 0.15) !important; backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important; }
:is(html.dark, body.dark) #chat-details, :is(html.dark, body.dark) div[role="dialog"][class*="w-96"][class*="h-dvh"] { background-color: rgba(24, 24, 27, 0.15) !important; }
.inset-0.z-50 { background: transparent !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
:is(aside, #chat-details, div[role="dialog"][class*="w-96"][class*="h-dvh"], [role="dialog"][class*="bg-popover"]) [class*="text-muted"],
:is(aside, #chat-details, div[role="dialog"][class*="w-96"][class*="h-dvh"], [role="dialog"][class*="bg-popover"]) [class*="text-secondary"] {
color: rgba(0, 0, 0, 0.75) !important;
}
:is(html.dark, body.dark) :is(aside, #chat-details, div[role="dialog"][class*="w-96"][class*="h-dvh"], [role="dialog"][class*="bg-popover"]) [class*="text-muted"],
:is(html.dark, body.dark) :is(aside, #chat-details, div[role="dialog"][class*="w-96"][class*="h-dvh"], [role="dialog"][class*="bg-popover"]) [class*="text-secondary"] {
color: rgba(255, 255, 255, 0.75) !important;
}
/* Base: Light Mode Translucent Backgrounds */
body.cai-transparent-bg [class*="bg-surface"]:not(${BUBBLE_TARGET}):not(.group\\/message *):not(.flex.grow:has(#chat-input-textarea)):not(#chat-input-textarea):not(button),
body.cai-transparent-bg [class*="bg-background"]:not(${BUBBLE_TARGET}):not(.group\\/message *):not(button),
body.cai-transparent-bg [role="dialog"][class*="bg-popover"],
body.cai-transparent-bg .bg-surface-elevated:not(${BUBBLE_TARGET}):not(.group\\/message *):not(.flex.grow:has(#chat-input-textarea)) {
background-color: rgba(255, 255, 255, 0.25) !important;
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
background-image: none !important;
border-color: rgba(0, 0, 0, 0.05) !important;
}
/* Adaptive Override: Dark Mode Translucent Backgrounds */
:is(html.dark, body.dark) body.cai-transparent-bg [class*="bg-surface"]:not(${BUBBLE_TARGET}):not(.group\\/message *):not(.flex.grow:has(#chat-input-textarea)):not(#chat-input-textarea):not(button),
:is(html.dark, body.dark) body.cai-transparent-bg [class*="bg-background"]:not(${BUBBLE_TARGET}):not(.group\\/message *):not(button),
:is(html.dark, body.dark) body.cai-transparent-bg [role="dialog"][class*="bg-popover"],
:is(html.dark, body.dark) body.cai-transparent-bg .bg-surface-elevated:not(${BUBBLE_TARGET}):not(.group\\/message *):not(.flex.grow:has(#chat-input-textarea)) {
background-color: rgba(18, 18, 24, 0.25) !important;
border-color: rgba(255, 255, 255, 0.05) !important;
}
`;
document.head.appendChild(s);
}
// ─── EMOJI WALLPAPER GENERATOR ───
function buildEmojiCSS(cfg, def) {
const emojis = (cfg.customEmoji && cfg.customEmoji.length) ? cfg.customEmoji : (def.defaultEmoji ? [def.defaultEmoji] : ['⭐']);
const angle = cfg.emojiAngle || 0;
const alpha = cfg.bgAlpha !== undefined ? cfg.bgAlpha : 0.85;
function hexToRgba(hex, al) {
if (!hex || !hex.startsWith('#')) return `rgba(255,255,255,${al})`;
const r = parseInt(hex.slice(1,3)||'ff',16), g = parseInt(hex.slice(3,5)||'ff',16), b = parseInt(hex.slice(5,7)||'ff',16);
return `rgba(${r},${g},${b},${al})`;
}
const n = emojis.length, w = 50, h = 46, fontSize = 22;
const pw = Math.max(w * n, w), ph = h * 2;
let texts = '';
for (let i = -2; i <= n + 1; i++) {
const idx = ((i % n) + n) % n, emoji = emojis[idx];
texts += `<text x="${i * w + w/2}" y="${h/2}" font-size="${fontSize}" text-anchor="middle" dominant-baseline="central">${emoji}</text>`;
texts += `<text x="${i * w + w/2 + w/2}" y="${h + h/2}" font-size="${fontSize}" text-anchor="middle" dominant-baseline="central">${emoji}</text>`;
}
const svgStr = `<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><defs><pattern id='p' width='${pw}' height='${ph}' patternUnits='userSpaceOnUse' patternTransform='rotate(${angle})'>${texts}</pattern></defs><rect width='100%' height='100%' fill='url(#p)' /></svg>`;
const enc = encodeURIComponent(svgStr).replace(/'/g, "%27").replace(/"/g, "%22");
const aiRgba = hexToRgba(cfg.aiColor || def.aiBg, alpha);
const uRgba = hexToRgba(cfg.userColor || def.uBg, alpha);
return `.cai-ai { background: linear-gradient(${aiRgba}, ${aiRgba}), url("data:image/svg+xml;charset=utf-8,${enc}") !important; } .cai-user { background: linear-gradient(${uRgba}, ${uRgba}), url("data:image/svg+xml;charset=utf-8,${enc}") !important; }`;
}
// ─── GHOST BLUR (backdrop-filter workaround for swiper-3d) ───
let ghostBlurRAF = null;
let ghostBlurBurstUntil = 0;
const GHOST_BLUR_BURST_MS = 500;
function ghostOuterShadowValue(style) {
if (!style.shadowEnabled) return 'none';
const color = style.shadowMode === 'individual' ? (style.shadowColorAi || '#000000') : (style.shadowColor || '#000000');
const spread = style.shadowSpread !== undefined ? style.shadowSpread : 0;
const blur = style.shadowBlur !== undefined ? style.shadowBlur : 10;
const offX = style.shadowOffsetX !== undefined ? style.shadowOffsetX : 0;
const offY = style.shadowOffsetY !== undefined ? style.shadowOffsetY : 4;
return `${offX}px ${offY}px ${blur}px ${spread}px ${color}`;
}
function syncGhostBlur() {
const style = loadStyle();
const blurOn = !!(style && style.blurEnabled);
const shadowOn = !!(style && style.shadowEnabled);
if (!style || (!blurOn && !shadowOn) || !isChatPage()) {
document.querySelectorAll('.cai-ghost-blur').forEach(g => g.remove());
return;
}
const amount = style.blurAmount !== undefined ? style.blurAmount : 8;
const outerShadow = ghostOuterShadowValue(style);
const targets = Array.from(document.querySelectorAll(`.swiper-3d .swiper-slide-active :is(${BUBBLE_TARGET})`));
document.querySelectorAll('.cai-ghost-blur').forEach(g => {
if (!targets.includes(g._caiTarget)) g.remove();
});
targets.forEach(el => {
const rect = el.getBoundingClientRect();
if (!rect.width || !rect.height) return;
let ghost = el._caiGhost;
if (!ghost || !ghost.isConnected) {
ghost = document.createElement('div');
ghost.className = 'cai-ghost-blur';
ghost.style.position = 'fixed';
ghost.style.pointerEvents = 'none';
ghost.style.zIndex = '0';
ghost.style.background = 'transparent';
document.body.insertBefore(ghost, document.body.firstChild);
ghost._caiTarget = el;
el._caiGhost = ghost;
}
ghost.style.left = rect.left + 'px';
ghost.style.top = rect.top + 'px';
ghost.style.width = rect.width + 'px';
ghost.style.height = rect.height + 'px';
ghost.style.borderRadius = getComputedStyle(el).borderRadius;
ghost.style.backdropFilter = blurOn ? `blur(${amount}px)` : 'none';
ghost.style.webkitBackdropFilter = blurOn ? `blur(${amount}px)` : 'none';
ghost.style.boxShadow = outerShadow;
});
}
function ghostBlurLoop() {
syncGhostBlur();
if (Date.now() < ghostBlurBurstUntil) {
ghostBlurRAF = requestAnimationFrame(ghostBlurLoop);
} else {
ghostBlurRAF = null;
}
}
let lastGhostBurstCall = 0;
function burstGhostBlurSync(duration) {
const style = loadStyle();
if (!style || (!style.blurEnabled && !style.shadowEnabled) || !isChatPage()) return;
const now = Date.now();
if (now - lastGhostBurstCall < 150) return;
lastGhostBurstCall = now;
ghostBlurBurstUntil = now + (duration || GHOST_BLUR_BURST_MS);
if (!ghostBlurRAF) ghostBlurLoop();
}
function stopGhostBlurLoop() {
if (ghostBlurRAF) cancelAnimationFrame(ghostBlurRAF);
ghostBlurRAF = null;
ghostBlurBurstUntil = 0;
document.querySelectorAll('.cai-ghost-blur').forEach(g => g.remove());
}
let ghostTriggerObserver = null;
let ghostTriggerObservedContainer = null;
let ghostScrollListenerAdded = false;
function setupGhostBlurTriggerObserver() {
const container = document.querySelector('#chat-messages');
if (!container) return;
if (container === ghostTriggerObservedContainer) return;
if (ghostTriggerObserver) ghostTriggerObserver.disconnect();
ghostTriggerObserver = new MutationObserver(() => burstGhostBlurSync());
ghostTriggerObserver.observe(container, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
ghostTriggerObservedContainer = container;
if (!ghostScrollListenerAdded) {
document.addEventListener('scroll', () => burstGhostBlurSync(), { capture: true, passive: true });
ghostScrollListenerAdded = true;
}
burstGhostBlurSync();
}
function applyStyle(cfg) {
const existing = document.getElementById('cai-bubble-style');
if (existing) existing.remove();
const bgStyle = document.getElementById('cai-bg-style');
if (bgStyle) bgStyle.remove();
document.body.classList.remove('cai-transparent-bg');
if (!cfg) { stopGhostBlurLoop(); return; }
const def = BUBBLES.find(b => b.id === cfg.bubbleId);
if (!def) return;
const font = cfg.font || def.font || '';
const fontParam = font ? (encodeURIComponent(font) + ':wght@400;500;600;700') : '';
const fontFaceCSS = font ? `font-family: '${font}', sans-serif !important;` : '';
const fontSize = clampFontSize(cfg.fontSize);
const lineHeight = cfg.lineHeight !== undefined ? clampLineHeight(cfg.lineHeight) : DEFAULT_LINE_HEIGHT;
const letterSpacing = cfg.letterSpacing !== undefined ? clampLetterSpacing(cfg.letterSpacing) : DEFAULT_LETTER_SPACING;
const textAlign = clampTextAlign(cfg.textAlign);
const fontScope = cfg.fontScope === 'individual' ? 'individual' : 'dual';
const fontAi = cfg.fontAi || def.font || '';
const fontUser = cfg.fontUser || def.font || '';
const fontAiFaceCSS = fontAi ? `font-family: '${fontAi}', sans-serif !important;` : '';
const fontUserFaceCSS = fontUser ? `font-family: '${fontUser}', sans-serif !important;` : '';
const fontAiParam = fontAi ? (encodeURIComponent(fontAi) + ':wght@400;500;600;700') : '';
const fontUserParam = fontUser ? (encodeURIComponent(fontUser) + ':wght@400;500;600;700') : '';
const fontSizeAi = clampFontSize(cfg.fontSizeAi);
const fontSizeUser = clampFontSize(cfg.fontSizeUser);
const lineHeightAi = cfg.lineHeightAi !== undefined ? clampLineHeight(cfg.lineHeightAi) : DEFAULT_LINE_HEIGHT;
const lineHeightUser = cfg.lineHeightUser !== undefined ? clampLineHeight(cfg.lineHeightUser) : DEFAULT_LINE_HEIGHT;
const letterSpacingAi = cfg.letterSpacingAi !== undefined ? clampLetterSpacing(cfg.letterSpacingAi) : DEFAULT_LETTER_SPACING;
const letterSpacingUser = cfg.letterSpacingUser !== undefined ? clampLetterSpacing(cfg.letterSpacingUser) : DEFAULT_LETTER_SPACING;
const textAlignAi = clampTextAlign(cfg.textAlignAi);
const textAlignUser = clampTextAlign(cfg.textAlignUser);
const inputFontFaceCSS = fontScope === 'individual' ? fontUserFaceCSS : fontFaceCSS;
const italicBoldEnabled = !!cfg.italicBoldEnabled;
const italicColor = cfg.italicColor || '#ffffff';
const boldColor = cfg.boldColor || '#ffffff';
const italicBoldEnabledAi = !!cfg.italicBoldEnabledAi;
const italicColorAi = cfg.italicColorAi || '#ffffff';
const boldColorAi = cfg.boldColorAi || '#ffffff';
const italicBoldEnabledUser = !!cfg.italicBoldEnabledUser;
const italicColorUser = cfg.italicColorUser || '#ffffff';
const boldColorUser = cfg.boldColorUser || '#ffffff';
const blurEnabled = !!cfg.blurEnabled;
const blurAmount = cfg.blurAmount !== undefined ? cfg.blurAmount : 8;
const msgSpacing = cfg.msgSpacing !== undefined ? clampMsgSpacing(cfg.msgSpacing) : DEFAULT_MSG_SPACING;
const borderEnabled = !!cfg.borderEnabled;
const borderWidth = cfg.borderWidth !== undefined ? clampBorderWidth(cfg.borderWidth) : DEFAULT_BORDER_WIDTH;
const borderMode = cfg.borderMode === 'individual' ? 'individual' : 'dual';
const borderColor = cfg.borderColor || '#ffffff';
const borderColorAi = cfg.borderColorAi || '#ffffff';
const borderColorUser = cfg.borderColorUser || '#ffffff';
const borderWidthAi = cfg.borderWidthAi !== undefined ? clampBorderWidth(cfg.borderWidthAi) : DEFAULT_BORDER_WIDTH;
const borderWidthUser = cfg.borderWidthUser !== undefined ? clampBorderWidth(cfg.borderWidthUser) : DEFAULT_BORDER_WIDTH;
const shadowEnabled = !!cfg.shadowEnabled;
const shadowMode = cfg.shadowMode === 'individual' ? 'individual' : 'dual';
const shadowColor = cfg.shadowColor || '#000000';
const shadowColorAi = cfg.shadowColorAi || '#000000';
const shadowColorUser = cfg.shadowColorUser || '#000000';
const shadowSpread = cfg.shadowSpread !== undefined ? clampShadowSpread(cfg.shadowSpread) : DEFAULT_SHADOW_SPREAD;
const shadowBlur = cfg.shadowBlur !== undefined ? clampShadowBlur(cfg.shadowBlur) : DEFAULT_SHADOW_BLUR;
const shadowOffsetX = cfg.shadowOffsetX !== undefined ? clampShadowOffset(cfg.shadowOffsetX) : DEFAULT_SHADOW_OFFSET_X;
const shadowOffsetY = cfg.shadowOffsetY !== undefined ? clampShadowOffset(cfg.shadowOffsetY) : DEFAULT_SHADOW_OFFSET_Y;
const innerShadowEnabled = !!cfg.innerShadowEnabled;
const innerShadowMode = cfg.innerShadowMode === 'individual' ? 'individual' : 'dual';
const innerShadowColor = cfg.innerShadowColor || '#000000';
const innerShadowColorAi = cfg.innerShadowColorAi || '#000000';
const innerShadowColorUser = cfg.innerShadowColorUser || '#000000';
const innerShadowSpread = cfg.innerShadowSpread !== undefined ? clampInnerShadowSpread(cfg.innerShadowSpread) : DEFAULT_INNER_SHADOW_SPREAD;
const innerShadowBlur = cfg.innerShadowBlur !== undefined ? clampInnerShadowBlur(cfg.innerShadowBlur) : DEFAULT_INNER_SHADOW_BLUR;
const innerShadowOffsetX = cfg.innerShadowOffsetX !== undefined ? clampInnerShadowOffset(cfg.innerShadowOffsetX) : DEFAULT_INNER_SHADOW_OFFSET_X;
const innerShadowOffsetY = cfg.innerShadowOffsetY !== undefined ? clampInnerShadowOffset(cfg.innerShadowOffsetY) : DEFAULT_INNER_SHADOW_OFFSET_Y;
const cornersEnabled = !!cfg.cornersEnabled;
const cornersScope = cfg.cornersScope === 'individual' ? 'individual' : 'dual';
const cornersMode = cfg.cornersMode === 'custom' ? 'custom' : 'uniform';
const cornerRadius = cfg.cornerRadius !== undefined ? clampCornerRadius(cfg.cornerRadius) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTL = cfg.cornerRadiusTL !== undefined ? clampCornerRadius(cfg.cornerRadiusTL) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTR = cfg.cornerRadiusTR !== undefined ? clampCornerRadius(cfg.cornerRadiusTR) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBL = cfg.cornerRadiusBL !== undefined ? clampCornerRadius(cfg.cornerRadiusBL) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBR = cfg.cornerRadiusBR !== undefined ? clampCornerRadius(cfg.cornerRadiusBR) : DEFAULT_CORNER_RADIUS;
const cornersModeAi = cfg.cornersModeAi === 'custom' ? 'custom' : 'uniform';
const cornersModeUser = cfg.cornersModeUser === 'custom' ? 'custom' : 'uniform';
const cornerRadiusAi = cfg.cornerRadiusAi !== undefined ? clampCornerRadius(cfg.cornerRadiusAi) : DEFAULT_CORNER_RADIUS;
const cornerRadiusUser = cfg.cornerRadiusUser !== undefined ? clampCornerRadius(cfg.cornerRadiusUser) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTLAi = cfg.cornerRadiusTLAi !== undefined ? clampCornerRadius(cfg.cornerRadiusTLAi) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTRAi = cfg.cornerRadiusTRAi !== undefined ? clampCornerRadius(cfg.cornerRadiusTRAi) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBLAi = cfg.cornerRadiusBLAi !== undefined ? clampCornerRadius(cfg.cornerRadiusBLAi) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBRAi = cfg.cornerRadiusBRAi !== undefined ? clampCornerRadius(cfg.cornerRadiusBRAi) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTLUser = cfg.cornerRadiusTLUser !== undefined ? clampCornerRadius(cfg.cornerRadiusTLUser) : DEFAULT_CORNER_RADIUS;
const cornerRadiusTRUser = cfg.cornerRadiusTRUser !== undefined ? clampCornerRadius(cfg.cornerRadiusTRUser) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBLUser = cfg.cornerRadiusBLUser !== undefined ? clampCornerRadius(cfg.cornerRadiusBLUser) : DEFAULT_CORNER_RADIUS;
const cornerRadiusBRUser = cfg.cornerRadiusBRUser !== undefined ? clampCornerRadius(cfg.cornerRadiusBRUser) : DEFAULT_CORNER_RADIUS;
const bubbleStyle = cfg.bubbleStyle === 'dense' ? 'dense' : 'classic';
const bubbleAlign = ['left','right','reverse'].includes(cfg.bubbleAlign) ? cfg.bubbleAlign : 'default';
// Colors
const aiBg = cfg.aiColor || def.aiBg || '#ffffff';
const aiTx = cfg.aiTextColor || def.aiTx || '#000000';
const uBg = cfg.userColor || def.uBg || '#ffffff';
const uTx = cfg.userTextColor || def.uTx || '#000000';
const sp = cfg.spColor || def.spDef || '#000000';
let compiledCSS = `
${fontScope === 'individual'
? `${fontAi ? `@import url('https://fonts.googleapis.com/css2?family=${fontAiParam}&display=swap');` : ''}${fontUser ? `@import url('https://fonts.googleapis.com/css2?family=${fontUserParam}&display=swap');` : ''}`
: (font ? `@import url('https://fonts.googleapis.com/css2?family=${fontParam}&display=swap');` : '')}
${selAIWrapper} { --cai-bg: ${aiBg}; --cai-tx: ${aiTx}; --cai-sp: ${sp}; ${fontScope === 'individual' ? fontAiFaceCSS : fontFaceCSS} transition: transform 0.2s ease; }
${selThemeTargets} { --cai-bg: ${uBg}; --cai-tx: ${uTx}; --cai-sp: ${sp}; }
${selAIWrapper} *, ${selAIWrapper} p, ${selAIWrapper} span, ${selAIWrapper} div, ${selAIWrapper} .prose { color: var(--cai-tx) !important; ${fontScope === 'individual' ? fontAiFaceCSS : fontFaceCSS} }
${fontScope === 'individual' ? `
:is(${selUser}, ${selInputBox}) { ${fontUserFaceCSS} }
${selUser} *, ${selUser} p, ${selUser} span, ${selUser} div, ${selUser} .prose { ${fontUserFaceCSS} }
` : ''}
/* Kill c.ai's native opacity-85 class so solid colors actually look solid */
${selMsg} { opacity: 1 !important; }
/* Base fallback for the Input Box (So it always defaults to round if a theme omits border-radius) */
${selInputBox} {
border-radius: 24px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
border: none;
}
`;
// ── BUBBLE BLUR (Frosted-glass effect: only visible when the bubble is transparent) ──
if (blurEnabled) {
compiledCSS += `
${selAIWrapper}, ${selUser} {
backdrop-filter: blur(${blurAmount}px) !important;
-webkit-backdrop-filter: blur(${blurAmount}px) !important;
}
.swiper-3d :is(${selAIWrapper}, ${selUser}) {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
${selInputBox} {
backdrop-filter: blur(${blurAmount}px) !important;
-webkit-backdrop-filter: blur(${blurAmount}px) !important;
}
`;
}
// ── BUBBLE BORDER ──
if (borderEnabled) {
if (borderMode === 'individual') {
compiledCSS += `
${selAIWrapper} { border: ${borderWidthAi}px solid ${borderColorAi} !important; }
:is(${selUser}, ${selInputBox}) { border: ${borderWidthUser}px solid ${borderColorUser} !important; }
`;
} else {
compiledCSS += `
:is(${selMsg}, ${selInputBox}) { border: ${borderWidth}px solid ${borderColor} !important; }
`;
}
}
// ── BUBBLE SHADOW (outer + inner) ──
if (shadowEnabled || innerShadowEnabled) {
const buildShadowList = (role) => {
const parts = [];
if (shadowEnabled) {
const c = shadowMode === 'individual' ? (role === 'ai' ? shadowColorAi : shadowColorUser) : shadowColor;
parts.push(`${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px ${shadowSpread}px ${c}`);
}
if (innerShadowEnabled) {
const c = innerShadowMode === 'individual' ? (role === 'ai' ? innerShadowColorAi : innerShadowColorUser) : innerShadowColor;
parts.push(`inset ${innerShadowOffsetX}px ${innerShadowOffsetY}px ${innerShadowBlur}px ${innerShadowSpread}px ${c}`);
}
return parts.join(', ');
};
const needsIndividual = (shadowEnabled && shadowMode === 'individual') || (innerShadowEnabled && innerShadowMode === 'individual');
if (needsIndividual) {
compiledCSS += `
${selAIWrapper} { box-shadow: ${buildShadowList('ai')} !important; }
:is(${selUser}, ${selInputBox}) { box-shadow: ${buildShadowList('user')} !important; }
`;
} else {
compiledCSS += `
${selMsg} { box-shadow: ${buildShadowList('ai')} !important; }
${selInputBox} { box-shadow: ${buildShadowList('ai')} !important; }
`;
}
}
// ── FIX: suppress outer shadow on active-slide bot bubble (drawn on ghost instead); keep inner shadow only ──
if (shadowEnabled) {
const innerOnly = innerShadowEnabled
? (() => {
const c = innerShadowMode === 'individual' ? innerShadowColorAi : innerShadowColor;
return `inset ${innerShadowOffsetX}px ${innerShadowOffsetY}px ${innerShadowBlur}px ${innerShadowSpread}px ${c}`;
})()
: 'none';
compiledCSS += `
.swiper-3d .swiper-slide-active ${selAIWrapper} {
box-shadow: ${innerOnly} !important;
}
`;
}
if (blurEnabled || shadowEnabled) {
burstGhostBlurSync();
} else {
stopGhostBlurLoop();
}
// Apply text colors via CSS variables
if (cfg.bubbleId !== 95) {
compiledCSS += `
${selAIWrapper}, ${selAIWrapper} *, ${selAIWrapper} p, ${selAIWrapper} span, ${selAIWrapper} div, ${selAIWrapper} .prose { color: ${aiTx} !important; }
${selUser}, ${selUser} *, ${selUser} p, ${selUser} span, ${selUser} div, ${selUser} .prose { color: ${uTx} !important; }
${selInputBox}, ${selInputBox} *, ${selOuterBtns}, ${selOuterBtns} * { color: ${uTx} !important; }
`;
}
// ── ITALIC/BOLD CUSTOM COLOR ──
if (fontScope === 'individual') {
if (italicBoldEnabledAi) {
compiledCSS += `${selAIWrapper} em, ${selAIWrapper} em * { color: ${italicColorAi} !important; } ${selAIWrapper} strong, ${selAIWrapper} strong * { color: ${boldColorAi} !important; }`;
}
if (italicBoldEnabledUser) {
compiledCSS += `${selUser} em, ${selUser} em * { color: ${italicColorUser} !important; } ${selUser} strong, ${selUser} strong * { color: ${boldColorUser} !important; }`;
}
} else if (italicBoldEnabled) {
compiledCSS += `:is(${selAIWrapper}, ${selUser}) em, :is(${selAIWrapper}, ${selUser}) em * { color: ${italicColor} !important; } :is(${selAIWrapper}, ${selUser}) strong, :is(${selAIWrapper}, ${selUser}) strong * { color: ${boldColor} !important; }`;
}
// ── 1. APPLY THEME CSS OVERRIDES (This gives the Chat Bar its theme shape and accessories)
let shared = (def.css || '')
.replace(/\.cai-msg/g, `:is(${selMsg}, ${selInputBox}, ${selOuterBtns})`)
.replace(/\.cai-ai/g, `:is(${selAIWrapper})`)
.replace(/\.cai-user/g, `:is(${selThemeTargets})`);
compiledCSS += shared;
// ── 2. APPLY EMOJI BACKGROUND CSS
if (def.emojiCat) {
let eCSS = buildEmojiCSS(cfg, def).replace(/\.cai-ai/g, `:is(${selAIWrapper})`).replace(/\.cai-user/g, `:is(${selThemeTargets})`);
compiledCSS += eCSS;
}
// ── 3. MANDATORY STRUCTURAL FIXES (These go LAST so they override any glitch-causing theme styles) ──
if (fontScope === 'individual') {
compiledCSS += `
#chat-messages ${selAIWrapper}, #chat-messages ${selAIWrapper} *, #chat-messages ${selAIWrapper} p, #chat-messages ${selAIWrapper} span, #chat-messages ${selAIWrapper} div {
font-size: ${fontSizeAi}px !important;
line-height: ${lineHeightAi} !important;
letter-spacing: ${letterSpacingAi}px !important;
text-align: ${textAlignAi} !important;
}
#chat-messages ${selUser}, #chat-messages ${selUser} *, #chat-messages ${selUser} p, #chat-messages ${selUser} span, #chat-messages ${selUser} div {
font-size: ${fontSizeUser}px !important;
line-height: ${lineHeightUser} !important;
letter-spacing: ${letterSpacingUser}px !important;
text-align: ${textAlignUser} !important;
}
`;
} else {
compiledCSS += `
#chat-messages ${selAIWrapper}, #chat-messages ${selAIWrapper} *, #chat-messages ${selAIWrapper} p, #chat-messages ${selAIWrapper} span, #chat-messages ${selAIWrapper} div,
#chat-messages ${selUser}, #chat-messages ${selUser} *, #chat-messages ${selUser} p, #chat-messages ${selUser} span, #chat-messages ${selUser} div {
font-size: ${fontSize}px !important;
line-height: ${lineHeight} !important;
letter-spacing: ${letterSpacing}px !important;
text-align: ${textAlign} !important;
}
`;
}
// ── MESSAGE SPACING ──
compiledCSS += `
#chat-messages ${selMsg} {
margin-bottom: ${msgSpacing}px !important;
}
`;
// ── FIX: own compositing layer + open overflow for active-slide bubble (avoids border/shadow clipping inside 3D swiper) ──
// :not(:hover) excludes it during hover so hover-only animations (e.g. Shake on Hover) keep control of `transform`.
compiledCSS += `
.swiper-3d .swiper-slide-active {
overflow: visible !important;
}
.swiper-3d .swiper-slide-active :is(${selAIWrapper}, ${selUser}):not(:hover) {
transform: translateZ(0) !important;
-webkit-transform: translateZ(0) !important;
}
`;
// ── BUBBLE CORNERS ──
if (cornersEnabled && cornersScope === 'dual') {
compiledCSS += cornersMode === 'uniform'
? `#chat-messages ${selMsg} { border-radius: ${cornerRadius}px !important; }`
: `#chat-messages ${selMsg} { border-radius: ${cornerRadiusTL}px ${cornerRadiusTR}px ${cornerRadiusBR}px ${cornerRadiusBL}px !important; }`;
} else if (cornersEnabled && cornersScope === 'individual') {
compiledCSS += cornersModeAi === 'uniform'
? `#chat-messages ${selAIWrapper} { border-radius: ${cornerRadiusAi}px !important; }`
: `#chat-messages ${selAIWrapper} { border-radius: ${cornerRadiusTLAi}px ${cornerRadiusTRAi}px ${cornerRadiusBRAi}px ${cornerRadiusBLAi}px !important; }`;
compiledCSS += cornersModeUser === 'uniform'
? `#chat-messages ${selUser} { border-radius: ${cornerRadiusUser}px !important; }`
: `#chat-messages ${selUser} { border-radius: ${cornerRadiusTLUser}px ${cornerRadiusTRUser}px ${cornerRadiusBRUser}px ${cornerRadiusBLUser}px !important; }`;
}
// ── BUBBLE STYLE (Classic / Dense) ──
if (bubbleStyle === 'dense') {
const mapJustify = a => a === 'center' ? 'center' : (a === 'right' ? 'flex-end' : 'flex-start');
const resolvedAlignAi = fontScope === 'individual' ? textAlignAi : textAlign;
const resolvedAlignUser = fontScope === 'individual' ? textAlignUser : textAlign;
compiledCSS += `
${selMsg} {
max-width: 100% !important;
width: 100% !important;
}
${selAIWrapper} { justify-content: ${mapJustify(resolvedAlignAi)} !important; }
${selUser} { justify-content: ${mapJustify(resolvedAlignUser)} !important; }
`;
}
// ── ALIGNMENT (Default / Left / Right) ──
if (bubbleAlign === 'left') {
compiledCSS += `
${selRow} {
flex-direction: row !important;
justify-content: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selRow} > div:last-child {
align-items: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
`;
} else if (bubbleAlign === 'right') {
compiledCSS += `
${selRow} {
flex-direction: row-reverse !important;
justify-content: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selRow} > div:last-child {
align-items: flex-end !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selActionsBar} {
left: auto !important;
right: 0 !important;
margin-left: 0 !important;
}
`;
} else if (bubbleAlign === 'reverse') {
compiledCSS += `
${selRowAI} {
flex-direction: row-reverse !important;
justify-content: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selRowAI} > div:last-child {
align-items: flex-end !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selRowUser} {
flex-direction: row !important;
justify-content: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selRowUser} > div:last-child {
align-items: flex-start !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
${selActionsBar} {
left: auto !important;
right: 0 !important;
margin-left: 0 !important;
}
`;
}
compiledCSS += `
/* Fix Input Box (chat bar) layout shifts */
${selInputBox} {
${inputFontFaceCSS}
margin: 0 !important; /* Extremely important: stops Fluffy Cloud/others from pushing the bar down */
position: relative !important; /* ensures theme clouds attach to it properly */
clip-path: none !important; /* prevents input text being sliced off */
transition: all 0.2s ease;
}
#chat-input-textarea {
background-color: transparent !important;
background: transparent !important;
color: var(--cai-tx) !important;
${inputFontFaceCSS}
box-shadow: none !important;
}
#chat-input-textarea::placeholder {
color: var(--cai-tx) !important;
opacity: 0.6 !important;
}
/* Keep Send Button Clean and unaffected by theme borders (solo estado "enviar") */
${selInputBox} button.bg-primary:not(:has(svg rect)) {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
${selInputBox} button.bg-primary:not(:has(svg rect)) svg,
${selInputBox} button.bg-primary:not(:has(svg rect)) path {
color: var(--cai-tx) !important;
fill: var(--cai-tx) !important;
}
/* Restaurar círculo blanco nativo en estado "detener" (svg con <rect> = cuadrado) */
${selInputBox} button.bg-primary:has(svg rect) {
background-color: #ffffff !important;
box-shadow: none !important;
border: none !important;
}
${selInputBox} button.bg-primary:has(svg rect) svg,
${selInputBox} button.bg-primary:has(svg rect) rect,
${selInputBox} button.bg-primary:has(svg rect) path {
color: #000000 !important;
fill: #000000 !important;
}
/* Strictly Sanitize Outer Buttons (Voice/Call) */
/* This allows them to inherit background color/pattern but strips ALL layout glitches and special elements */
${selOuterBtns} {
border-radius: 50% !important; /* Force to be a circle */
border: 1px solid color-mix(in srgb, var(--cai-tx) 20%, transparent) !important;
aspect-ratio: 1 / 1 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 0 0 8px !important; /* Force margin to avoid jumping/misalignment */
clip-path: none !important;
mask-image: none !important;
-webkit-mask-image: none !important;
transform: none !important;
animation: none !important;
transition: transform 0.2s ease, opacity 0.2s ease !important;
width: 40px !important; /* Uniform size */
height: 40px !important; /* Uniform size */
flex-shrink: 0 !important;
overflow: hidden !important;
}
${selOuterBtns}:hover {
transform: scale(1.05) !important;
opacity: 0.9 !important;
}
${selOuterBtns} svg,
${selOuterBtns} path {
color: var(--cai-tx) !important;
fill: var(--cai-tx) !important;
}
/* Kill ALL Theme Pseudo Elements (Clouds, Crowns, Quotes, etc.) exactly on outer buttons */
${selOuterBtns}::before, ${selOuterBtns}::after {
content: none !important;
display: none !important;
background: none !important;
box-shadow: none !important;
border: none !important;
animation: none !important;
}
/* Ensure Flex container doesn't get messed up */
.w-full.flex.justify-center.items-center {
align-items: center !important;
}
`;
// ─── APPLY BORDER/SHADOW/BLUR TO OUTER BUTTONS (call & image upload) ───
if (borderEnabled) {
const outerBorderColor = borderMode === 'individual' ? borderColorUser : borderColor;
const outerBorderWidth = borderMode === 'individual' ? borderWidthUser : borderWidth;
compiledCSS += `${selOuterBtns} { border: ${outerBorderWidth}px solid ${outerBorderColor} !important; }`;
}
if (shadowEnabled || innerShadowEnabled) {
const outerBtnParts = [];
if (shadowEnabled) {
const outerShadowColor = shadowMode === 'individual' ? shadowColorUser : shadowColor;
outerBtnParts.push(`${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px ${shadowSpread}px ${outerShadowColor}`);
}
if (innerShadowEnabled) {
const innerShadowColorFinal = innerShadowMode === 'individual' ? innerShadowColorUser : innerShadowColor;
outerBtnParts.push(`inset ${innerShadowOffsetX}px ${innerShadowOffsetY}px ${innerShadowBlur}px ${innerShadowSpread}px ${innerShadowColorFinal}`);
}
compiledCSS += `${selOuterBtns} { box-shadow: ${outerBtnParts.join(', ')} !important; }`;
}
if (blurEnabled) {
compiledCSS += `${selOuterBtns} { backdrop-filter: blur(${blurAmount}px) !important; -webkit-backdrop-filter: blur(${blurAmount}px) !important; }`;
}
if (cfg.hideEditedMarks) {
const FORCE_HIDE_EDIT_BORDER_IDS = [3, 8, 29, 30, 38, 68, 69, 77, 82];
const themeHasOwnBorder = /border(-(top|bottom|left|right|color|style|width))?\s*:/i.test(def.css || '');
const suppressNativeBorder = !borderEnabled && (!themeHasOwnBorder || FORCE_HIDE_EDIT_BORDER_IDS.includes(cfg.bubbleId));
compiledCSS += `
${suppressNativeBorder ? `
[data-testid="completed-message"][class*="border-dashed"][class*="border-blue"] {
border-color: transparent !important;
}` : ''}
`;
}
const styleTag = document.createElement('style');
styleTag.id = 'cai-bubble-style';
styleTag.textContent = compiledCSS;
document.head.appendChild(styleTag);
const opacity = cfg.bgOpacity !== undefined ? cfg.bgOpacity : 1;
const brightness = cfg.bgBrightness !== undefined ? cfg.bgBrightness : 1;
const contrast = cfg.bgContrast !== undefined ? cfg.bgContrast : 1;
const saturation = cfg.bgSaturation !== undefined ? cfg.bgSaturation : 1;
const blur = cfg.bgBlur !== undefined ? cfg.bgBlur : 0;
if (cfg.bgType === 'image' && cfg.bgUrl) {
document.body.classList.add('cai-transparent-bg');
const bs = document.createElement('style');
bs.id = 'cai-bg-style';
const fitMap = { cover: 'cover', contain: 'contain', stretch: '100% 100%' };
const bgSize = fitMap[cfg.bgFit] || 'cover';
bs.textContent = `body::before { content: ""; position: fixed; top: -20px; left: -20px; width: calc(100vw + 40px); height: calc(100vh + 40px); background-image: url('${cfg.bgUrl}'); background-size: ${bgSize}; background-position: center; z-index: -9999; pointer-events: none; opacity: ${opacity}; filter: brightness(${brightness}) contrast(${contrast}) saturate(${saturation}) blur(${blur}px); }`;
document.head.appendChild(bs);
} else if (cfg.bgType === 'color' && cfg.bgColor) {
document.body.classList.add('cai-transparent-bg');
const bs = document.createElement('style');
bs.id = 'cai-bg-style';
bs.textContent = `body::before { content: ""; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: ${cfg.bgColor}; z-index: -9999; pointer-events: none; }`;
document.head.appendChild(bs);
}
}
// ─── AVATAR ENGINE ───
const botConfigCache = new Map();
const userConfigCache = new Map();
const CONFIG_CACHE_TTL = 3000;
function getBotConfig(botId) {
const cached = botConfigCache.get(botId);
if (cached && (Date.now() - cached.t) < CONFIG_CACHE_TTL) return cached.v;
const configStr = localStorage.getItem('customBotAvatarConfig_' + botId);
const parsed = configStr ? JSON.parse(configStr) : null;
botConfigCache.set(botId, { v: parsed, t: Date.now() });
return parsed;
}
function getUserConfig(botId) {
const cached = userConfigCache.get(botId);
if (cached && (Date.now() - cached.t) < CONFIG_CACHE_TTL) return cached.v;
const configStr = localStorage.getItem('customUserAvatarConfig_' + botId);
const parsed = configStr ? JSON.parse(configStr) : null;
userConfigCache.set(botId, { v: parsed, t: Date.now() });
return parsed;
}
// Returns the avatar diameter in px chosen in the Avatar tab (0=24, 1=36, 2=48, 3=64),
// or null if this character has never had a style saved (leave avatars untouched).
function getAvatarPx() {
const style = loadStyle();
if (!style) return null;
const idx = clampAvatarIdx(style.avatarSize !== undefined ? style.avatarSize : DEFAULT_AVATAR_IDX);
return AVATAR_SIZES[idx];
}
function applyAvatarSize(img) {
if (!img) return;
if (!img.closest('#chat-messages')) return;
const wrap = img.parentElement;
if (!wrap) return;
const style = loadStyle();
const outer = wrap.closest('.hidden.md\\:flex');
if (outer) {
if (style && style.hideAvatarsDesktop && window.innerWidth >= 768) {
outer.style.display = 'none';
} else {
outer.style.display = (style && style.avatarForceMobile) ? 'flex' : '';
}
}
const px = getAvatarPx();
if (!px) return;
wrap.style.width = px + 'px';
wrap.style.height = px + 'px';
wrap.style.borderRadius = px + 'px';
img.style.width = px + 'px';
img.style.height = px + 'px';
}
function getActiveChatMessageCount() {
const container = document.querySelector('#chat-messages');
if (!container) return 0;
const nodes = container.querySelectorAll('[data-testid="completed-message"]');
let count = 0;
nodes.forEach(node => {
const slide = node.closest('.swiper-slide');
if (!slide || slide.classList.contains('swiper-slide-active')) count++;
});
return count;
}
let maxKnownMessageCount = 0;
let bgMsgCounterObserver = null;
function trackMessageCountForActiveChat() {
const container = document.querySelector('#chat-messages');
if (!container) return;
if (bgMsgCounterObserver) bgMsgCounterObserver.disconnect();
maxKnownMessageCount = getActiveChatMessageCount();
setupDraftClearObserver();
if (!container) return;
if (bgMsgCounterObserver) bgMsgCounterObserver.disconnect();
maxKnownMessageCount = getActiveChatMessageCount();
bgMsgCounterObserver = new MutationObserver(() => {
const current = getActiveChatMessageCount();
if (current > maxKnownMessageCount) maxKnownMessageCount = current;
});
bgMsgCounterObserver.observe(container, { childList: true, subtree: true });
}
let msgCounterObserver = null;
function setupMessageCounterObserver() {
const header = document.querySelector('#chat-header');
const container = document.querySelector('#chat-messages');
if (!header || !container) return;
let counter = document.getElementById('cai-msg-counter');
if (!counter) {
counter = document.createElement('span');
counter.id = 'cai-msg-counter';
counter.style.cssText = 'font-size:11px;color:#ffffff;margin-left:8px;white-space:nowrap;pointer-events:none;';
header.appendChild(counter);
}
const update = () => { counter.textContent = getActiveChatMessageCount() + ' active messages'; };
update();
if (msgCounterObserver) msgCounterObserver.disconnect();
msgCounterObserver = new MutationObserver(update);
msgCounterObserver.observe(container, { childList: true, subtree: true });
}
function getActiveBotId() {
const pathParts = window.location.pathname.split('/').filter(Boolean);
if (pathParts.length > 0) return pathParts[pathParts.length - 1].split('?')[0];
return null;
}
function isProfilePage() {
return window.location.pathname.includes('/character/');
}
function getCharacterIdFromDom() {
const links = document.querySelectorAll('a[href*="/character/"]');
for (const link of links) {
const match = link.getAttribute('href').match(/^\/character\/([a-zA-Z0-9_-]+)$/);
if (!match) continue;
const rect = link.getBoundingClientRect();
if ((rect.width > 0 || rect.height > 0) && rect.left < window.innerWidth * 0.8) return match[1];
}
return null;
}
function getDraftBotId() {
const urlId = getActiveBotId();
if (!urlId || isProfilePage()) return urlId;
return getCharacterIdFromDom() || urlId;
}
function applyConfig(img, config) {
if (img.src !== config.url) {
img.src = config.url;
if (img.hasAttribute('srcset')) img.removeAttribute('srcset');
}
img.style.objectFit = 'cover';
img.style.objectPosition = `${config.posX}% ${config.posY}%`;
img.style.filter = `brightness(${config.brightness}%) contrast(${config.contrast}%) saturate(${config.saturation !== undefined ? config.saturation : 100}%)`;
const scale = (config.zoom || 100) / 100;
const panX = config.panX || 0, panY = config.panY || 0;
img.style.transform = `scale(${scale}) translate(${panX}%, ${panY}%)`;
img.setAttribute('data-cai-avatar-applied', '1');
}
function revertAvatarImg(img) {
if (img.getAttribute('data-orig') && img.src !== img.getAttribute('data-orig')) {
img.src = img.getAttribute('data-orig');
}
img.style.transform = '';
img.style.filter = '';
img.style.objectPosition = '';
img.removeAttribute('data-cai-avatar-applied');
}
function revertOrphanedAvatars() {
const currentActiveId = getActiveBotId();
document.querySelectorAll('img[data-cai-avatar-applied="1"]').forEach(img => {
const parentLink = img.closest('a[href*="/chat/"]');
let cfg;
if (parentLink) {
const parts = parentLink.getAttribute('href').split('/').filter(Boolean);
const targetBotId = parts[parts.length - 1].split('?')[0];
cfg = getBotConfig(targetBotId) || getUserConfig(targetBotId);
} else {
cfg = (currentActiveId && (getBotConfig(currentActiveId) || getUserConfig(currentActiveId))) || null;
}
if (!cfg || !cfg.url) revertAvatarImg(img);
});
}
function truncateName(name) {
if (name && name.length > 20) return name.slice(0, 17) + '...';
return name;
}
// ─── RENDERING ENGINE (AVATARS) ───
function replaceAvatars() {
const currentActiveId = getActiveBotId();
if (!currentActiveId) return;
if (isProfilePage()) {
const config = getBotConfig(currentActiveId);
if (!config || !config.url) return;
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('data-orig')) img.setAttribute('data-orig', img.src);
const rect = img.getBoundingClientRect();
const centerX = (rect.left + rect.right) / 2;
if (rect.width > 60 && centerX > window.innerWidth * 0.2 && centerX < window.innerWidth * 0.72
&& rect.top >= -50 && rect.top < window.innerHeight * 0.75) {
applyConfig(img, config);
}
});
return;
}
const allImages = Array.from(document.querySelectorAll('img'));
allImages.forEach(img => {
if (!img.hasAttribute('data-orig')) img.setAttribute('data-orig', img.src);
const parentLink = img.closest('a[href*="/chat/"]');
if (!parentLink) return;
const href = parentLink.getAttribute('href');
const parts = href.split('/').filter(Boolean);
const linkBotId = parts[parts.length - 1].split('?')[0];
if (linkBotId === currentActiveId) {
const origSrc = img.getAttribute('data-orig');
window['activeBotOrigSrc_' + currentActiveId] = origSrc;
localStorage.setItem('botOrigSrc_' + currentActiveId, origSrc);
}
});
if (!window['activeBotOrigSrc_' + currentActiveId]) {
window['activeBotOrigSrc_' + currentActiveId] = localStorage.getItem('botOrigSrc_' + currentActiveId);
}
const activeBotOrigSrc = window['activeBotOrigSrc_' + currentActiveId];
allImages.forEach(img => {
const origSrc = img.getAttribute('data-orig') || img.src;
const parentLink = img.closest('a[href*="/chat/"]');
if (parentLink) {
const href = parentLink.getAttribute('href');
const parts = href.split('/').filter(Boolean);
const targetBotId = parts[parts.length - 1].split('?')[0];
const botConfig = getBotConfig(targetBotId);
if (botConfig && botConfig.url) {
applyConfig(img, botConfig);
} else if (img.getAttribute('data-orig')) {
if (img.src !== img.getAttribute('data-orig')) img.src = img.getAttribute('data-orig');
img.style.transform = ''; img.style.filter = '';
}
} else {
const isFallbackPlaceholder = origSrc.includes('pfp-fallbacks');
if (!isFallbackPlaceholder && (!activeBotOrigSrc || origSrc !== activeBotOrigSrc)) return;
const config = getBotConfig(currentActiveId);
if (config && config.url) {
applyConfig(img, config);
} else if (img.getAttribute('data-orig')) {
if (img.src !== img.getAttribute('data-orig')) img.src = img.getAttribute('data-orig');
img.style.transform = ''; img.style.filter = '';
}
applyAvatarSize(img);
}
});
}
// ─── FORCE-PATCH FALLBACK AVATAR DURING GENERATION ───
function forcePatchGeneratingAvatar() {
const currentActiveId = getActiveBotId();
if (!currentActiveId || !isChatPage()) return;
const config = getBotConfig(currentActiveId);
document.querySelectorAll('#chat-messages img').forEach(img => {
if (img.closest('.flex-row-reverse')) return;
const isFallback = img.src.includes('pfp-fallbacks');
if (config && config.url && isFallback) applyConfig(img, config);
applyAvatarSize(img);
});
}
// ─── RENDERING ENGINE (NAMES) ───
function replaceAvatarsAndNames() {
checkSendTransitionInstant();
restoreDraftIfNeeded();
revertOrphanedAvatars();
replaceAvatars();
replaceUserAvatarAndName();
applyAuthorMobileReplacement();
setupGhostBlurTriggerObserver();
applyHideNames();
applyHideEditedMarks();
applyHideDistractors();
const currentActiveId = getActiveBotId();
if (!currentActiveId) return;
const customName = localStorage.getItem('customBotNameConfig_' + currentActiveId);
const origName = window['activeBotOrigName_' + currentActiveId] || localStorage.getItem('botOrigName_' + currentActiveId);
document.querySelectorAll('a[href*="/chat/"]').forEach(link => {
const href = link.getAttribute('href');
const parts = href.split('/').filter(Boolean);
const linkBotId = parts[parts.length - 1].split('?')[0];
let nameEl = nameElCache.get(link) || null;
if (nameEl && !link.contains(nameEl)) nameEl = null;
if (!nameEl) {
const divs = link.querySelectorAll('div, span, p');
for (const el of divs) {
if (el.children.length === 0 && el.innerText.trim() && !el.innerText.includes('@') && !el.innerText.includes('interactions') && !el.innerText.match(/^\d+(\.\d+)?[km]?$/i)) {
nameEl = el; break;
}
}
if (nameEl) nameElCache.set(link, nameEl);
}
if (nameEl) {
if (!nameEl.hasAttribute('data-orig-name')) nameEl.setAttribute('data-orig-name', nameEl.innerText.trim());
const linkOrigName = nameEl.getAttribute('data-orig-name');
if (linkBotId === currentActiveId) {
window['activeBotOrigName_' + currentActiveId] = linkOrigName;
localStorage.setItem('botOrigName_' + currentActiveId, linkOrigName);
}
const linkCustomName = localStorage.getItem('customBotNameConfig_' + linkBotId);
if (linkCustomName) {
const targetText = truncateName(linkCustomName);
if (nameEl.innerText !== targetText) nameEl.innerText = targetText;
} else if (nameEl.innerText !== linkOrigName) {
nameEl.innerText = linkOrigName;
}
}
});
document.querySelectorAll('textarea, input').forEach(input => {
let placeholder = input.getAttribute('placeholder');
if (placeholder && (placeholder.startsWith('Message ') || input.hasAttribute('data-orig-placeholder'))) {
if (!input.hasAttribute('data-orig-placeholder')) input.setAttribute('data-orig-placeholder', placeholder);
const origPlaceholder = input.getAttribute('data-orig-placeholder');
if (customName) {
const newPlaceholder = 'Message ' + truncateName(customName) + '...';
if (input.getAttribute('placeholder') !== newPlaceholder) input.setAttribute('placeholder', newPlaceholder);
} else if (input.getAttribute('placeholder') !== origPlaceholder) {
input.setAttribute('placeholder', origPlaceholder);
}
}
});
if (origName) {
// Fast check
document.querySelectorAll('[data-orig-name]').forEach(el => {
if (el.closest('#cai-style-modal')) return;
if (el.closest('a[href*="/chat/"]')) return;
const rect = el.getBoundingClientRect();
const isRightSidebar = rect.left > window.innerWidth * 0.72;
if (customName) {
const targetText = isRightSidebar ? truncateName(customName) : customName;
if (el.innerText !== targetText) el.innerText = targetText;
} else {
const defaultName = el.getAttribute('data-orig-name');
if (el.innerText !== defaultName) el.innerText = defaultName;
}
});
// Fast check for popups/dialogs (right panel) — small scope, runs every time, no cooldown
try {
document.querySelectorAll('[role="dialog"] *, #chat-header *').forEach(el => {
if (el.hasAttribute('data-orig-name')) return;
if (el.closest('a[href*="/chat/"]')) return;
if (el.closest('#cai-style-modal')) return;
if (el.children.length === 0 && typeof el.innerText === 'string' && el.innerText.trim() === origName) {
el.setAttribute('data-orig-name', origName);
const rect = el.getBoundingClientRect();
const isRightSidebar = rect.left > window.innerWidth * 0.72;
if (customName) el.innerText = isRightSidebar ? truncateName(customName) : customName;
}
});
} catch (err) { console.error('CAI fast-check error:', err); }
// Slow check
const now = Date.now();
if (now - lastNameFullScan > 3000) {
lastNameFullScan = now;
document.querySelectorAll('div, span, p, h1, h2, h3, h4').forEach(el => {
if (el.hasAttribute('data-orig-name')) return;
if (el.closest('a[href*="/chat/"]')) return;
if (el.closest('#cai-style-modal')) return;
if (el.children.length === 0 && el.innerText.trim() === origName) {
el.setAttribute('data-orig-name', origName);
const rect = el.getBoundingClientRect();
const isRightSidebar = rect.left > window.innerWidth * 0.72;
if (customName) el.innerText = isRightSidebar ? truncateName(customName) : customName;
}
});
}
}
}
// ─── RENDERING ENGINE (USER AVATAR & NAME) ───
function replaceUserAvatarAndName() {
const currentActiveId = getActiveBotId();
if (!currentActiveId) return;
const userCfg = getUserConfig(currentActiveId);
const customUserName = localStorage.getItem('customUserNameConfig_' + currentActiveId);
document.querySelectorAll('.flex-row-reverse').forEach(row => {
if (!row.querySelector(BUBBLE_TARGET)) return;
// AVATAR
const avatarCol = row.children[0];
const img = avatarCol ? avatarCol.querySelector('img') : null;
if (img) {
if (!img.hasAttribute('data-orig')) img.setAttribute('data-orig', img.src);
if (userCfg && userCfg.url) {
applyConfig(img, userCfg);
} else if (img.getAttribute('data-orig') && img.src !== img.getAttribute('data-orig')) {
img.src = img.getAttribute('data-orig');
img.style.transform = ''; img.style.filter = ''; img.style.objectPosition = '';
}
applyAvatarSize(img);
}
// NAME
const contentCol = row.children[1];
const nameWrap = contentCol ? contentCol.firstElementChild : null;
const nameEl = nameWrap ? nameWrap.querySelector('div.text-sm') : null;
if (nameEl) {
if (!nameEl.hasAttribute('data-orig-username')) nameEl.setAttribute('data-orig-username', nameEl.innerText.trim());
const origUserName = nameEl.getAttribute('data-orig-username');
if (customUserName) {
const targetText = truncateName(customUserName);
if (nameEl.innerText !== targetText) nameEl.innerText = targetText;
} else if (nameEl.innerText !== origUserName) {
nameEl.innerText = origUserName;
}
}
});
}
// ─── HIDE NAMES (JS, avoid global recalculation of :has()) ───
function applyHideNames() {
const style = loadStyle();
const enabled = !!(style && style.hideNames);
document.querySelectorAll('#chat-messages .mx-2.flex.flex-row.items-center.gap-2.font-light').forEach(el => {
const hasName = el.querySelector(':scope > [data-orig-name], :scope > [data-orig-username]');
if (hasName) el.style.display = enabled ? 'none' : '';
});
}
// ─── HIDE "EDITED" MARKS (JS, avoid global recalculation of :has()) ───
function applyHideEditedMarks() {
const style = loadStyle();
const enabled = !!(style && style.hideEditedMarks);
document.querySelectorAll('#chat-messages .flex.flex-row.gap-1.items-center').forEach(el => {
const hasMark = el.querySelector(':scope > p[title]');
if (hasMark) el.style.display = enabled ? 'none' : '';
});
}
// ─── HIDE DISTRACTORS (AI disclaimer bar, profile warning box, Feed/Labs sidebar links) ───
function applyHideDistractors() {
const enabled = isHideDistractorsEnabled();
document.documentElement.classList.toggle('cai-early-hide-distractors', enabled);
const WARNING_TEXTS = ['This is not a real person or licensed professional', 'no es una persona real'];
document.querySelectorAll('button[aria-label="Common.expand"]').forEach(el => {
el.style.display = enabled ? 'none' : '';
});
document.querySelectorAll('[class*="bg-warning"]').forEach(el => {
if (WARNING_TEXTS.some(t => el.textContent.includes(t))) {
el.style.display = enabled ? 'none' : '';
}
});
document.querySelectorAll('a[href="/feed"], a[href="/labs"], a[href="/video"], a[href="https://labs.character.ai"], a[href="https://character.ai/video"]').forEach(el => {
el.style.display = enabled ? 'none' : '';
});
}
// ─── AUTHOR "BY @X" MOBILE REPLACEMENT ───
function applyAuthorMobileReplacement() {
const style = loadStyle();
const enabled = !!(style && style.replaceAuthorMobile);
const link = document.querySelector('#chat-header a[href^="/profile/"]');
if (!link) return;
if (!link.hasAttribute('data-orig-author-text')) {
link.setAttribute('data-orig-author-text', link.textContent);
}
const isMobile = window.innerWidth < 768;
if (enabled && isMobile) {
if (link.textContent !== 'Online') link.textContent = 'Online';
} else {
const orig = link.getAttribute('data-orig-author-text');
if (link.textContent !== orig) link.textContent = orig;
}
}
// ─── OBSERVER ───
let isUpdating = false;
let debounceTimer = null;
let lastNameFullScan = 0;
let pendingRerun = false;
function triggerReplacementSafely() {
if (isUpdating) { pendingRerun = true; return; }
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (isUpdating) { pendingRerun = true; return; }
isUpdating = true;
avatarObserver.disconnect();
try { replaceAvatarsAndNames(); }
finally {
avatarObserver.observe(document.body, { childList: true, subtree: true });
isUpdating = false;
if (pendingRerun) { pendingRerun = false; triggerReplacementSafely(); }
}
}, 150);
}
const avatarObserver = new MutationObserver(() => triggerReplacementSafely());
// ─── BETA BADGE ───
const BETA_SVG = `<svg viewBox="0 0 45 17" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:45px;height:17px;margin-left:6px;"><rect x="0.5" y="1" width="44" height="15" rx="3.5" stroke="#ffffff"/><path fill="#ffffff" d="M7.5 5.875C7.5 5.66789 7.66789 5.5 7.875 5.5H12.125C12.3321 5.5 12.5 5.66789 12.5 5.875C12.5 6.08211 12.3321 6.25 12.125 6.25H12V7.86629C12.1194 8.00963 12.2527 8.1588 12.3958 8.31774C12.4112 8.33484 12.4267 8.3520 12.4423 8.36937C12.6035 8.54833 12.7748 8.73846 12.94 8.93544C13.3002 9.36505 13.657 9.85631 13.8547 10.4095C13.9448 10.662 14 10.9291 14 11.2081C14 12.4739 12.9739 13.5 11.7081 13.5H8.29188C7.02611 13.5 6 12.4739 6 11.2081C6 10.9291 6.05516 10.662 6.14535 10.4095C6.34301 9.85631 6.69982 9.36505 7.06005 8.93544C7.22521 8.73846 7.39649 8.54833 7.5577 8.36937C7.5733 8.35206 7.5888 8.33484 7.6042 8.31774C7.74728 8.1588 7.88058 8.00963 8 7.86629V6.25H7.875C7.66789 6.25 7.5 6.08211 7.5 5.875ZM8.75 6.25V8.13154L8.66783 8.23426C8.51305 8.42774 8.3371 8.62458 8.16162 8.81952C8.14641 8.83642 8.13119 8.85331 8.11598 8.8702C7.95335 9.05075 7.7909 9.23111 7.63475 9.41733C7.46273 9.62248 7.30478 9.82746 7.17085 10.0345C7.1871 10.0319 7.20351 10.0294 7.22008 10.0269C7.6673 9.95864 8.24651 9.88941 8.71536 9.90192C9.28335 9.91707 9.71495 10.0529 10.1021 10.1747L10.1126 10.178C10.502 10.3005 10.8469 10.4076 11.3046 10.4198C11.7032 10.4304 12.2275 10.3702 12.6668 10.3031C12.7726 10.287 12.8721 10.2707 12.962 10.2553C12.8054 9.97266 12.5982 9.69515 12.3652 9.41733C12.2091 9.23111 12.0467 9.05075 11.884 8.87021C11.8688 8.85331 11.8536 8.83642 11.8384 8.81952C11.6629 8.62458 11.487 8.42774 11.3322 8.23426L11.25 8.13154V6.25H8.75Z"/><path fill="#8E8E8E" d="M9.5 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm2-.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z"/><path fill="#ffffff" d="M18.702 11.5V5.137h2.61c.294 0 .564.027.81.081.252.048.468.135.648.261.186.126.33.294.432.504.102.21.153.471.153.783 0 .288-.066.552-.198.792-.132.234-.327.411-.585.531.348.09.615.264.801.522.186.252.279.582.279.99 0 .366-.069.672-.207.918-.132.24-.318.432-.558.576-.234.144-.507.249-.819.315-.312.06-.645.09-.999.09h-2.367zm1.125-1.026h1.341c.168 0 .333-.012.495-.036.162-.024.309-.066.441-.126.132-.06.234-.147.306-.261.078-.12.117-.273.117-.459 0-.168-.03-.306-.09-.414a.77.77 0 0 0-.252-.252 1.03 1.03 0 0 0-.369-.135 2.52 2.52 0 0 0-.456-.054h-1.539v1.737zm0-2.673h1.188c.186 0 .354-.018.504-.054.15-.036.279-.09.387-.162a.74.74 0 0 0 .252-.279.86.86 0 0 0 .09-.378c0-.21-.048-.366-.144-.468-.096-.108-.228-.177-.396-.207a2.66 2.66 0 0 0-.543-.054h-1.332v1.602zm5.858 3.699V5.137h4.491v1.026h-3.366v1.674h2.925v1.008h-2.925v1.629h3.366V11.5h-4.491zm6.931 0V6.163h-1.989V5.137h5.094v1.026h-1.989V11.5h-1.116zm2.712 0 2.439-6.363h1.341l2.43 6.363h-1.215l-.423-1.179h-2.934l-.432 1.179h-1.206zm2.001-2.196h2.205l-1.098-3.033-1.107 3.033z"/></svg>`;
// ─── MODAL ────────────────────────────────────────────────────────────────
function buildModal() {
if (document.getElementById('cai-style-modal')) return;
const saved = loadStyle() || {};
let tempCfg = { ...saved };
const modal = document.createElement('div');
modal.id = 'cai-style-modal';
modal.innerHTML = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&icon_names=cached');
@import url('https://fonts.googleapis.com/icon?family=Material+Icons+Outlined');
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 20px;
line-height: 1;
display: inline-block;
white-space: nowrap;
direction: ltr;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.material-icons-outlined {
font-family: 'Material Icons Outlined';
font-weight: normal;
font-style: normal;
font-size: 22px;
line-height: 1;
display: inline-block;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
.cai-preset-iconbtn {
width: 56px; height: 56px; flex-shrink: 0;
border-radius: 14px; border: 1px solid rgba(255,255,255,0.4);
background: rgba(39,39,42,0.55); color: #e4e4e7;
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: background 0.2s, border-color 0.2s;
}
.cai-preset-iconbtn:hover { background: #27272a; border-color: #fff; color: #fff; }
.cai-preset-iconbtn-wide { width: 90px; }
.cai-preset-iconbtn-wide .material-icons-outlined { font-size: 28px; }
.cai-preset-textinput {
height: 56px; min-width: 220px; flex: 1;
border-radius: 14px; border: 1px solid rgba(255,255,255,0.4);
background: transparent; color: #fff; text-align: center;
font-size: 1rem; font-weight: 500; padding: 0 16px; outline: none;
}
.cai-preset-textinput:focus { border-color: #fff; }
.cai-preset-textinput::placeholder { color: #a1a1aa; }
.cai-preset-list { display:flex; flex-direction:column; gap:10px; margin-top:4px; }
.cai-preset-row { display:flex; gap:10px; align-items:center; }
.cai-preset-row-btn {
width: 44px; height: 44px; flex-shrink: 0;
border-radius: 12px; border: 1px solid rgba(255,255,255,0.4);
background: rgba(39,39,42,0.55); color: #e4e4e7;
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: background 0.2s, border-color 0.2s;
}
.cai-preset-row-btn .material-icons-outlined { font-size: 20px; }
.cai-preset-row-btn:hover { background: #27272a; border-color: #fff; color: #fff; }
.cai-preset-row-input {
height: 44px; flex: 1; min-width: 0;
border-radius: 12px; border: 1px solid rgba(255,255,255,0.4);
background: transparent; color: #fff; text-align: center;
font-size: 0.9rem; font-weight: 500; padding: 0 12px; outline: none;
}
.cai-preset-row-input:focus { border-color: #fff; }
/* ── MOBILE OPTIMIZATION V2 (Fixes Double Scrollbars & Ugly Layout) ── */
@media (max-width: 768px) {
/* 1. Main Box Adjustments */
#cai-mbox { width: 95vw; max-height: 90vh; }
#cai-mhdr h2 { font-size: 1.05rem; } /* Prevent title from wrapping awkwardly */
#cai-mhdr { padding: 16px 16px 12px; }
/* 2. Horizontal Scroll for Tabs */
#cai-mtabs { overflow-x: auto; padding: 0 16px 12px; flex-wrap: nowrap; }
#cai-mtabs::-webkit-scrollbar { height: 0px; display: none; } /* Hide ugly scrollbar on tabs */
.cai-tab { padding: 8px 12px; font-size: 0.8rem; flex-shrink: 0; }
/* 3. Stack the layout vertically */
#cai-mbody { padding: 16px; flex-direction: column; overflow-y: auto; }
.cai-panel.active { flex-direction: column; gap: 16px; }
/* 4. Move Settings/Preview to the TOP */
#cai-rpanel { width: 100%; order: -1; }
/* Background tab preview: stack below controls, no reorder */
#cai-bgrpanel { width: 100%; }
/* 5. THE MAGIC FIX: Kill the nested scrollbars entirely */
#cai-blist, #cai-settings, .cai-scroll-panel {
max-height: none !important;
overflow-y: visible !important;
padding-right: 0;
}
/* 6. Make the Theme Grid exactly 2 columns so it looks neat */
.cai-bgrid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
.cai-bitem { padding: 8px; }
/* 7. Clean up the Footer Buttons */
#cai-mfooter { flex-wrap: wrap; gap: 10px; padding: 16px; justify-content: center; }
#cai-cancel, #cai-save { flex: 1; text-align: center; margin: 0; padding: 12px; }
#cai-reset { width: 100%; order: 3; margin: 0; padding: 12px; } /* Drops reset to the bottom full-width */
/* 8. Fix Advanced options squishing */
.cai-adv-info { max-width: 75%; }
/* 9. Fix Load Preset row overflow on narrow screens */
.cai-preset-textinput { min-width: 0; }
/* 10. Prevent iOS auto-zoom on input focus */
input[type=text], input[type=email], input[type=number], select, textarea {
font-size: 16px !important;
}
/* 11. Smoother scroll on iOS, no background bleed-through */
#cai-mbody, #cai-blist, .cai-scroll-panel, #cai-settings {
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
}
#cai-style-modal { position:fixed; inset:0; z-index:99998; background:transparent; display:flex; align-items:center; justify-content:center; font-family:'Onest',sans-serif; }
#cai-mbox { background:rgba(24,24,27,0.50); backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); border:1px solid #27272a; border-radius:24px; width:min(860px,95vw); max-height:90vh; display:flex; flex-direction:column; box-shadow:0 25px 50px -12px rgba(0,0,0,0.55); overflow:hidden; }
@keyframes caiModalIn { from { opacity:0; } to { opacity:1; } }
@keyframes caiModalOut { from { opacity:1; } to { opacity:0; } }
@keyframes caiBoxIn { from { opacity:0; } to { opacity:1; } }
#cai-mhdr { display:flex; align-items:center; justify-content:space-between; padding:24px 24px 16px; flex-shrink:0; }
#cai-mhdr h2 { margin:0; font-size:1.25rem; font-weight:500; color:#fff; display:flex; align-items:center; gap:8px; }
#cai-mhdr-title-wrap { display:flex; flex-direction:column; gap:2px; }
#cai-msg-counter { font-size:11px; font-weight:400; color:#ffffff; white-space:nowrap; }
#cai-mclose { background:transparent; border:none; color:#ffffff; cursor:pointer; font-size:1.2rem; padding:8px; border-radius:50%; transition:all 0.2s; display:flex; align-items:center; justify-content:center; }
#cai-mclose:hover { background:#27272a; color:#fff; }
#cai-mtabs { display:flex; gap:12px; padding:0 24px 16px; flex-shrink:0; border-bottom:1px solid #27272a; overflow-x:auto; flex-wrap:nowrap; scrollbar-width:none; -ms-overflow-style:none; }
#cai-mtabs::-webkit-scrollbar { display:none; }
.cai-tab { flex-shrink:0; }
.cai-tab { background:rgba(39,39,42,0.55); border:1px solid transparent; color:#e4e4e7; cursor:pointer; font-size:0.85rem; font-weight:500; padding:10px 20px; border-radius:12px; transition:all 0.2s; }
.cai-tab.active { background:#ffffff; color:#000000; border-color:#ffffff; }
.cai-tab:hover:not(.active) { background:#3f3f46; }
#cai-mbody { flex:1; overflow-y:auto; padding:20px 24px; display:flex; gap:20px; }
#cai-mbody::-webkit-scrollbar { width:6px; }
#cai-mbody::-webkit-scrollbar-thumb { background:#3f3f46; border-radius:3px; }
.cai-panel { display:none; width:100%; }
.cai-panel.active { display:flex; gap:20px; width:100%; }
#cai-blist { flex:1; min-width:0; display:flex; flex-direction:column; gap:16px; overflow-y:auto; max-height:60vh; padding-right:8px; }
#cai-blist::-webkit-scrollbar { width:4px; }
#cai-blist::-webkit-scrollbar-thumb { background:#3f3f46; border-radius:2px; }
.cai-scroll-panel { flex:1; min-width:0; overflow-y:auto; max-height:60vh; padding-right:8px; }
.cai-scroll-panel::-webkit-scrollbar { width:4px; }
.cai-scroll-panel::-webkit-scrollbar-thumb { background:#3f3f46; border-radius:2px; }
.cai-clabel { font-size:0.75rem; font-weight:500; color:#ffffff; margin-top:8px; margin-bottom:4px; }
.cai-bgrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:12px; }
.cai-bitem { background:#27272a; border:2px solid transparent; border-radius:16px; padding:10px; cursor:pointer; transition:all 0.2s; display:flex; flex-direction:column; gap:8px; }
.cai-bitem:hover { border-color:#52525b; background:#3f3f46; }
.cai-bitem.selected { border-color:#ffffff; background:#27272a; }
.cai-bdemo { padding:8px 12px; font-size:0.75rem; border-radius:12px; max-width:100%; word-break:break-word; line-height:1.4; position:relative; z-index:1; }
.cai-bname { font-size:0.7rem; color:#e4e4e7; text-align:center; font-weight:500; }
#cai-rpanel { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:16px; }
#cai-preview { background:rgba(39,39,42,0.55); border-radius:16px; padding:16px; display:flex; flex-direction:column; gap:12px; }
.cai-plabel { font-size:0.7rem; color:#ffffff; font-weight:500; margin-bottom:4px; }
.cai-pbubble { padding:10px 14px; font-size:0.85rem; border-radius:14px; line-height:1.4; word-break:break-word; position:relative; }
#cai-settings { background:rgba(39,39,42,0.55); border-radius:16px; padding:16px; display:flex; flex-direction:column; gap:16px; overflow-y:auto; max-height:45vh; }
#cai-settings::-webkit-scrollbar { width:4px; }
#cai-settings::-webkit-scrollbar-thumb { background:#52525b; border-radius:2px; }
.cai-stitle { font-size:0.8rem; font-weight:500; color:#fff; margin-bottom:4px; }
.cai-field { display:flex; flex-direction:column; gap:6px; }
.cai-field label { font-size:0.75rem; color:#ffffff; }
.cai-field input[type=text], .cai-field select { background:transparent; border:1px solid rgba(255,255,255,0.4); border-radius:12px; color:#fff; font-size:0.85rem; padding:10px 12px; outline:none; transition:border-color 0.2s; width:100%; }
.cai-field input:focus, .cai-field select:focus { border-color:#ffffff; }
.cai-field input[type=color] { width:100%; height:36px; border-radius:12px; border:1px solid rgba(255,255,255,0.4); padding:2px; background:transparent; cursor:pointer; }
.cai-field .clr-field { width: 100%; height: 36px; border-radius: 12px; border: 1px solid #3f3f46; overflow: hidden; }
.cai-field .clr-field input.coloris { width: 100%; height: 100%; opacity: 0; cursor: pointer; padding: 0; border: none; }
.cai-field .clr-field button { width: 100%; border-radius: 12px; }
.cai-field input[type=range] { width:100%; accent-color:#ffffff; }
.cai-row { display:flex; gap:12px; }
.cai-row .cai-field { flex:1; }
.cai-locked-bg { font-size:0.7rem; color:#ffffff; font-style:italic; text-align:center; background:#18181b; padding:6px 8px; border-radius:8px; }
#cai-colorblock { display:flex; flex-direction:column; gap:8px; }
#cai-colorblock .cai-row { gap:8px; }
#cai-colorblock .cai-field input[type=text] { padding:7px 10px; }
#cai-egrid { display:flex; flex-wrap:wrap; gap:6px; max-height:80px; overflow-y:auto; padding:2px; }
.cai-echip { background:#3f3f46; border:1px solid #52525b; border-radius:8px; padding:4px 8px; font-size:0.85rem; cursor:pointer; display:flex; align-items:center; gap:4px; transition:background 0.2s; color:#fff; }
.cai-echip:hover { background:#ef4444; border-color:#ef4444; }
.cai-echip span { color:#fca5a5; font-size:0.65rem; font-weight:bold; }
.cai-fs-row { display:flex; align-items:center; gap:8px; }
.cai-fs-btn { flex-shrink:0; width:36px; height:36px; border-radius:12px; border:1px solid rgba(255,255,255,0.4); background:rgba(39,39,42,0.55); color:#fff; font-size:1rem; font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background 0.2s; user-select:none; }
.cai-fs-btn:hover { background:#27272a; }
.cai-fs-btn:active { transform:scale(0.95); }
.cai-fs-input { flex:1; min-width:0; text-align:center; background:transparent; border:1px solid rgba(255,255,255,0.4); border-radius:12px; color:#fff; font-size:0.85rem; padding:10px 4px; outline:none; transition:border-color 0.2s; }
.cai-fs-input:focus { border-color:#ffffff; }
.cai-fs-unit { font-size:0.75rem; color:#ffffff; flex-shrink:0; }
/* ── BG PANEL ── */
#cai-bgpanel { flex:1; min-width:0; display:flex; flex-direction:column; gap:20px; }
#cai-bgrpanel { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:12px; }
.cai-bg-note { background:#27272a; padding:16px; border-radius:16px; font-size:0.85rem; color:#e4e4e7; line-height:1.5; border-left:4px solid #ffffff; }
#cai-bgmode-btns { display:flex; gap:10px; }
.cai-bgmode-btn { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; padding:16px 12px; background:rgba(39,39,42,0.55); border:2px solid transparent; border-radius:16px; color:#ffffff; cursor:pointer; font-size:0.78rem; font-weight:500; transition:all 0.2s; }
.cai-bgmode-btn svg { width:22px; height:22px; }
.cai-bgmode-btn:hover { border-color:#52525b; color:#e4e4e7; }
.cai-bgmode-btn.active { background:#ffffff; border-color:#ffffff; color:#000000; }
/* ── AVATAR PANEL ── */
#cai-avatarpanel { width:100%; display:flex; flex-direction:column; gap:20px; max-width:600px; margin:0 auto; }
#cai-avatarmode-btns { display:flex; gap:10px; }
#cai-avatar-preview-wrap { width:120px; height:120px; margin:0 auto; border-radius:50%; border:2px dashed rgba(255,255,255,0.4); display:flex; align-items:center; justify-content:center; overflow:hidden; background:#18181b; position:relative; }
#cai-avatar-preview { width:100%; height:100%; object-fit:cover; object-position:50% 50%; display:none; cursor:move; user-select:none; touch-action:none; }
#cai-avatar-placeholder { color:#71717a; font-size:0.75rem; }
#cai-uavatar-preview-wrap { width:120px; height:120px; margin:0 auto; border-radius:50%; border:2px dashed rgba(255,255,255,0.4); display:flex; align-items:center; justify-content:center; overflow:hidden; background:#18181b; position:relative; }
#cai-uavatar-preview { width:100%; height:100%; object-fit:cover; object-position:50% 50%; display:none; cursor:move; user-select:none; touch-action:none; }
#cai-uavatar-placeholder { color:#71717a; font-size:0.75rem; }
/* ── ADVANCED PANEL ── */
.cai-adv-item { display: flex; align-items: center; justify-content: space-between; background: #27272a; padding: 16px; border-radius: 16px; border: 1px solid #3f3f46; margin-bottom: 12px; }
.cai-adv-info { display: flex; flex-direction: column; gap: 6px; max-width: 75%; }
.cai-adv-title { font-size: 0.95rem; font-weight: 600; color: #f4f4f5; }
.cai-adv-desc { font-size: 0.75rem; color: #a1a1aa; line-height: 1.4; }
.cai-switch { position: relative; display: inline-block; width: 46px; height: 26px; flex-shrink: 0; }
.cai-switch input { opacity: 0; width: 0; height: 0; }
.cai-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #52525b; transition: .3s; border-radius: 26px; }
.cai-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.cai-switch input:checked + .cai-slider { background-color: #10b981; }
.cai-switch input:checked + .cai-slider:before { transform: translateX(20px); }
.clr-picker { z-index: 999999 !important; }
/* footer */
#cai-mfooter { padding:16px 24px; border-top:1px solid #27272a; display:flex; justify-content:flex-end; align-items:center; gap:12px; flex-shrink:0; }
.cai-btn { padding:10px 20px; border-radius:12px; font-size:0.85rem; font-weight:500; cursor:pointer; border:none; transition:all 0.2s; }
.cai-ghost { background:transparent; color:#ffffff; } .cai-ghost:hover { background:#27272a; color:#fff; }
.cai-reset { background:rgba(39,39,42,0.55); color:#e4e4e7; margin-right:auto; border:1px solid #3f3f46; } .cai-reset:hover { background:#3f3f46; color:#fff; }
.cai-primary { background:#ffffff; color:#000000; font-weight:600; } .cai-primary:hover { opacity:0.9; transform:scale(0.98); }
</style>
<div id="cai-mbox">
<div id="cai-mhdr">
<div id="cai-mhdr-title-wrap">
<h2>•˚✦ C.ai Customizer ✦˚• ${BETA_SVG}</h2>
<span id="cai-msg-counter">${Math.max(maxKnownMessageCount, getActiveChatMessageCount())} active messages</span>
</div>
<button id="cai-mclose"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
</div>
<div id="cai-mtabs">
<button class="cai-tab active" data-tab="bubble">Chat Bubbles</button>
<button class="cai-tab" data-tab="adv-bubble">Adv. Bubbles</button>
<button class="cai-tab" data-tab="font">Font</button>
<button class="cai-tab" data-tab="bg">Background </button>
<button class="cai-tab" data-tab="avatar">Avatar</button>
<button class="cai-tab" data-tab="svd-presets">Svd. Presets</button>
</div>
<div id="cai-mbody">
<!-- BUBBLE TAB -->
<div class="cai-panel active" id="cai-tab-bubble">
<div id="cai-blist"></div>
<div id="cai-rpanel">
<div id="cai-preview">
<div class="cai-plabel">Preview</div>
<div class="cai-pbubble" id="cai-pai">Hello. This is the bot message ✨</div>
<div class="cai-pbubble" id="cai-puser" style="align-self:flex-end">This would be my message 😊</div>
</div>
<div id="cai-settings">
<div id="cai-spsec" style="display:none;">
<div class="cai-field">
<label id="cai-splabel">Special color</label>
<input type="text" id="cai-spinp" class="coloris" />
</div>
</div>
<div id="cai-colorblock">
<div id="cai-bg-locked-note" class="cai-locked-bg" style="display:none">🔒 Background color set by the theme</div>
<div class="cai-row" id="cai-bg-pickers">
<div class="cai-field"><label>BG bot</label><input type="text" id="cai-ai-bg" class="coloris" /></div>
<div class="cai-field"><label>BG you</label><input type="text" id="cai-u-bg" class="coloris" /></div>
</div>
<div class="cai-row">
<div class="cai-field"><label>Text color bot</label><input type="text" id="cai-ai-tx" class="coloris" /></div>
<div class="cai-field"><label>Text color you</label><input type="text" id="cai-u-tx" class="coloris" /></div>
</div>
<div class="cai-field" id="cai-blur-wrap" style="margin-top:4px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:ffffff;">
<input type="checkbox" id="cai-blur-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Blur bubble background
</label>
<div id="cai-blur-slider-wrap" style="display:none; margin-top:8px;">
<label>Blur: <span id="cai-blur-val" style="color:#fff">8px</span></label>
<input type="range" id="cai-blur-amount" min="0" max="20" value="8"/>
</div>
</div>
</div>
<div id="cai-esec" style="display:none">
<div class="cai-stitle">Emoji background</div>
<div class="cai-field">
<label>Add emoji (max 20)</label>
<div style="display:flex;gap:8px">
<input type="text" id="cai-einput" placeholder="Emoji…" style="flex:1" maxlength="8"/>
<button class="cai-btn cai-primary" id="cai-eadd" style="padding:10px 14px;">Add</button>
</div>
</div>
<div id="cai-egrid"></div>
<div class="cai-field">
<label>Spin: <span id="cai-aval" style="color:#fff">0°</span></label>
<input type="range" id="cai-angle" min="-180" max="180" value="0"/>
</div>
<div class="cai-field">
<label>Opacity: <span id="cai-alphaval" style="color:#fff">85%</span></label>
<input type="range" id="cai-alpha" min="0" max="100" value="85"/>
</div>
</div>
</div>
</div>
</div>
<!-- BG TAB -->
<div class="cai-panel" id="cai-tab-bg">
<div id="cai-bgpanel">
<div class="cai-field"><label>BG Mode</label></div>
<div id="cai-bgmode-btns">
<button class="cai-bgmode-btn" data-mode="none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
None
</button>
<button class="cai-bgmode-btn" data-mode="image">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
Image
</button>
</div>
<div class="cai-field" id="cai-bgnone-wrap" style="display:flex; flex-direction:column; gap:10px;">
<label>Solid color background:</label>
<div class="cai-row" style="gap:10px;">
<button type="button" class="cai-btn cai-reset" id="cai-bgcolor-default">Default</button>
<input type="text" id="cai-bgcolor" class="coloris" placeholder="#000000" style="flex:1;"/>
</div>
</div>
<div class="cai-field" id="cai-bgurl-wrap" style="display:none">
<label>Paste the link of the image here:</label>
<input type="text" id="cai-bgurl" placeholder="https://..."/>
</div>
<div class="cai-field" id="cai-bgfit-wrap" style="display:none">
<label>Image fit:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bgfit" id="cai-bgfit-cover" style="width:16px;height:16px;cursor:pointer;"/> Fill
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bgfit" id="cai-bgfit-contain" style="width:16px;height:16px;cursor:pointer;"/> Full
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bgfit" id="cai-bgfit-stretch" style="width:16px;height:16px;cursor:pointer;"/> Stretch
</label>
</div>
</div>
<div id="cai-bg-controls" style="display:none; flex-direction:column; gap:16px;">
<div class="cai-field">
<label>BG Opacity: <span id="cai-bg-opacity-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-bg-opacity" min="0" max="100" value="100"/>
</div>
<div class="cai-field">
<label>BG Contrast: <span id="cai-bg-contrast-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-bg-contrast" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>BG Brightness: <span id="cai-bg-bright-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-bg-bright" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>BG Saturation: <span id="cai-bg-sat-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-bg-sat" min="0" max="300" value="100"/>
</div>
<div class="cai-field">
<label>BG Blur: <span id="cai-bg-blur-val" style="color:#fff">0px</span></label>
<input type="range" id="cai-bg-blur" min="0" max="20" value="0"/>
</div>
</div>
</div>
<div id="cai-bgrpanel">
<div class="cai-plabel">Preview</div>
<div id="cai-bgpreview" style="border-radius:16px;overflow:hidden;max-height:300px;display:none;border:1px solid #3f3f46;background:#18181b;">
<img id="cai-bgimg" style="width:100%;object-fit:cover;display:none"/>
</div>
</div>
</div>
<!-- ADV. BUBBLE TAB -->
<div class="cai-panel" id="cai-tab-adv-bubble">
<div class="cai-scroll-panel">
<div class="cai-stitle" style="margin-bottom: 16px;">Advanced Bubble Options</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Space between bubbles: <span id="cai-mspacing-val" style="color:#fff">0</span>px</label>
<input type="range" id="cai-mspacing" min="-10" max="40" step="1" value="0"/>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Style:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-style" id="cai-style-classic" style="width:16px;height:16px;cursor:pointer;"/> Classic
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-style" id="cai-style-dense" style="width:16px;height:16px;cursor:pointer;"/> Dense
</label>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Alignment:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-align" id="cai-align-default" style="width:16px;height:16px;cursor:pointer;"/> Default
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-align" id="cai-align-left" style="width:16px;height:16px;cursor:pointer;"/> Left
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-align" id="cai-align-right" style="width:16px;height:16px;cursor:pointer;"/> Right
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-bubble-align" id="cai-align-reverse" style="width:16px;height:16px;cursor:pointer;"/> Reverse
</label>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-border-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Chat bubble border
</label>
<div id="cai-border-controls" style="display:none; margin-top:10px; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Color mode:</label>
<select id="cai-border-mode";>
<option value="dual" style="color: #000000;">Dual (one color for both)</option>
<option value="individual" style="color: #000000;">Individual (bot & user separated colors)</option>
</select>
</div>
<div class="cai-row" id="cai-border-dual-wrap">
<div class="cai-field">
<label>Border width: <span id="cai-border-width-val">2</span>px</label>
<input type="range" id="cai-border-width" min="0" max="10" step="1" value="2"/>
</div>
<div class="cai-field">
<label>Border color</label>
<input type="text" id="cai-border-color" class="coloris" />
</div>
</div>
<div id="cai-border-individual-wrap" style="display:none; flex-direction:column; gap:12px;">
<div class="cai-row">
<div class="cai-field">
<label>Bot border width: <span id="cai-border-width-ai-val">2</span>px</label>
<input type="range" id="cai-border-width-ai" min="0" max="10" step="1" value="2"/>
</div>
<div class="cai-field">
<label>User border width: <span id="cai-border-width-user-val">2</span>px</label>
<input type="range" id="cai-border-width-user" min="0" max="10" step="1" value="2"/>
</div>
</div>
<div class="cai-row">
<div class="cai-field">
<label>Border color bot</label>
<input type="text" id="cai-border-color-ai" class="coloris" />
</div>
<div class="cai-field">
<label>Border color you</label>
<input type="text" id="cai-border-color-user" class="coloris" />
</div>
</div>
</div>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-shadow-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Outer shadow
</label>
<div id="cai-shadow-controls" style="display:none; margin-top:10px; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Color mode:</label>
<select id="cai-shadow-mode">
<option value="dual" style="color: #000000;">Dual (one color for both)</option>
<option value="individual" style="color: #000000;">Individual (bot & user separated colors)</option>
</select>
</div>
<div class="cai-row" id="cai-shadow-dual-wrap">
<div class="cai-field">
<label>Shadow color</label>
<input type="text" id="cai-shadow-color" class="coloris" />
</div>
</div>
<div class="cai-row" id="cai-shadow-individual-wrap" style="display:none;">
<div class="cai-field">
<label>Shadow color bot</label>
<input type="text" id="cai-shadow-color-ai" class="coloris" />
</div>
<div class="cai-field">
<label>Shadow color you</label>
<input type="text" id="cai-shadow-color-user" class="coloris" />
</div>
</div>
<div class="cai-row">
<div class="cai-field">
<label>Spread: <span id="cai-shadow-spread-val" style="color:#fff">0</span>px</label>
<input type="range" id="cai-shadow-spread" min="-30" max="50" step="1" value="0"/>
</div>
<div class="cai-field">
<label>Blur: <span id="cai-shadow-blur-val" style="color:#fff">10</span>px</label>
<input type="range" id="cai-shadow-blur" min="0" max="40" step="1" value="10"/>
</div>
</div>
<div class="cai-row">
<div class="cai-field">
<label>Offset X: <span id="cai-shadow-offx-val" style="color:#fff">0</span>px</label>
<input type="range" id="cai-shadow-offx" min="-20" max="20" step="1" value="0"/>
</div>
<div class="cai-field">
<label>Offset Y: <span id="cai-shadow-offy-val" style="color:#fff">4</span>px</label>
<input type="range" id="cai-shadow-offy" min="-20" max="20" step="1" value="4"/>
</div>
</div>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-innershadow-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Inner shadow
</label>
<div id="cai-innershadow-controls" style="display:none; margin-top:10px; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Color mode:</label>
<select id="cai-innershadow-mode">
<option value="dual" style="color: #000000;">Dual (one color for both)</option>
<option value="individual" style="color: #000000;">Individual (bot & user separated colors)</option>
</select>
</div>
<div class="cai-row" id="cai-innershadow-dual-wrap">
<div class="cai-field">
<label>Shadow color</label>
<input type="text" id="cai-innershadow-color" class="coloris" />
</div>
</div>
<div class="cai-row" id="cai-innershadow-individual-wrap" style="display:none;">
<div class="cai-field">
<label>Shadow color bot</label>
<input type="text" id="cai-innershadow-color-ai" class="coloris" />
</div>
<div class="cai-field">
<label>Shadow color you</label>
<input type="text" id="cai-innershadow-color-user" class="coloris" />
</div>
</div>
<div class="cai-row">
<div class="cai-field">
<label>Spread: <span id="cai-innershadow-spread-val" style="color:#fff">0</span>px</label>
<input type="range" id="cai-innershadow-spread" min="-30" max="50" step="1" value="0"/>
</div>
<div class="cai-field">
<label>Blur: <span id="cai-innershadow-blur-val" style="color:#fff">10</span>px</label>
<input type="range" id="cai-innershadow-blur" min="0" max="40" step="1" value="10"/>
</div>
</div>
<div class="cai-row">
<div class="cai-field">
<label>Offset X: <span id="cai-innershadow-offx-val" style="color:#fff">0</span>px</label>
<input type="range" id="cai-innershadow-offx" min="-20" max="20" step="1" value="0"/>
</div>
<div class="cai-field">
<label>Offset Y: <span id="cai-innershadow-offy-val" style="color:#fff">4</span>px</label>
<input type="range" id="cai-innershadow-offy" min="-20" max="20" step="1" value="4"/>
</div>
</div>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-corners-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Custom bubble corners
</label>
<div id="cai-corners-controls" style="display:none; margin-top:10px; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Corners:</label>
<select id="cai-corners-scope">
<option value="dual" style="color:#000000;">Dual (one setting for both)</option>
<option value="individual" style="color:#000000;">Individual (bot & user separated setting)</option>
</select>
</div>
<div id="cai-corners-dual-wrap" style="display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Shape:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode" id="cai-corners-uniform" style="width:16px;height:16px;cursor:pointer;"/> Uniform
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode" id="cai-corners-custom" style="width:16px;height:16px;cursor:pointer;"/> Custom
</label>
</div>
</div>
<div class="cai-field" id="cai-corners-uniform-wrap">
<label>Corner radius: <span id="cai-corner-radius-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-radius" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-row" id="cai-corners-custom-wrap" style="display:none;">
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Left: <span id="cai-corner-tl-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tl" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Left: <span id="cai-corner-bl-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-bl" min="0" max="50" step="1" value="16"/>
</div>
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Right: <span id="cai-corner-tr-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tr" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Right: <span id="cai-corner-br-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-br" min="0" max="50" step="1" value="16"/>
</div>
</div>
</div>
</div>
<div id="cai-corners-individual-wrap" style="display:none; flex-direction:column; gap:16px;">
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cai-stitle" style="margin-bottom:0;">Bot</div>
<div class="cai-field">
<div style="display:flex; gap:16px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode-ai" id="cai-corners-uniform-ai" style="width:16px;height:16px;cursor:pointer;"/> Uniform
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode-ai" id="cai-corners-custom-ai" style="width:16px;height:16px;cursor:pointer;"/> Custom
</label>
</div>
</div>
<div class="cai-field" id="cai-corners-uniform-wrap-ai">
<label>Corner radius: <span id="cai-corner-radius-ai-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-radius-ai" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-row" id="cai-corners-custom-wrap-ai" style="display:none;">
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Left: <span id="cai-corner-tl-ai-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tl-ai" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Left: <span id="cai-corner-bl-ai-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-bl-ai" min="0" max="50" step="1" value="16"/>
</div>
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Right: <span id="cai-corner-tr-ai-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tr-ai" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Right: <span id="cai-corner-br-ai-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-br-ai" min="0" max="50" step="1" value="16"/>
</div>
</div>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cai-stitle" style="margin-bottom:0;">User</div>
<div class="cai-field">
<div style="display:flex; gap:16px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode-user" id="cai-corners-uniform-user" style="width:16px;height:16px;cursor:pointer;"/> Uniform
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-corners-mode-user" id="cai-corners-custom-user" style="width:16px;height:16px;cursor:pointer;"/> Custom
</label>
</div>
</div>
<div class="cai-field" id="cai-corners-uniform-wrap-user">
<label>Corner radius: <span id="cai-corner-radius-user-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-radius-user" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-row" id="cai-corners-custom-wrap-user" style="display:none;">
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Left: <span id="cai-corner-tl-user-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tl-user" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Left: <span id="cai-corner-bl-user-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-bl-user" min="0" max="50" step="1" value="16"/>
</div>
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:12px;">
<div class="cai-field">
<label>Top-Right: <span id="cai-corner-tr-user-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-tr-user" min="0" max="50" step="1" value="16"/>
</div>
<div class="cai-field">
<label>Bottom-Right: <span id="cai-corner-br-user-val" style="color:#fff">16</span>px</label>
<input type="range" id="cai-corner-br-user" min="0" max="50" step="1" value="16"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Aquí irán más ajustes avanzados en el futuro -->
</div>
</div>
<!-- FONT TAB -->
<div class="cai-panel" id="cai-tab-font">
<div class="cai-scroll-panel">
<div class="cai-stitle" style="margin-bottom: 16px;">Font Options</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Scope:</label>
<select id="cai-font-scope">
<option value="dual" style="color:#000000;">Dual (one setting for both)</option>
<option value="individual" style="color:#000000;">Individual (bot & user separated setting)</option>
</select>
</div>
<div id="cai-font-dual-wrap" style="display:flex; flex-direction:column;">
<div class="cai-field" style="margin-bottom:16px;">
<label>Font name (from <a href="https://fonts.google.com/" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Google Fonts</a>)</label>
<input type="text" id="cai-font" placeholder="Noto, Montserrat, Sniglet…" />
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Font size</label>
<div class="cai-fs-row">
<button type="button" id="cai-fs-dec" class="cai-fs-btn">−</button>
<input type="text" id="cai-fs-val" class="cai-fs-input" inputmode="numeric" />
<button type="button" id="cai-fs-inc" class="cai-fs-btn">+</button>
<span class="cai-fs-unit">px</span>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Line height: <span id="cai-lh-val" style="color:#fff">1.4</span></label>
<input type="range" id="cai-lh" min="1" max="2.2" step="0.1" value="1.4"/>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Letter spacing: <span id="cai-ls-val" style="color:#fff">0px</span></label>
<input type="range" id="cai-ls" min="-2" max="8" step="0.1" value="0"/>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label>Text alignment:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign" id="cai-talign-left" style="width:16px;height:16px;cursor:pointer;"/> Left
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign" id="cai-talign-center" style="width:16px;height:16px;cursor:pointer;"/> Center
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign" id="cai-talign-right" style="width:16px;height:16px;cursor:pointer;"/> Right
</label>
</div>
</div>
<div class="cai-field" style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-italicbold-toggle" style="width:16px;height:16px;cursor:pointer;"/>
Custom italic and bold color
</label>
<div class="cai-row" id="cai-italicbold-colors" style="display:none; margin-top:10px;">
<div class="cai-field"><label>Italic color</label><input type="text" id="cai-italic-color" class="coloris" /></div>
<div class="cai-field"><label>Bold color</label><input type="text" id="cai-bold-color" class="coloris" /></div>
</div>
</div>
</div>
<div id="cai-font-individual-wrap" style="display:none; flex-direction:column; gap:20px;">
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cai-stitle" style="margin-bottom:0;">Bot</div>
<div class="cai-field">
<label>Font name (from <a href="https://fonts.google.com/" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Google Fonts</a>)</label>
<input type="text" id="cai-font-ai" placeholder="Noto, Montserrat, Sniglet…" />
</div>
<div class="cai-field">
<label>Font size</label>
<div class="cai-fs-row">
<button type="button" id="cai-fs-ai-dec" class="cai-fs-btn">−</button>
<input type="text" id="cai-fs-ai-val" class="cai-fs-input" inputmode="numeric" />
<button type="button" id="cai-fs-ai-inc" class="cai-fs-btn">+</button>
<span class="cai-fs-unit">px</span>
</div>
</div>
<div class="cai-field">
<label>Line height: <span id="cai-lh-ai-val" style="color:#fff">1.4</span></label>
<input type="range" id="cai-lh-ai" min="1" max="2.2" step="0.1" value="1.4"/>
</div>
<div class="cai-field">
<label>Letter spacing: <span id="cai-ls-ai-val" style="color:#fff">0px</span></label>
<input type="range" id="cai-ls-ai" min="-2" max="8" step="0.1" value="0"/>
</div>
<div class="cai-field">
<label>Text alignment:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-ai" id="cai-talign-ai-left" style="width:16px;height:16px;cursor:pointer;"/> Left
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-ai" id="cai-talign-ai-center" style="width:16px;height:16px;cursor:pointer;"/> Center
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-ai" id="cai-talign-ai-right" style="width:16px;height:16px;cursor:pointer;"/> Right
</label>
</div>
</div>
<div class="cai-field">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-italicbold-toggle-ai" style="width:16px;height:16px;cursor:pointer;"/>
Custom italic and bold color
</label>
<div class="cai-row" id="cai-italicbold-colors-ai" style="display:none; margin-top:10px;">
<div class="cai-field"><label>Italic color</label><input type="text" id="cai-italic-color-ai" class="coloris" /></div>
<div class="cai-field"><label>Bold color</label><input type="text" id="cai-bold-color-ai" class="coloris" /></div>
</div>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:12px;">
<div class="cai-stitle" style="margin-bottom:0;">User</div>
<div class="cai-field">
<label>Font name (from <a href="https://fonts.google.com/" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Google Fonts</a>)</label>
<input type="text" id="cai-font-user" placeholder="Noto, Montserrat, Sniglet…" />
</div>
<div class="cai-field">
<label>Font size</label>
<div class="cai-fs-row">
<button type="button" id="cai-fs-user-dec" class="cai-fs-btn">−</button>
<input type="text" id="cai-fs-user-val" class="cai-fs-input" inputmode="numeric" />
<button type="button" id="cai-fs-user-inc" class="cai-fs-btn">+</button>
<span class="cai-fs-unit">px</span>
</div>
</div>
<div class="cai-field">
<label>Line height: <span id="cai-lh-user-val" style="color:#fff">1.4</span></label>
<input type="range" id="cai-lh-user" min="1" max="2.2" step="0.1" value="1.4"/>
</div>
<div class="cai-field">
<label>Letter spacing: <span id="cai-ls-user-val" style="color:#fff">0px</span></label>
<input type="range" id="cai-ls-user" min="-2" max="8" step="0.1" value="0"/>
</div>
<div class="cai-field">
<label>Text alignment:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-user" id="cai-talign-user-left" style="width:16px;height:16px;cursor:pointer;"/> Left
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-user" id="cai-talign-user-center" style="width:16px;height:16px;cursor:pointer;"/> Center
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-talign-user" id="cai-talign-user-right" style="width:16px;height:16px;cursor:pointer;"/> Right
</label>
</div>
</div>
<div class="cai-field">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7;">
<input type="checkbox" id="cai-italicbold-toggle-user" style="width:16px;height:16px;cursor:pointer;"/>
Custom italic and bold color
</label>
<div class="cai-row" id="cai-italicbold-colors-user" style="display:none; margin-top:10px;">
<div class="cai-field"><label>Italic color</label><input type="text" id="cai-italic-color-user" class="coloris" /></div>
<div class="cai-field"><label>Bold color</label><input type="text" id="cai-bold-color-user" class="coloris" /></div>
</div>
</div>
</div>
</div>
<div class="cai-field" style="margin-top:20px;">
<label>Enter key:</label>
<div style="display:flex; gap:16px; margin-top:6px;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-enterkey" id="cai-enterkey-newline" style="width:16px;height:16px;cursor:pointer;"/> Insert line
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="radio" name="cai-enterkey" id="cai-enterkey-send" style="width:16px;height:16px;cursor:pointer;"/> Send message
</label>
</div>
</div>
<div class="cai-field" style="margin-top:20px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="checkbox" id="cai-msg-draft" style="width:16px;height:16px;cursor:pointer;"/> Message draft
</label>
</div>
<div class="cai-field" style="margin-top:12px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="checkbox" id="cai-hide-distractors" style="width:16px;height:16px;cursor:pointer;"/> Hide distractors
</label>
</div>
<div class="cai-field" style="margin-top:12px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#e4e4e7; font-size:0.85rem;">
<input type="checkbox" id="cai-hide-edited" style="width:16px;height:16px;cursor:pointer;"/> Hide "edited" marks
</label>
</div>
</div>
</div>
<!-- AVATAR TAB -->
<div class="cai-panel" id="cai-tab-avatar">
<div id="cai-avatarpanel">
<div class="cai-field" style="margin-bottom:16px;">
<label>Avatar size (bot & user): <span id="cai-avatar-size-val" style="color:#fff">24px</span></label>
<input type="range" id="cai-avatar-size" min="0" max="3" step="1" value="0"/>
</div>
<div class="cai-field" style="margin-bottom:16px; flex-direction:row; flex-wrap:wrap; gap:20px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:ffffff;">
<input type="checkbox" id="cai-avatar-force-mobile" style="width:16px;height:16px;cursor:pointer;"/>
Show avatars on mobile
</label>
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:ffffff;">
<input type="checkbox" id="cai-hide-avatars-desktop" style="width:16px;height:16px;cursor:pointer;"/>
Hide avatars on desktop
</label>
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:ffffff;">
<input type="checkbox" id="cai-hide-names" style="width:16px;height:16px;cursor:pointer;"/>
Hide names
</label>
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:ffffff;">
<input type="checkbox" id="cai-replace-author-mobile" style="width:16px;height:16px;cursor:pointer;"/>
Replace "by @author" with "Online" (mobile)
</label>
</div>
<div class="cai-field"><label>Avatar Type</label></div>
<div id="cai-avatarmode-btns">
<button class="cai-bgmode-btn" data-mode="bot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/></svg>
Bot
</button>
<button class="cai-bgmode-btn" data-mode="user">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
User
</button>
</div>
<!-- BOT PANEL -->
<div id="cai-avatar-bot-wrap" style="display:flex; flex-direction:column; gap:16px;">
<div class="cai-field">
<label>Bot custom name (max 50 chars)</label>
<div class="cai-row" style="gap:10px; align-items:center;">
<input type="text" id="cai-avatar-name" placeholder="Custom Bot Name" maxlength="50" style="flex:1;"/>
<button type="button" class="cai-btn cai-reset" id="cai-avatar-reset-name" title="Clear name" style="margin-right:0;"><span class="material-symbols-outlined">cached</span></button>
</div>
</div>
<div id="cai-avatar-preview-wrap">
<img id="cai-avatar-preview" draggable="false">
<span id="cai-avatar-placeholder">No image</span>
</div>
<div class="cai-field">
<label>Paste the link of the image here:</label>
<input type="text" id="cai-avatar-url" placeholder="https://example.com/image.jpg"/>
</div>
<div class="cai-field">
<label>Brightness: <span id="cai-avatar-bright-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-avatar-bright" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>Contrast: <span id="cai-avatar-contrast-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-avatar-contrast" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>Saturation: <span id="cai-avatar-sat-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-avatar-sat" min="0" max="300" value="100"/>
</div>
<div class="cai-field">
<label>Zoom: <span id="cai-avatar-zoom-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-avatar-zoom" min="100" max="300" value="100"/>
</div>
<button type="button" class="cai-btn cai-reset" id="cai-avatar-reset-all" style="align-self:flex-start;">Reset avatar & name</button>
</div>
<!-- USER PANEL -->
<div id="cai-avatar-user-wrap" style="display:flex; flex-direction:column; gap:16px;">
<div class="cai-field">
<label>User custom name (max 50 chars)</label>
<div class="cai-row" style="gap:10px; align-items:center;">
<input type="text" id="cai-uavatar-name" placeholder="Custom User Name" maxlength="50" style="flex:1;"/>
<button type="button" class="cai-btn cai-reset" id="cai-uavatar-reset-name" title="Clear name" style="margin-right:0;"><span class="material-symbols-outlined">cached</span></button>
</div>
</div>
<div id="cai-uavatar-preview-wrap">
<img id="cai-uavatar-preview" draggable="false">
<span id="cai-uavatar-placeholder">No image</span>
</div>
<div class="cai-field">
<label>Paste the link of the image here:</label>
<input type="text" id="cai-uavatar-url" placeholder="https://example.com/image.jpg"/>
</div>
<div class="cai-field">
<label>Brightness: <span id="cai-uavatar-bright-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-uavatar-bright" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>Contrast: <span id="cai-uavatar-contrast-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-uavatar-contrast" min="0" max="200" value="100"/>
</div>
<div class="cai-field">
<label>Saturation: <span id="cai-uavatar-sat-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-uavatar-sat" min="0" max="300" value="100"/>
</div>
<div class="cai-field">
<label>Zoom: <span id="cai-uavatar-zoom-val" style="color:#fff">100%</span></label>
<input type="range" id="cai-uavatar-zoom" min="100" max="300" value="100"/>
</div>
<button type="button" class="cai-btn cai-reset" id="cai-uavatar-reset-all" style="align-self:flex-start;">Reset avatar & name</button>
</div>
</div>
</div>
<!-- SVD. PRESETS TAB -->
<div class="cai-panel" id="cai-tab-svd-presets">
<div style="width:100%; display:flex; flex-direction:column; gap:20px;">
<div style="display:flex; justify-content:center; align-items:flex-start; gap:20px; flex-wrap:wrap;">
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="cai-clabel" style="margin-top:0;">Load Preset</div>
<button type="button" class="cai-preset-iconbtn cai-preset-iconbtn-wide" id="cai-preset-load" title="Load"><span class="material-icons-outlined">upload_file</span></button>
</div>
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="cai-clabel" style="margin-top:0;">Save Preset</div>
<button type="button" class="cai-preset-iconbtn cai-preset-iconbtn-wide" id="cai-preset-save" title="Save"><span class="material-icons-outlined">save</span></button>
</div>
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="cai-clabel" style="margin-top:0;">Delete all</div>
<button type="button" class="cai-preset-iconbtn cai-preset-iconbtn-wide" id="cai-preset-delete-all" title="Delete all"><span class="material-icons-outlined">delete_forever</span></button>
</div>
</div>
<input type="file" id="cai-preset-file-input" accept=".json,application/json" style="display:none;"/>
<div style="display:flex; flex-direction:column; gap:8px;">
<div class="cai-clabel" style="margin-top:0;">Saved Presets</div>
<div id="cai-preset-list" class="cai-preset-list"></div>
</div>
</div>
</div>
</div>
<div id="cai-mfooter">
<button class="cai-btn cai-reset" id="cai-reset">Reset to default</button>
<button class="cai-btn cai-ghost" id="cai-cancel">Cancel</button>
<button class="cai-btn cai-primary" id="cai-save">Save</button>
</div>
</div>`;
document.body.appendChild(modal);
Coloris({
el: '.coloris',
theme: 'default',
themeMode: 'dark',
formatToggle: true,
alpha: true
});
const $ = id => modal.querySelector('#' + id);
const closeBtn = $('cai-mclose'), cancelBtn = $('cai-cancel'), saveBtn = $('cai-save'), resetBtn = $('cai-reset');
const blist = $('cai-blist');
const previewAI = $('cai-pai'), previewUser = $('cai-puser');
const fontInp = $('cai-font');
const fsDec = $('cai-fs-dec'), fsVal = $('cai-fs-val'), fsInc = $('cai-fs-inc');
const lhInp = $('cai-lh'), lhVal = $('cai-lh-val');
const lsInp = $('cai-ls'), lsVal = $('cai-ls-val');
const blurToggle = $('cai-blur-toggle'), blurSliderWrap = $('cai-blur-slider-wrap');
const blurWrap = $('cai-blur-wrap');
const blurAmountInp = $('cai-blur-amount'), blurVal = $('cai-blur-val');
const spSec = $('cai-spsec'), spLabel = $('cai-splabel'), spInp = $('cai-spinp');
const aiBgInp = $('cai-ai-bg'), aiTxInp = $('cai-ai-tx');
const uBgInp = $('cai-u-bg'), uTxInp = $('cai-u-tx');
const bgLockedNote = $('cai-bg-locked-note'), bgPickers = $('cai-bg-pickers');
const eSec = $('cai-esec'), eGrid = $('cai-egrid');
const colorBlock = $('cai-colorblock');
const rpanelNode = $('cai-rpanel');
const rpanelHosts = {
bubble: modal.querySelector('#cai-tab-bubble'),
'adv-bubble': modal.querySelector('#cai-tab-adv-bubble'),
font: modal.querySelector('#cai-tab-font')
};
function moveRpanelTo(tab) { const h = rpanelHosts[tab]; if (h) h.appendChild(rpanelNode); }
const advTabBtn = modal.querySelector('.cai-tab[data-tab="adv-bubble"]');
const mspacingInp = $('cai-mspacing'), mspacingVal = $('cai-mspacing-val');
function updateAdvBubbleTabVisibility(id) {
const def = BUBBLES.find(b => b.id === id);
const isDefaultTheme = !!(def && def.isDefault);
advTabBtn.style.display = isDefaultTheme ? '' : 'none';
if (!isDefaultTheme && advTabBtn.classList.contains('active')) {
advTabBtn.classList.remove('active');
modal.querySelectorAll('.cai-panel').forEach(x => x.classList.remove('active'));
modal.querySelector('.cai-tab[data-tab="bubble"]').classList.add('active');
modal.querySelector('#cai-tab-bubble').classList.add('active');
moveRpanelTo('bubble');
}
}
const eInput = $('cai-einput'), eAdd = $('cai-eadd');
const angleSlider = $('cai-angle'), angleVal = $('cai-aval');
const alphaSlider = $('cai-alpha'), alphaValEl = $('cai-alphaval');
const bgUrlWrap = $('cai-bgurl-wrap'), bgUrl = $('cai-bgurl');
const bgFitWrap = $('cai-bgfit-wrap');
const bgFitCoverInp = $('cai-bgfit-cover'), bgFitContainInp = $('cai-bgfit-contain'), bgFitStretchInp = $('cai-bgfit-stretch');
function getBgFit() { return bgFitContainInp.checked ? 'contain' : (bgFitStretchInp.checked ? 'stretch' : 'cover'); }
const bgPreview = $('cai-bgpreview'), bgImg = $('cai-bgimg');
const bgRpanel = $('cai-bgrpanel');
const bgModeBtns = modal.querySelectorAll('#cai-bgmode-btns .cai-bgmode-btn');
const bgNoneWrap = $('cai-bgnone-wrap'), bgColorInp = $('cai-bgcolor'), bgColorDefaultBtn = $('cai-bgcolor-default');
const bgControls = $('cai-bg-controls');
const bgOpacityInp = $('cai-bg-opacity'), bgBrightInp = $('cai-bg-bright');
const bgOpacityVal = $('cai-bg-opacity-val'), bgBrightVal = $('cai-bg-bright-val');
const bgContrastInp = $('cai-bg-contrast'), bgContrastVal = $('cai-bg-contrast-val');
const bgSaturationInp = $('cai-bg-sat'), bgSaturationVal = $('cai-bg-sat-val');
const bgBlurInp = $('cai-bg-blur'), bgBlurVal = $('cai-bg-blur-val');
const avatarSizeInp = $('cai-avatar-size'), avatarSizeVal = $('cai-avatar-size-val');
const avatarForceMobileInp = $('cai-avatar-force-mobile');
const hideAvatarsDesktopInp = $('cai-hide-avatars-desktop');
const hideNamesInp = $('cai-hide-names');
const replaceAuthorMobileInp = $('cai-replace-author-mobile');
const avatarModeBtns = modal.querySelectorAll('#cai-avatarmode-btns .cai-bgmode-btn');
const avatarBotWrap = $('cai-avatar-bot-wrap'), avatarUserWrap = $('cai-avatar-user-wrap');
const avatarNameInp = $('cai-avatar-name'), avatarResetNameBtn = $('cai-avatar-reset-name');
const avatarUrlInp = $('cai-avatar-url');
const avatarPreviewImg = $('cai-avatar-preview'), avatarPlaceholder = $('cai-avatar-placeholder');
const avatarBrightInp = $('cai-avatar-bright'), avatarContrastInp = $('cai-avatar-contrast'), avatarSatInp = $('cai-avatar-sat'), avatarZoomInp = $('cai-avatar-zoom');
const avatarBrightVal = $('cai-avatar-bright-val'), avatarContrastVal = $('cai-avatar-contrast-val'), avatarSatVal = $('cai-avatar-sat-val'), avatarZoomVal = $('cai-avatar-zoom-val');
const avatarResetAllBtn = $('cai-avatar-reset-all');
const avatarActiveBotId = getActiveBotId();
let avatarPosX = 50, avatarPosY = 50;
let avatarPanX = 0, avatarPanY = 0;
let isDraggingAvatarPreview = false, aStartX, aStartY, aStartPosX, aStartPosY, aStartPanX, aStartPanY;
const avatarUserNameInp = $('cai-uavatar-name'), avatarUserResetNameBtn = $('cai-uavatar-reset-name');
const avatarUserUrlInp = $('cai-uavatar-url');
const avatarUserPreviewImg = $('cai-uavatar-preview'), avatarUserPlaceholder = $('cai-uavatar-placeholder');
const avatarUserBrightInp = $('cai-uavatar-bright'), avatarUserContrastInp = $('cai-uavatar-contrast'), avatarUserSatInp = $('cai-uavatar-sat'), avatarUserZoomInp = $('cai-uavatar-zoom');
const avatarUserBrightVal = $('cai-uavatar-bright-val'), avatarUserContrastVal = $('cai-uavatar-contrast-val'), avatarUserSatVal = $('cai-uavatar-sat-val'), avatarUserZoomVal = $('cai-uavatar-zoom-val');
const avatarUserResetAllBtn = $('cai-uavatar-reset-all');
let avatarUserPosX = 50, avatarUserPosY = 50;
let avatarUserPanX = 0, avatarUserPanY = 0;
let isDraggingAvatarUserPreview = false, uStartX, uStartY, uStartPosX, uStartPosY, uStartPanX, uStartPanY;
let selId = tempCfg.bubbleId ?? 0;
let customEmojis = tempCfg.customEmoji ? [...tempCfg.customEmoji] : [];
let currentBgMode = tempCfg.bgType || 'none';
// TABS
modal.querySelectorAll('.cai-tab').forEach(t => t.addEventListener('click', () => {
modal.querySelectorAll('.cai-tab').forEach(x => x.classList.remove('active'));
modal.querySelectorAll('.cai-panel').forEach(x => x.classList.remove('active'));
t.classList.add('active');
modal.querySelector('#cai-tab-' + t.dataset.tab).classList.add('active');
moveRpanelTo(t.dataset.tab);
}));
// BG MODE BUTTONS
function setBgMode(mode) {
currentBgMode = mode;
bgModeBtns.forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
bgNoneWrap.style.display = mode === 'none' ? 'flex' : 'none';
bgUrlWrap.style.display = mode === 'image' ? 'flex' : 'none';
bgFitWrap.style.display = mode === 'image' ? 'flex' : 'none';
bgControls.style.display = mode === 'image' ? 'flex' : 'none';
bgRpanel.style.display = mode === 'image' ? 'flex' : 'none';
if (mode === 'image') showBgPreview(mode, bgUrl.value);
else bgPreview.style.display = 'none';
}
bgModeBtns.forEach(b => b.addEventListener('click', () => setBgMode(b.dataset.mode)));
// ─── AVATAR MODE TOGGLE ───
function setAvatarMode(mode) {
avatarModeBtns.forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
avatarBotWrap.style.display = mode === 'bot' ? 'flex' : 'none';
avatarUserWrap.style.display = mode === 'user' ? 'flex' : 'none';
}
avatarModeBtns.forEach(b => b.addEventListener('click', () => setAvatarMode(b.dataset.mode)));
setAvatarMode('bot');
// ─── PREVIEW FILTERS ───
function applyAvatarPreviewFilters() {
avatarBrightVal.textContent = avatarBrightInp.value + '%';
avatarContrastVal.textContent = avatarContrastInp.value + '%';
avatarZoomVal.textContent = avatarZoomInp.value + '%';
avatarSatVal.textContent = avatarSatInp.value + '%';
avatarPreviewImg.style.filter = `brightness(${avatarBrightInp.value}%) contrast(${avatarContrastInp.value}%) saturate(${avatarSatInp.value}%)`;
const scale = parseInt(avatarZoomInp.value) / 100;
if (scale > 1) {
const maxPan = 50 * (1 - 1 / scale);
avatarPanX = Math.max(-maxPan, Math.min(maxPan, avatarPanX));
avatarPanY = Math.max(-maxPan, Math.min(maxPan, avatarPanY));
} else { avatarPanX = 0; avatarPanY = 0; }
avatarPreviewImg.style.transform = `scale(${scale}) translate(${avatarPanX}%, ${avatarPanY}%)`;
}
function refreshAvatarPreview(url, posX = 50, posY = 50, brightness = 100, contrast = 100, saturation = 100, zoom = 100, panX = 0, panY = 0) {
if (url) {
avatarPreviewImg.src = url;
avatarPreviewImg.style.display = 'block';
avatarPlaceholder.style.display = 'none';
avatarPosX = posX; avatarPosY = posY; avatarPanX = panX; avatarPanY = panY;
avatarPreviewImg.style.objectPosition = `${avatarPosX}% ${avatarPosY}%`;
avatarBrightInp.value = brightness; avatarContrastInp.value = contrast; avatarSatInp.value = saturation; avatarZoomInp.value = zoom;
applyAvatarPreviewFilters();
} else {
avatarPreviewImg.src = ''; avatarPreviewImg.style.display = 'none';
avatarPlaceholder.style.display = 'block'; avatarPlaceholder.textContent = 'No image';
avatarPosX = 50; avatarPosY = 50; avatarPanX = 0; avatarPanY = 0;
avatarBrightInp.value = 100; avatarContrastInp.value = 100; avatarSatInp.value = 100; avatarZoomInp.value = 100;
applyAvatarPreviewFilters();
}
}
avatarPreviewImg.onerror = () => {
avatarPreviewImg.style.display = 'none';
avatarPlaceholder.style.display = 'flex';
avatarPlaceholder.style.flexDirection = 'column';
avatarPlaceholder.style.justifyContent = 'center';
avatarPlaceholder.style.alignItems = 'center';
avatarPlaceholder.style.textAlign = 'center';
avatarPlaceholder.innerHTML = 'Broken link /<br>No valid <br>Drag to frame';
};
avatarUrlInp.addEventListener('input', () => refreshAvatarPreview(avatarUrlInp.value.trim(), 50, 50, 100, 100, 100, 100, 0, 0));
avatarBrightInp.addEventListener('input', applyAvatarPreviewFilters);
avatarContrastInp.addEventListener('input', applyAvatarPreviewFilters);
avatarSatInp.addEventListener('input', applyAvatarPreviewFilters);
avatarZoomInp.addEventListener('input', applyAvatarPreviewFilters);
// ─── DRAG (mouse) ───
avatarPreviewImg.addEventListener('mousedown', (e) => {
if (!avatarPreviewImg.src || avatarPreviewImg.style.display === 'none') return;
isDraggingAvatarPreview = true;
aStartX = e.clientX; aStartY = e.clientY;
aStartPosX = avatarPosX; aStartPosY = avatarPosY; aStartPanX = avatarPanX; aStartPanY = avatarPanY;
e.preventDefault();
});
function handleAvatarMouseMove(e) {
if (!isDraggingAvatarPreview) return;
const deltaX = e.clientX - aStartX, deltaY = e.clientY - aStartY;
const scale = parseInt(avatarZoomInp.value) / 100;
if (scale === 1) {
avatarPosX = Math.max(0, Math.min(100, aStartPosX - (deltaX / 120) * 100));
avatarPosY = Math.max(0, Math.min(100, aStartPosY - (deltaY / 120) * 100));
avatarPreviewImg.style.objectPosition = `${avatarPosX}% ${avatarPosY}%`;
} else {
const maxPan = 50 * (1 - 1 / scale);
avatarPanX = Math.max(-maxPan, Math.min(maxPan, aStartPanX + ((deltaX / 120) * 100) / scale));
avatarPanY = Math.max(-maxPan, Math.min(maxPan, aStartPanY + ((deltaY / 120) * 100) / scale));
avatarPreviewImg.style.transform = `scale(${scale}) translate(${avatarPanX}%, ${avatarPanY}%)`;
}
}
function handleAvatarMouseUp() { isDraggingAvatarPreview = false; }
document.addEventListener('mousemove', handleAvatarMouseMove);
document.addEventListener('mouseup', handleAvatarMouseUp);
// ─── DRAG (touch) ───
function handleAvatarTouchMove(e) {
if (!isDraggingAvatarPreview) return;
const touch = e.touches[0];
const deltaX = touch.clientX - aStartX, deltaY = touch.clientY - aStartY;
const scale = parseInt(avatarZoomInp.value) / 100;
if (scale === 1) {
avatarPosX = Math.max(0, Math.min(100, aStartPosX - (deltaX / 120) * 100));
avatarPosY = Math.max(0, Math.min(100, aStartPosY - (deltaY / 120) * 100));
avatarPreviewImg.style.objectPosition = `${avatarPosX}% ${avatarPosY}%`;
} else {
const maxPan = 50 * (1 - 1 / scale);
avatarPanX = Math.max(-maxPan, Math.min(maxPan, aStartPanX + ((deltaX / 120) * 100) / scale));
avatarPanY = Math.max(-maxPan, Math.min(maxPan, aStartPanY + ((deltaY / 120) * 100) / scale));
avatarPreviewImg.style.transform = `scale(${scale}) translate(${avatarPanX}%, ${avatarPanY}%)`;
}
e.preventDefault();
}
function handleAvatarTouchEnd() {
isDraggingAvatarPreview = false;
document.removeEventListener('touchmove', handleAvatarTouchMove);
document.removeEventListener('touchend', handleAvatarTouchEnd);
}
avatarPreviewImg.addEventListener('touchstart', (e) => {
if (!avatarPreviewImg.src || avatarPreviewImg.style.display === 'none') return;
isDraggingAvatarPreview = true;
const touch = e.touches[0];
aStartX = touch.clientX; aStartY = touch.clientY;
aStartPosX = avatarPosX; aStartPosY = avatarPosY; aStartPanX = avatarPanX; aStartPanY = avatarPanY;
document.addEventListener('touchmove', handleAvatarTouchMove, { passive: false });
document.addEventListener('touchend', handleAvatarTouchEnd);
e.preventDefault();
}, { passive: false });
// ─── PREVIEW FILTERS (USER) ───
function applyAvatarUserPreviewFilters() {
avatarUserBrightVal.textContent = avatarUserBrightInp.value + '%';
avatarUserContrastVal.textContent = avatarUserContrastInp.value + '%';
avatarUserZoomVal.textContent = avatarUserZoomInp.value + '%';
avatarUserSatVal.textContent = avatarUserSatInp.value + '%';
avatarUserPreviewImg.style.filter = `brightness(${avatarUserBrightInp.value}%) contrast(${avatarUserContrastInp.value}%) saturate(${avatarUserSatInp.value}%)`;
const scale = parseInt(avatarUserZoomInp.value) / 100;
if (scale > 1) {
const maxPan = 50 * (1 - 1 / scale);
avatarUserPanX = Math.max(-maxPan, Math.min(maxPan, avatarUserPanX));
avatarUserPanY = Math.max(-maxPan, Math.min(maxPan, avatarUserPanY));
} else { avatarUserPanX = 0; avatarUserPanY = 0; }
avatarUserPreviewImg.style.transform = `scale(${scale}) translate(${avatarUserPanX}%, ${avatarUserPanY}%)`;
}
function refreshAvatarUserPreview(url, posX = 50, posY = 50, brightness = 100, contrast = 100, saturation = 100, zoom = 100, panX = 0, panY = 0) {
if (url) {
avatarUserPreviewImg.src = url;
avatarUserPreviewImg.style.display = 'block';
avatarUserPlaceholder.style.display = 'none';
avatarUserPosX = posX; avatarUserPosY = posY; avatarUserPanX = panX; avatarUserPanY = panY;
avatarUserPreviewImg.style.objectPosition = `${avatarUserPosX}% ${avatarUserPosY}%`;
avatarUserBrightInp.value = brightness; avatarUserContrastInp.value = contrast; avatarUserSatInp.value = saturation; avatarUserZoomInp.value = zoom;
applyAvatarUserPreviewFilters();
} else {
avatarUserPreviewImg.src = ''; avatarUserPreviewImg.style.display = 'none';
avatarUserPlaceholder.style.display = 'block'; avatarUserPlaceholder.textContent = 'No image';
avatarUserPosX = 50; avatarUserPosY = 50; avatarUserPanX = 0; avatarUserPanY = 0;
avatarUserBrightInp.value = 100; avatarUserContrastInp.value = 100; avatarUserSatInp.value = 100; avatarUserZoomInp.value = 100;
applyAvatarUserPreviewFilters();
}
}
avatarUserPreviewImg.onerror = () => {
avatarUserPreviewImg.style.display = 'none';
avatarUserPlaceholder.style.display = 'flex';
avatarUserPlaceholder.style.flexDirection = 'column';
avatarUserPlaceholder.style.justifyContent = 'center';
avatarUserPlaceholder.style.alignItems = 'center';
avatarUserPlaceholder.style.textAlign = 'center';
avatarUserPlaceholder.innerHTML = 'Broken link /<br>No valid <br>Drag to frame';
};
avatarUserUrlInp.addEventListener('input', () => refreshAvatarUserPreview(avatarUserUrlInp.value.trim(), 50, 50, 100, 100, 100, 100, 0, 0));
avatarUserBrightInp.addEventListener('input', applyAvatarUserPreviewFilters);
avatarUserContrastInp.addEventListener('input', applyAvatarUserPreviewFilters);
avatarUserSatInp.addEventListener('input', applyAvatarUserPreviewFilters);
avatarUserZoomInp.addEventListener('input', applyAvatarUserPreviewFilters);
// ─── DRAG (mouse, USER) ───
avatarUserPreviewImg.addEventListener('mousedown', (e) => {
if (!avatarUserPreviewImg.src || avatarUserPreviewImg.style.display === 'none') return;
isDraggingAvatarUserPreview = true;
uStartX = e.clientX; uStartY = e.clientY;
uStartPosX = avatarUserPosX; uStartPosY = avatarUserPosY; uStartPanX = avatarUserPanX; uStartPanY = avatarUserPanY;
e.preventDefault();
});
function handleAvatarUserMouseMove(e) {
if (!isDraggingAvatarUserPreview) return;
const deltaX = e.clientX - uStartX, deltaY = e.clientY - uStartY;
const scale = parseInt(avatarUserZoomInp.value) / 100;
if (scale === 1) {
avatarUserPosX = Math.max(0, Math.min(100, uStartPosX - (deltaX / 120) * 100));
avatarUserPosY = Math.max(0, Math.min(100, uStartPosY - (deltaY / 120) * 100));
avatarUserPreviewImg.style.objectPosition = `${avatarUserPosX}% ${avatarUserPosY}%`;
} else {
const maxPan = 50 * (1 - 1 / scale);
avatarUserPanX = Math.max(-maxPan, Math.min(maxPan, uStartPanX + ((deltaX / 120) * 100) / scale));
avatarUserPanY = Math.max(-maxPan, Math.min(maxPan, uStartPanY + ((deltaY / 120) * 100) / scale));
avatarUserPreviewImg.style.transform = `scale(${scale}) translate(${avatarUserPanX}%, ${avatarUserPanY}%)`;
}
}
function handleAvatarUserMouseUp() { isDraggingAvatarUserPreview = false; }
document.addEventListener('mousemove', handleAvatarUserMouseMove);
document.addEventListener('mouseup', handleAvatarUserMouseUp);
// ─── DRAG (touch, USER) ───
function handleAvatarUserTouchMove(e) {
if (!isDraggingAvatarUserPreview) return;
const touch = e.touches[0];
const deltaX = touch.clientX - uStartX, deltaY = touch.clientY - uStartY;
const scale = parseInt(avatarUserZoomInp.value) / 100;
if (scale === 1) {
avatarUserPosX = Math.max(0, Math.min(100, uStartPosX - (deltaX / 120) * 100));
avatarUserPosY = Math.max(0, Math.min(100, uStartPosY - (deltaY / 120) * 100));
avatarUserPreviewImg.style.objectPosition = `${avatarUserPosX}% ${avatarUserPosY}%`;
} else {
const maxPan = 50 * (1 - 1 / scale);
avatarUserPanX = Math.max(-maxPan, Math.min(maxPan, uStartPanX + ((deltaX / 120) * 100) / scale));
avatarUserPanY = Math.max(-maxPan, Math.min(maxPan, uStartPanY + ((deltaY / 120) * 100) / scale));
avatarUserPreviewImg.style.transform = `scale(${scale}) translate(${avatarUserPanX}%, ${avatarUserPanY}%)`;
}
e.preventDefault();
}
function handleAvatarUserTouchEnd() {
isDraggingAvatarUserPreview = false;
document.removeEventListener('touchmove', handleAvatarUserTouchMove);
document.removeEventListener('touchend', handleAvatarUserTouchEnd);
}
avatarUserPreviewImg.addEventListener('touchstart', (e) => {
if (!avatarUserPreviewImg.src || avatarUserPreviewImg.style.display === 'none') return;
isDraggingAvatarUserPreview = true;
const touch = e.touches[0];
uStartX = touch.clientX; uStartY = touch.clientY;
uStartPosX = avatarUserPosX; uStartPosY = avatarUserPosY; uStartPanX = avatarUserPanX; uStartPanY = avatarUserPanY;
document.addEventListener('touchmove', handleAvatarUserTouchMove, { passive: false });
document.addEventListener('touchend', handleAvatarUserTouchEnd);
e.preventDefault();
}, { passive: false });
avatarResetNameBtn.onclick = () => { avatarNameInp.value = ''; };
// ─── LOAD SAVED CONFIG ACTIVE BOT ───
if (avatarActiveBotId) {
avatarNameInp.value = localStorage.getItem('customBotNameConfig_' + avatarActiveBotId) || '';
const savedAvatarCfg = getBotConfig(avatarActiveBotId);
if (savedAvatarCfg) {
avatarUrlInp.value = savedAvatarCfg.url;
refreshAvatarPreview(savedAvatarCfg.url, savedAvatarCfg.posX, savedAvatarCfg.posY, savedAvatarCfg.brightness, savedAvatarCfg.contrast, savedAvatarCfg.saturation || 100, savedAvatarCfg.zoom || 100, savedAvatarCfg.panX || 0, savedAvatarCfg.panY || 0);
} else {
refreshAvatarPreview('');
}
}
avatarResetAllBtn.onclick = () => {
if (!avatarActiveBotId) return;
localStorage.removeItem('customBotAvatarConfig_' + avatarActiveBotId);
localStorage.removeItem('customBotNameConfig_' + avatarActiveBotId);
botConfigCache.delete(avatarActiveBotId);
const linkedCharId = localStorage.getItem('charId_for_' + avatarActiveBotId);
if (linkedCharId) {
localStorage.removeItem('customBotAvatarConfig_' + linkedCharId);
localStorage.removeItem('customBotNameConfig_' + linkedCharId);
localStorage.removeItem('charId_for_' + avatarActiveBotId);
botConfigCache.delete(linkedCharId);
}
avatarNameInp.value = ''; avatarUrlInp.value = '';
refreshAvatarPreview('');
triggerReplacementSafely();
caiToast('Avatar reset!');
};
// ─── LOAD SAVED CONFIG (USER, per chat) ───
avatarUserResetNameBtn.onclick = () => { avatarUserNameInp.value = ''; };
if (avatarActiveBotId) {
avatarUserNameInp.value = localStorage.getItem('customUserNameConfig_' + avatarActiveBotId) || '';
const savedUserCfg = getUserConfig(avatarActiveBotId);
if (savedUserCfg) {
avatarUserUrlInp.value = savedUserCfg.url;
refreshAvatarUserPreview(savedUserCfg.url, savedUserCfg.posX, savedUserCfg.posY, savedUserCfg.brightness, savedUserCfg.contrast, savedUserCfg.saturation || 100, savedUserCfg.zoom || 100, savedUserCfg.panX || 0, savedUserCfg.panY || 0);
} else {
refreshAvatarUserPreview('');
}
}
avatarUserResetAllBtn.onclick = () => {
if (!avatarActiveBotId) return;
localStorage.removeItem('customUserAvatarConfig_' + avatarActiveBotId);
localStorage.removeItem('customUserNameConfig_' + avatarActiveBotId);
userConfigCache.delete(avatarActiveBotId);
const linkedCharId = localStorage.getItem('charId_for_' + avatarActiveBotId);
if (linkedCharId) {
localStorage.removeItem('customUserAvatarConfig_' + linkedCharId);
localStorage.removeItem('customUserNameConfig_' + linkedCharId);
userConfigCache.delete(linkedCharId);
}
avatarUserNameInp.value = ''; avatarUserUrlInp.value = '';
refreshAvatarUserPreview('');
triggerReplacementSafely();
caiToast('User avatar reset!');
};
bgColorDefaultBtn.addEventListener('click', () => {
bgColorInp.value = '';
bgColorInp.dispatchEvent(new Event('input', { bubbles: true }));
});
if (tempCfg.bgColor) bgColorInp.value = tempCfg.bgColor;
bgColorInp.dispatchEvent(new Event('input', { bubbles: true }));
if (tempCfg.bgUrl) bgUrl.value = tempCfg.bgUrl;
({ contain: bgFitContainInp, stretch: bgFitStretchInp }[tempCfg.bgFit] || bgFitCoverInp).checked = true;
if (tempCfg.bgOpacity !== undefined) bgOpacityInp.value = Math.round(tempCfg.bgOpacity * 100);
if (tempCfg.bgBrightness !== undefined) bgBrightInp.value = Math.round(tempCfg.bgBrightness * 100);
if (tempCfg.bgContrast !== undefined) bgContrastInp.value = Math.round(tempCfg.bgContrast * 100);
if (tempCfg.bgSaturation !== undefined) bgSaturationInp.value = Math.round(tempCfg.bgSaturation * 100);
bgOpacityVal.textContent = bgOpacityInp.value + '%';
bgBrightVal.textContent = bgBrightInp.value + '%';
bgContrastVal.textContent = bgContrastInp.value + '%';
bgSaturationVal.textContent = bgSaturationInp.value + '%';
setBgMode(currentBgMode);
if (tempCfg.bgBlur !== undefined) bgBlurInp.value = tempCfg.bgBlur;
bgBlurVal.textContent = bgBlurInp.value + 'px';
updateBgPreviewStyle();
bgUrl.addEventListener('input', () => showBgPreview(currentBgMode, bgUrl.value));
[bgFitCoverInp, bgFitContainInp, bgFitStretchInp].forEach(inp => inp.addEventListener('change', () => {
tempCfg.bgFit = getBgFit();
updateBgPreviewStyle();
}));
bgOpacityInp.addEventListener('input', () => { bgOpacityVal.textContent = bgOpacityInp.value + '%'; updateBgPreviewStyle(); });
bgBrightInp.addEventListener('input', () => { bgBrightVal.textContent = bgBrightInp.value + '%'; updateBgPreviewStyle(); });
bgContrastInp.addEventListener('input', () => { bgContrastVal.textContent = bgContrastInp.value + '%'; updateBgPreviewStyle(); });
bgSaturationInp.addEventListener('input', () => { bgSaturationVal.textContent = bgSaturationInp.value + '%'; updateBgPreviewStyle(); });
bgBlurInp.addEventListener('input', () => { bgBlurVal.textContent = bgBlurInp.value + 'px'; updateBgPreviewStyle(); });
// ─── LINE HEIGHT ───
lhInp.value = tempCfg.lineHeight !== undefined ? clampLineHeight(tempCfg.lineHeight) : DEFAULT_LINE_HEIGHT;
lhVal.textContent = (+lhInp.value).toFixed(1);
lhInp.addEventListener('input', () => {
lhVal.textContent = (+lhInp.value).toFixed(1);
tempCfg.lineHeight = clampLineHeight(lhInp.value);
scheduleUpdatePreview();
});
// ─── LETTER SPACING ───
lsInp.value = tempCfg.letterSpacing !== undefined ? clampLetterSpacing(tempCfg.letterSpacing) : DEFAULT_LETTER_SPACING;
lsVal.textContent = (+lsInp.value).toFixed(1) + 'px';
lsInp.addEventListener('input', () => {
lsVal.textContent = (+lsInp.value).toFixed(1) + 'px';
tempCfg.letterSpacing = clampLetterSpacing(lsInp.value);
scheduleUpdatePreview();
});
// ─── TEXT ALIGN ───
const talignLeftInp = $('cai-talign-left'), talignCenterInp = $('cai-talign-center'), talignRightInp = $('cai-talign-right');
({ center: talignCenterInp, right: talignRightInp }[tempCfg.textAlign] || talignLeftInp).checked = true;
talignLeftInp.addEventListener('change', () => { if (talignLeftInp.checked) { tempCfg.textAlign = 'left'; scheduleUpdatePreview(); } });
talignCenterInp.addEventListener('change', () => { if (talignCenterInp.checked) { tempCfg.textAlign = 'center'; scheduleUpdatePreview(); } });
talignRightInp.addEventListener('change', () => { if (talignRightInp.checked) { tempCfg.textAlign = 'right'; scheduleUpdatePreview(); } });
// ─── FONT SCOPE (Dual / Individual) ───
const fontScopeSel = $('cai-font-scope');
const fontDualWrap = $('cai-font-dual-wrap'), fontIndividualWrap = $('cai-font-individual-wrap');
const fontAiInp = $('cai-font-ai'), fontUserInp = $('cai-font-user');
const fsAiDec = $('cai-fs-ai-dec'), fsAiVal = $('cai-fs-ai-val'), fsAiInc = $('cai-fs-ai-inc');
const fsUserDec = $('cai-fs-user-dec'), fsUserVal = $('cai-fs-user-val'), fsUserInc = $('cai-fs-user-inc');
const lhAiInp = $('cai-lh-ai'), lhAiVal = $('cai-lh-ai-val');
const lhUserInp = $('cai-lh-user'), lhUserVal = $('cai-lh-user-val');
const lsAiInp = $('cai-ls-ai'), lsAiVal = $('cai-ls-ai-val');
const lsUserInp = $('cai-ls-user'), lsUserVal = $('cai-ls-user-val');
const talignAiLeftInp = $('cai-talign-ai-left'), talignAiCenterInp = $('cai-talign-ai-center'), talignAiRightInp = $('cai-talign-ai-right');
const talignUserLeftInp = $('cai-talign-user-left'), talignUserCenterInp = $('cai-talign-user-center'), talignUserRightInp = $('cai-talign-user-right');
const enterKeyNewlineInp = $('cai-enterkey-newline'), enterKeySendInp = $('cai-enterkey-send');
const draftToggle = $('cai-msg-draft');
draftToggle.checked = isDraftEnabled();
const hideDistractorsToggle = $('cai-hide-distractors');
hideDistractorsToggle.checked = isHideDistractorsEnabled();
hideDistractorsToggle.addEventListener('change', () => {
GM_setValue(HIDE_DISTRACTORS_KEY, hideDistractorsToggle.checked);
applyHideDistractors();
});
fontScopeSel.value = tempCfg.fontScope === 'individual' ? 'individual' : 'dual';
fontDualWrap.style.display = fontScopeSel.value === 'individual' ? 'none' : 'flex';
fontIndividualWrap.style.display = fontScopeSel.value === 'individual' ? 'flex' : 'none';
fontScopeSel.addEventListener('change', () => {
tempCfg.fontScope = fontScopeSel.value;
fontDualWrap.style.display = fontScopeSel.value === 'individual' ? 'none' : 'flex';
fontIndividualWrap.style.display = fontScopeSel.value === 'individual' ? 'flex' : 'none';
scheduleUpdatePreview();
});
fontAiInp.value = tempCfg.fontAi || '';
fontUserInp.value = tempCfg.fontUser || '';
fontAiInp.addEventListener('input', () => { tempCfg.fontAi = fontAiInp.value; scheduleUpdatePreview(); });
fontUserInp.addEventListener('input', () => { tempCfg.fontUser = fontUserInp.value; scheduleUpdatePreview(); });
fsAiVal.value = tempCfg.fontSizeAi || DEFAULT_FONT_SIZE;
fsUserVal.value = tempCfg.fontSizeUser || DEFAULT_FONT_SIZE;
fsAiDec.addEventListener('click', () => { fsAiVal.value = clampFontSize((+fsAiVal.value || DEFAULT_FONT_SIZE) - 1); fsAiVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsAiInc.addEventListener('click', () => { fsAiVal.value = clampFontSize((+fsAiVal.value || DEFAULT_FONT_SIZE) + 1); fsAiVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsAiVal.addEventListener('keydown', e => { if (e.key === 'Enter') fsAiVal.blur(); });
fsAiVal.addEventListener('change', () => { fsAiVal.value = clampFontSize(fsAiVal.value); fsAiVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsAiVal.addEventListener('input', () => { tempCfg.fontSizeAi = clampFontSize(fsAiVal.value); scheduleUpdatePreview(); });
fsUserDec.addEventListener('click', () => { fsUserVal.value = clampFontSize((+fsUserVal.value || DEFAULT_FONT_SIZE) - 1); fsUserVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsUserInc.addEventListener('click', () => { fsUserVal.value = clampFontSize((+fsUserVal.value || DEFAULT_FONT_SIZE) + 1); fsUserVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsUserVal.addEventListener('keydown', e => { if (e.key === 'Enter') fsUserVal.blur(); });
fsUserVal.addEventListener('change', () => { fsUserVal.value = clampFontSize(fsUserVal.value); fsUserVal.dispatchEvent(new Event('input', { bubbles: true })); });
fsUserVal.addEventListener('input', () => { tempCfg.fontSizeUser = clampFontSize(fsUserVal.value); scheduleUpdatePreview(); });
lhAiInp.value = tempCfg.lineHeightAi !== undefined ? clampLineHeight(tempCfg.lineHeightAi) : DEFAULT_LINE_HEIGHT;
lhAiVal.textContent = (+lhAiInp.value).toFixed(1);
lhAiInp.addEventListener('input', () => { lhAiVal.textContent = (+lhAiInp.value).toFixed(1); tempCfg.lineHeightAi = clampLineHeight(lhAiInp.value); scheduleUpdatePreview(); });
lhUserInp.value = tempCfg.lineHeightUser !== undefined ? clampLineHeight(tempCfg.lineHeightUser) : DEFAULT_LINE_HEIGHT;
lhUserVal.textContent = (+lhUserInp.value).toFixed(1);
lhUserInp.addEventListener('input', () => { lhUserVal.textContent = (+lhUserInp.value).toFixed(1); tempCfg.lineHeightUser = clampLineHeight(lhUserInp.value); scheduleUpdatePreview(); });
lsAiInp.value = tempCfg.letterSpacingAi !== undefined ? clampLetterSpacing(tempCfg.letterSpacingAi) : DEFAULT_LETTER_SPACING;
lsAiVal.textContent = (+lsAiInp.value).toFixed(1) + 'px';
lsAiInp.addEventListener('input', () => { lsAiVal.textContent = (+lsAiInp.value).toFixed(1) + 'px'; tempCfg.letterSpacingAi = clampLetterSpacing(lsAiInp.value); scheduleUpdatePreview(); });
lsUserInp.value = tempCfg.letterSpacingUser !== undefined ? clampLetterSpacing(tempCfg.letterSpacingUser) : DEFAULT_LETTER_SPACING;
lsUserVal.textContent = (+lsUserInp.value).toFixed(1) + 'px';
lsUserInp.addEventListener('input', () => { lsUserVal.textContent = (+lsUserInp.value).toFixed(1) + 'px'; tempCfg.letterSpacingUser = clampLetterSpacing(lsUserInp.value); scheduleUpdatePreview(); });
({ center: talignAiCenterInp, right: talignAiRightInp }[tempCfg.textAlignAi] || talignAiLeftInp).checked = true;
talignAiLeftInp.addEventListener('change', () => { if (talignAiLeftInp.checked) { tempCfg.textAlignAi = 'left'; scheduleUpdatePreview(); } });
talignAiCenterInp.addEventListener('change', () => { if (talignAiCenterInp.checked) { tempCfg.textAlignAi = 'center'; scheduleUpdatePreview(); } });
talignAiRightInp.addEventListener('change', () => { if (talignAiRightInp.checked) { tempCfg.textAlignAi = 'right'; scheduleUpdatePreview(); } });
({ center: talignUserCenterInp, right: talignUserRightInp }[tempCfg.textAlignUser] || talignUserLeftInp).checked = true;
talignUserLeftInp.addEventListener('change', () => { if (talignUserLeftInp.checked) { tempCfg.textAlignUser = 'left'; scheduleUpdatePreview(); } });
talignUserCenterInp.addEventListener('change', () => { if (talignUserCenterInp.checked) { tempCfg.textAlignUser = 'center'; scheduleUpdatePreview(); } });
talignUserRightInp.addEventListener('change', () => { if (talignUserRightInp.checked) { tempCfg.textAlignUser = 'right'; scheduleUpdatePreview(); } });
// ─── ITALIC/BOLD CUSTOM COLOR ───
const italicBoldToggle = $('cai-italicbold-toggle'), italicBoldColors = $('cai-italicbold-colors');
const italicColorInp = $('cai-italic-color'), boldColorInp = $('cai-bold-color');
const italicBoldToggleAi = $('cai-italicbold-toggle-ai'), italicBoldColorsAi = $('cai-italicbold-colors-ai');
const italicColorAiInp = $('cai-italic-color-ai'), boldColorAiInp = $('cai-bold-color-ai');
const italicBoldToggleUser = $('cai-italicbold-toggle-user'), italicBoldColorsUser = $('cai-italicbold-colors-user');
const italicColorUserInp = $('cai-italic-color-user'), boldColorUserInp = $('cai-bold-color-user');
italicBoldToggle.checked = !!tempCfg.italicBoldEnabled;
italicBoldColors.style.display = italicBoldToggle.checked ? 'flex' : 'none';
italicColorInp.value = tempCfg.italicColor || '#ffffff';
boldColorInp.value = tempCfg.boldColor || '#ffffff';
italicColorInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorInp.dispatchEvent(new Event('input', { bubbles: true }));
italicBoldToggle.addEventListener('change', () => {
italicBoldColors.style.display = italicBoldToggle.checked ? 'flex' : 'none';
tempCfg.italicBoldEnabled = italicBoldToggle.checked;
scheduleUpdatePreview();
});
italicColorInp.addEventListener('input', () => { tempCfg.italicColor = italicColorInp.value; scheduleUpdatePreview(); });
boldColorInp.addEventListener('input', () => { tempCfg.boldColor = boldColorInp.value; scheduleUpdatePreview(); });
italicBoldToggleAi.checked = !!tempCfg.italicBoldEnabledAi;
italicBoldColorsAi.style.display = italicBoldToggleAi.checked ? 'flex' : 'none';
italicColorAiInp.value = tempCfg.italicColorAi || '#ffffff';
boldColorAiInp.value = tempCfg.boldColorAi || '#ffffff';
italicColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
italicBoldToggleAi.addEventListener('change', () => {
italicBoldColorsAi.style.display = italicBoldToggleAi.checked ? 'flex' : 'none';
tempCfg.italicBoldEnabledAi = italicBoldToggleAi.checked;
scheduleUpdatePreview();
});
italicColorAiInp.addEventListener('input', () => { tempCfg.italicColorAi = italicColorAiInp.value; scheduleUpdatePreview(); });
boldColorAiInp.addEventListener('input', () => { tempCfg.boldColorAi = boldColorAiInp.value; scheduleUpdatePreview(); });
italicBoldToggleUser.checked = !!tempCfg.italicBoldEnabledUser;
italicBoldColorsUser.style.display = italicBoldToggleUser.checked ? 'flex' : 'none';
italicColorUserInp.value = tempCfg.italicColorUser || '#ffffff';
boldColorUserInp.value = tempCfg.boldColorUser || '#ffffff';
italicColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
italicBoldToggleUser.addEventListener('change', () => {
italicBoldColorsUser.style.display = italicBoldToggleUser.checked ? 'flex' : 'none';
tempCfg.italicBoldEnabledUser = italicBoldToggleUser.checked;
scheduleUpdatePreview();
});
italicColorUserInp.addEventListener('input', () => { tempCfg.italicColorUser = italicColorUserInp.value; scheduleUpdatePreview(); });
boldColorUserInp.addEventListener('input', () => { tempCfg.boldColorUser = boldColorUserInp.value; scheduleUpdatePreview(); });
// ─── ENTER KEY ───
(tempCfg.enterKeySend ? enterKeySendInp : enterKeyNewlineInp).checked = true;
enterKeyNewlineInp.addEventListener('change', () => { if (enterKeyNewlineInp.checked) tempCfg.enterKeySend = false; });
enterKeySendInp.addEventListener('change', () => { if (enterKeySendInp.checked) tempCfg.enterKeySend = true; });
// ─── MESSAGE SPACING ───
mspacingInp.value = tempCfg.msgSpacing !== undefined ? clampMsgSpacing(tempCfg.msgSpacing) : DEFAULT_MSG_SPACING;
mspacingVal.textContent = mspacingInp.value;
mspacingInp.addEventListener('input', () => {
mspacingVal.textContent = mspacingInp.value;
tempCfg.msgSpacing = clampMsgSpacing(mspacingInp.value);
scheduleUpdatePreview();
});
// ─── BUBBLE STYLE (Classic / Dense) ───
const styleClassicInp = $('cai-style-classic'), styleDenseInp = $('cai-style-dense');
(tempCfg.bubbleStyle === 'dense' ? styleDenseInp : styleClassicInp).checked = true;
styleClassicInp.addEventListener('change', () => { if (styleClassicInp.checked) tempCfg.bubbleStyle = 'classic'; });
styleDenseInp.addEventListener('change', () => { if (styleDenseInp.checked) tempCfg.bubbleStyle = 'dense'; });
// ─── ALIGNMENT (Default / Left / Right) ───
const alignDefaultInp = $('cai-align-default'), alignLeftInp = $('cai-align-left'), alignRightInp = $('cai-align-right'), alignReverseInp = $('cai-align-reverse');
({ left: alignLeftInp, right: alignRightInp, reverse: alignReverseInp }[tempCfg.bubbleAlign] || alignDefaultInp).checked = true;
alignDefaultInp.addEventListener('change', () => { if (alignDefaultInp.checked) tempCfg.bubbleAlign = 'default'; });
alignLeftInp.addEventListener('change', () => { if (alignLeftInp.checked) tempCfg.bubbleAlign = 'left'; });
alignRightInp.addEventListener('change', () => { if (alignRightInp.checked) tempCfg.bubbleAlign = 'right'; });
alignReverseInp.addEventListener('change', () => { if (alignReverseInp.checked) tempCfg.bubbleAlign = 'reverse'; });
// ─── BUBBLE BLUR ───
blurToggle.checked = !!tempCfg.blurEnabled;
blurSliderWrap.style.display = blurToggle.checked ? 'block' : 'none';
blurAmountInp.value = tempCfg.blurAmount !== undefined ? tempCfg.blurAmount : 8;
blurVal.textContent = blurAmountInp.value + 'px';
blurToggle.addEventListener('change', () => {
blurSliderWrap.style.display = blurToggle.checked ? 'block' : 'none';
tempCfg.blurEnabled = blurToggle.checked;
scheduleUpdatePreview();
});
blurAmountInp.addEventListener('input', () => {
blurVal.textContent = blurAmountInp.value + 'px';
tempCfg.blurAmount = +blurAmountInp.value;
scheduleUpdatePreview();
});
// ─── BUBBLE BORDER ───
const borderToggle = $('cai-border-toggle'), borderControls = $('cai-border-controls');
const borderModeSel = $('cai-border-mode');
const borderDualWrap = $('cai-border-dual-wrap'), borderIndividualWrap = $('cai-border-individual-wrap');
const borderWidthInp = $('cai-border-width'), borderWidthVal = $('cai-border-width-val');
const borderColorInp = $('cai-border-color');
const borderWidthAiInp = $('cai-border-width-ai'), borderWidthAiVal = $('cai-border-width-ai-val');
const borderWidthUserInp = $('cai-border-width-user'), borderWidthUserVal = $('cai-border-width-user-val');
const borderColorAiInp = $('cai-border-color-ai'), borderColorUserInp = $('cai-border-color-user');
borderToggle.checked = !!tempCfg.borderEnabled;
borderControls.style.display = borderToggle.checked ? 'flex' : 'none';
borderModeSel.value = tempCfg.borderMode === 'individual' ? 'individual' : 'dual';
borderDualWrap.style.display = borderModeSel.value === 'dual' ? 'flex' : 'none';
borderIndividualWrap.style.display = borderModeSel.value === 'individual' ? 'flex' : 'none';
borderWidthInp.value = tempCfg.borderWidth !== undefined ? tempCfg.borderWidth : DEFAULT_BORDER_WIDTH;
borderWidthVal.textContent = borderWidthInp.value;
borderWidthAiInp.value = tempCfg.borderWidthAi !== undefined ? tempCfg.borderWidthAi : DEFAULT_BORDER_WIDTH;
borderWidthAiVal.textContent = borderWidthAiInp.value;
borderWidthUserInp.value = tempCfg.borderWidthUser !== undefined ? tempCfg.borderWidthUser : DEFAULT_BORDER_WIDTH;
borderWidthUserVal.textContent = borderWidthUserInp.value;
borderColorInp.value = tempCfg.borderColor || '#ffffff';
borderColorAiInp.value = tempCfg.borderColorAi || '#ffffff';
borderColorUserInp.value = tempCfg.borderColorUser || '#ffffff';
borderColorInp.dispatchEvent(new Event('input', { bubbles: true }));
borderColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
borderColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
borderToggle.addEventListener('change', () => {
borderControls.style.display = borderToggle.checked ? 'flex' : 'none';
tempCfg.borderEnabled = borderToggle.checked;
scheduleUpdatePreview();
});
borderModeSel.addEventListener('change', () => {
tempCfg.borderMode = borderModeSel.value;
borderDualWrap.style.display = borderModeSel.value === 'dual' ? 'flex' : 'none';
borderIndividualWrap.style.display = borderModeSel.value === 'individual' ? 'flex' : 'none';
scheduleUpdatePreview();
});
borderWidthInp.addEventListener('input', () => {
borderWidthVal.textContent = borderWidthInp.value;
tempCfg.borderWidth = +borderWidthInp.value;
scheduleUpdatePreview();
});
borderWidthAiInp.addEventListener('input', () => {
borderWidthAiVal.textContent = borderWidthAiInp.value;
tempCfg.borderWidthAi = +borderWidthAiInp.value;
scheduleUpdatePreview();
});
borderWidthUserInp.addEventListener('input', () => {
borderWidthUserVal.textContent = borderWidthUserInp.value;
tempCfg.borderWidthUser = +borderWidthUserInp.value;
scheduleUpdatePreview();
});
borderColorInp.addEventListener('input', () => { tempCfg.borderColor = borderColorInp.value; scheduleUpdatePreview(); });
borderColorAiInp.addEventListener('input', () => { tempCfg.borderColorAi = borderColorAiInp.value; scheduleUpdatePreview(); });
borderColorUserInp.addEventListener('input', () => { tempCfg.borderColorUser = borderColorUserInp.value; scheduleUpdatePreview(); });
// ─── BUBBLE SHADOW ───
const shadowToggle = $('cai-shadow-toggle'), shadowControls = $('cai-shadow-controls');
const shadowModeSel = $('cai-shadow-mode');
const shadowDualWrap = $('cai-shadow-dual-wrap'), shadowIndividualWrap = $('cai-shadow-individual-wrap');
const shadowColorInp = $('cai-shadow-color');
const shadowColorAiInp = $('cai-shadow-color-ai'), shadowColorUserInp = $('cai-shadow-color-user');
const shadowSpreadInp = $('cai-shadow-spread'), shadowSpreadVal = $('cai-shadow-spread-val');
const shadowBlurInp = $('cai-shadow-blur'), shadowBlurVal = $('cai-shadow-blur-val');
const shadowOffxInp = $('cai-shadow-offx'), shadowOffxVal = $('cai-shadow-offx-val');
const shadowOffyInp = $('cai-shadow-offy'), shadowOffyVal = $('cai-shadow-offy-val');
const innerShadowToggle = $('cai-innershadow-toggle'), innerShadowControls = $('cai-innershadow-controls');
const innerShadowModeSel = $('cai-innershadow-mode');
const innerShadowDualWrap = $('cai-innershadow-dual-wrap'), innerShadowIndividualWrap = $('cai-innershadow-individual-wrap');
const innerShadowColorInp = $('cai-innershadow-color');
const innerShadowColorAiInp = $('cai-innershadow-color-ai'), innerShadowColorUserInp = $('cai-innershadow-color-user');
const innerShadowSpreadInp = $('cai-innershadow-spread'), innerShadowSpreadVal = $('cai-innershadow-spread-val');
const innerShadowBlurInp = $('cai-innershadow-blur'), innerShadowBlurVal = $('cai-innershadow-blur-val');
const innerShadowOffxInp = $('cai-innershadow-offx'), innerShadowOffxVal = $('cai-innershadow-offx-val');
const innerShadowOffyInp = $('cai-innershadow-offy'), innerShadowOffyVal = $('cai-innershadow-offy-val');
shadowToggle.checked = !!tempCfg.shadowEnabled;
shadowControls.style.display = shadowToggle.checked ? 'flex' : 'none';
shadowModeSel.value = tempCfg.shadowMode === 'individual' ? 'individual' : 'dual';
shadowDualWrap.style.display = shadowModeSel.value === 'dual' ? 'flex' : 'none';
shadowIndividualWrap.style.display = shadowModeSel.value === 'individual' ? 'flex' : 'none';
shadowColorInp.value = tempCfg.shadowColor || '#000000';
shadowColorAiInp.value = tempCfg.shadowColorAi || '#000000';
shadowColorUserInp.value = tempCfg.shadowColorUser || '#000000';
shadowColorInp.dispatchEvent(new Event('input', { bubbles: true }));
shadowColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
shadowColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
shadowSpreadInp.value = tempCfg.shadowSpread !== undefined ? tempCfg.shadowSpread : DEFAULT_SHADOW_SPREAD;
shadowSpreadVal.textContent = shadowSpreadInp.value;
shadowBlurInp.value = tempCfg.shadowBlur !== undefined ? tempCfg.shadowBlur : DEFAULT_SHADOW_BLUR;
shadowBlurVal.textContent = shadowBlurInp.value;
shadowOffxInp.value = tempCfg.shadowOffsetX !== undefined ? tempCfg.shadowOffsetX : DEFAULT_SHADOW_OFFSET_X;
shadowOffxVal.textContent = shadowOffxInp.value;
shadowOffyInp.value = tempCfg.shadowOffsetY !== undefined ? tempCfg.shadowOffsetY : DEFAULT_SHADOW_OFFSET_Y;
shadowOffyVal.textContent = shadowOffyInp.value;
shadowToggle.addEventListener('change', () => {
shadowControls.style.display = shadowToggle.checked ? 'flex' : 'none';
tempCfg.shadowEnabled = shadowToggle.checked;
scheduleUpdatePreview();
});
shadowModeSel.addEventListener('change', () => {
tempCfg.shadowMode = shadowModeSel.value;
shadowDualWrap.style.display = shadowModeSel.value === 'dual' ? 'flex' : 'none';
shadowIndividualWrap.style.display = shadowModeSel.value === 'individual' ? 'flex' : 'none';
scheduleUpdatePreview();
});
shadowColorInp.addEventListener('input', () => { tempCfg.shadowColor = shadowColorInp.value; scheduleUpdatePreview(); });
shadowColorAiInp.addEventListener('input', () => { tempCfg.shadowColorAi = shadowColorAiInp.value; scheduleUpdatePreview(); });
shadowColorUserInp.addEventListener('input', () => { tempCfg.shadowColorUser = shadowColorUserInp.value; scheduleUpdatePreview(); });
shadowSpreadInp.addEventListener('input', () => {
shadowSpreadVal.textContent = shadowSpreadInp.value;
tempCfg.shadowSpread = clampShadowSpread(shadowSpreadInp.value);
scheduleUpdatePreview();
});
shadowBlurInp.addEventListener('input', () => {
shadowBlurVal.textContent = shadowBlurInp.value;
tempCfg.shadowBlur = clampShadowBlur(shadowBlurInp.value);
scheduleUpdatePreview();
});
shadowOffxInp.addEventListener('input', () => {
shadowOffxVal.textContent = shadowOffxInp.value;
tempCfg.shadowOffsetX = clampShadowOffset(shadowOffxInp.value);
scheduleUpdatePreview();
});
shadowOffyInp.addEventListener('input', () => {
shadowOffyVal.textContent = shadowOffyInp.value;
tempCfg.shadowOffsetY = clampShadowOffset(shadowOffyInp.value);
scheduleUpdatePreview();
});
innerShadowToggle.checked = !!tempCfg.innerShadowEnabled;
innerShadowControls.style.display = innerShadowToggle.checked ? 'flex' : 'none';
innerShadowModeSel.value = tempCfg.innerShadowMode === 'individual' ? 'individual' : 'dual';
innerShadowDualWrap.style.display = innerShadowModeSel.value === 'dual' ? 'flex' : 'none';
innerShadowIndividualWrap.style.display = innerShadowModeSel.value === 'individual' ? 'flex' : 'none';
innerShadowColorInp.value = tempCfg.innerShadowColor || '#000000';
innerShadowColorAiInp.value = tempCfg.innerShadowColorAi || '#000000';
innerShadowColorUserInp.value = tempCfg.innerShadowColorUser || '#000000';
innerShadowColorInp.dispatchEvent(new Event('input', { bubbles: true }));
innerShadowColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
innerShadowColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
innerShadowSpreadInp.value = tempCfg.innerShadowSpread !== undefined ? tempCfg.innerShadowSpread : DEFAULT_INNER_SHADOW_SPREAD;
innerShadowSpreadVal.textContent = innerShadowSpreadInp.value;
innerShadowBlurInp.value = tempCfg.innerShadowBlur !== undefined ? tempCfg.innerShadowBlur : DEFAULT_INNER_SHADOW_BLUR;
innerShadowBlurVal.textContent = innerShadowBlurInp.value;
innerShadowOffxInp.value = tempCfg.innerShadowOffsetX !== undefined ? tempCfg.innerShadowOffsetX : DEFAULT_INNER_SHADOW_OFFSET_X;
innerShadowOffxVal.textContent = innerShadowOffxInp.value;
innerShadowOffyInp.value = tempCfg.innerShadowOffsetY !== undefined ? tempCfg.innerShadowOffsetY : DEFAULT_INNER_SHADOW_OFFSET_Y;
innerShadowOffyVal.textContent = innerShadowOffyInp.value;
innerShadowToggle.addEventListener('change', () => {
innerShadowControls.style.display = innerShadowToggle.checked ? 'flex' : 'none';
tempCfg.innerShadowEnabled = innerShadowToggle.checked;
scheduleUpdatePreview();
});
innerShadowModeSel.addEventListener('change', () => {
tempCfg.innerShadowMode = innerShadowModeSel.value;
innerShadowDualWrap.style.display = innerShadowModeSel.value === 'dual' ? 'flex' : 'none';
innerShadowIndividualWrap.style.display = innerShadowModeSel.value === 'individual' ? 'flex' : 'none';
scheduleUpdatePreview();
});
innerShadowColorInp.addEventListener('input', () => { tempCfg.innerShadowColor = innerShadowColorInp.value; scheduleUpdatePreview(); });
innerShadowColorAiInp.addEventListener('input', () => { tempCfg.innerShadowColorAi = innerShadowColorAiInp.value; scheduleUpdatePreview(); });
innerShadowColorUserInp.addEventListener('input', () => { tempCfg.innerShadowColorUser = innerShadowColorUserInp.value; scheduleUpdatePreview(); });
innerShadowSpreadInp.addEventListener('input', () => {
innerShadowSpreadVal.textContent = innerShadowSpreadInp.value;
tempCfg.innerShadowSpread = clampInnerShadowSpread(innerShadowSpreadInp.value);
scheduleUpdatePreview();
});
innerShadowBlurInp.addEventListener('input', () => {
innerShadowBlurVal.textContent = innerShadowBlurInp.value;
tempCfg.innerShadowBlur = clampInnerShadowBlur(innerShadowBlurInp.value);
scheduleUpdatePreview();
});
innerShadowOffxInp.addEventListener('input', () => {
innerShadowOffxVal.textContent = innerShadowOffxInp.value;
tempCfg.innerShadowOffsetX = clampInnerShadowOffset(innerShadowOffxInp.value);
scheduleUpdatePreview();
});
innerShadowOffyInp.addEventListener('input', () => {
innerShadowOffyVal.textContent = innerShadowOffyInp.value;
tempCfg.innerShadowOffsetY = clampInnerShadowOffset(innerShadowOffyInp.value);
scheduleUpdatePreview();
});
// ─── BUBBLE CORNERS ───
const cornersToggle = $('cai-corners-toggle'), cornersControls = $('cai-corners-controls');
const cornersScopeSel = $('cai-corners-scope');
const cornersDualWrap = $('cai-corners-dual-wrap'), cornersIndividualWrap = $('cai-corners-individual-wrap');
const cornersUniformInp = $('cai-corners-uniform'), cornersCustomInp = $('cai-corners-custom');
const cornersUniformWrap = $('cai-corners-uniform-wrap');
const cornerRadiusInp = $('cai-corner-radius'), cornerRadiusVal = $('cai-corner-radius-val');
const cornersCustomWrap = $('cai-corners-custom-wrap');
const cornerTLInp = $('cai-corner-tl'), cornerTLVal = $('cai-corner-tl-val');
const cornerTRInp = $('cai-corner-tr'), cornerTRVal = $('cai-corner-tr-val');
const cornerBLInp = $('cai-corner-bl'), cornerBLVal = $('cai-corner-bl-val');
const cornerBRInp = $('cai-corner-br'), cornerBRVal = $('cai-corner-br-val');
const cornersUniformAiInp = $('cai-corners-uniform-ai'), cornersCustomAiInp = $('cai-corners-custom-ai');
const cornersUniformWrapAi = $('cai-corners-uniform-wrap-ai'), cornersCustomWrapAi = $('cai-corners-custom-wrap-ai');
const cornerRadiusAiInp = $('cai-corner-radius-ai'), cornerRadiusAiVal = $('cai-corner-radius-ai-val');
const cornerTLAiInp = $('cai-corner-tl-ai'), cornerTLAiVal = $('cai-corner-tl-ai-val');
const cornerTRAiInp = $('cai-corner-tr-ai'), cornerTRAiVal = $('cai-corner-tr-ai-val');
const cornerBLAiInp = $('cai-corner-bl-ai'), cornerBLAiVal = $('cai-corner-bl-ai-val');
const cornerBRAiInp = $('cai-corner-br-ai'), cornerBRAiVal = $('cai-corner-br-ai-val');
const cornersUniformUserInp = $('cai-corners-uniform-user'), cornersCustomUserInp = $('cai-corners-custom-user');
const cornersUniformWrapUser = $('cai-corners-uniform-wrap-user'), cornersCustomWrapUser = $('cai-corners-custom-wrap-user');
const cornerRadiusUserInp = $('cai-corner-radius-user'), cornerRadiusUserVal = $('cai-corner-radius-user-val');
const cornerTLUserInp = $('cai-corner-tl-user'), cornerTLUserVal = $('cai-corner-tl-user-val');
const cornerTRUserInp = $('cai-corner-tr-user'), cornerTRUserVal = $('cai-corner-tr-user-val');
const cornerBLUserInp = $('cai-corner-bl-user'), cornerBLUserVal = $('cai-corner-bl-user-val');
const cornerBRUserInp = $('cai-corner-br-user'), cornerBRUserVal = $('cai-corner-br-user-val');
cornersToggle.checked = !!tempCfg.cornersEnabled;
cornersControls.style.display = cornersToggle.checked ? 'flex' : 'none';
cornersScopeSel.value = tempCfg.cornersScope === 'individual' ? 'individual' : 'dual';
cornersDualWrap.style.display = cornersScopeSel.value === 'individual' ? 'none' : 'flex';
cornersIndividualWrap.style.display = cornersScopeSel.value === 'individual' ? 'flex' : 'none';
(tempCfg.cornersMode === 'custom' ? cornersCustomInp : cornersUniformInp).checked = true;
cornersUniformWrap.style.display = (tempCfg.cornersMode === 'custom') ? 'none' : 'block';
cornerRadiusInp.value = tempCfg.cornerRadius !== undefined ? tempCfg.cornerRadius : DEFAULT_CORNER_RADIUS;
cornerRadiusVal.textContent = cornerRadiusInp.value;
cornersCustomWrap.style.display = (tempCfg.cornersMode === 'custom') ? 'flex' : 'none';
cornerTLInp.value = tempCfg.cornerRadiusTL !== undefined ? tempCfg.cornerRadiusTL : DEFAULT_CORNER_RADIUS;
cornerTRInp.value = tempCfg.cornerRadiusTR !== undefined ? tempCfg.cornerRadiusTR : DEFAULT_CORNER_RADIUS;
cornerBLInp.value = tempCfg.cornerRadiusBL !== undefined ? tempCfg.cornerRadiusBL : DEFAULT_CORNER_RADIUS;
cornerBRInp.value = tempCfg.cornerRadiusBR !== undefined ? tempCfg.cornerRadiusBR : DEFAULT_CORNER_RADIUS;
cornerTLVal.textContent = cornerTLInp.value;
cornerTRVal.textContent = cornerTRInp.value;
cornerBLVal.textContent = cornerBLInp.value;
cornerBRVal.textContent = cornerBRInp.value;
(tempCfg.cornersModeAi === 'custom' ? cornersCustomAiInp : cornersUniformAiInp).checked = true;
cornersUniformWrapAi.style.display = (tempCfg.cornersModeAi === 'custom') ? 'none' : 'block';
cornersCustomWrapAi.style.display = (tempCfg.cornersModeAi === 'custom') ? 'flex' : 'none';
cornerRadiusAiInp.value = tempCfg.cornerRadiusAi !== undefined ? tempCfg.cornerRadiusAi : DEFAULT_CORNER_RADIUS;
cornerRadiusAiVal.textContent = cornerRadiusAiInp.value;
cornerTLAiInp.value = tempCfg.cornerRadiusTLAi !== undefined ? tempCfg.cornerRadiusTLAi : DEFAULT_CORNER_RADIUS;
cornerTRAiInp.value = tempCfg.cornerRadiusTRAi !== undefined ? tempCfg.cornerRadiusTRAi : DEFAULT_CORNER_RADIUS;
cornerBLAiInp.value = tempCfg.cornerRadiusBLAi !== undefined ? tempCfg.cornerRadiusBLAi : DEFAULT_CORNER_RADIUS;
cornerBRAiInp.value = tempCfg.cornerRadiusBRAi !== undefined ? tempCfg.cornerRadiusBRAi : DEFAULT_CORNER_RADIUS;
cornerTLAiVal.textContent = cornerTLAiInp.value;
cornerTRAiVal.textContent = cornerTRAiInp.value;
cornerBLAiVal.textContent = cornerBLAiInp.value;
cornerBRAiVal.textContent = cornerBRAiInp.value;
(tempCfg.cornersModeUser === 'custom' ? cornersCustomUserInp : cornersUniformUserInp).checked = true;
cornersUniformWrapUser.style.display = (tempCfg.cornersModeUser === 'custom') ? 'none' : 'block';
cornersCustomWrapUser.style.display = (tempCfg.cornersModeUser === 'custom') ? 'flex' : 'none';
cornerRadiusUserInp.value = tempCfg.cornerRadiusUser !== undefined ? tempCfg.cornerRadiusUser : DEFAULT_CORNER_RADIUS;
cornerRadiusUserVal.textContent = cornerRadiusUserInp.value;
cornerTLUserInp.value = tempCfg.cornerRadiusTLUser !== undefined ? tempCfg.cornerRadiusTLUser : DEFAULT_CORNER_RADIUS;
cornerTRUserInp.value = tempCfg.cornerRadiusTRUser !== undefined ? tempCfg.cornerRadiusTRUser : DEFAULT_CORNER_RADIUS;
cornerBLUserInp.value = tempCfg.cornerRadiusBLUser !== undefined ? tempCfg.cornerRadiusBLUser : DEFAULT_CORNER_RADIUS;
cornerBRUserInp.value = tempCfg.cornerRadiusBRUser !== undefined ? tempCfg.cornerRadiusBRUser : DEFAULT_CORNER_RADIUS;
cornerTLUserVal.textContent = cornerTLUserInp.value;
cornerTRUserVal.textContent = cornerTRUserInp.value;
cornerBLUserVal.textContent = cornerBLUserInp.value;
cornerBRUserVal.textContent = cornerBRUserInp.value;
cornersToggle.addEventListener('change', () => {
cornersControls.style.display = cornersToggle.checked ? 'flex' : 'none';
tempCfg.cornersEnabled = cornersToggle.checked;
scheduleUpdatePreview();
});
cornersScopeSel.addEventListener('change', () => {
tempCfg.cornersScope = cornersScopeSel.value;
cornersDualWrap.style.display = cornersScopeSel.value === 'individual' ? 'none' : 'flex';
cornersIndividualWrap.style.display = cornersScopeSel.value === 'individual' ? 'flex' : 'none';
scheduleUpdatePreview();
});
cornersUniformInp.addEventListener('change', () => {
if (cornersUniformInp.checked) {
tempCfg.cornersMode = 'uniform';
cornersUniformWrap.style.display = 'block';
cornersCustomWrap.style.display = 'none';
scheduleUpdatePreview();
}
});
cornersCustomInp.addEventListener('change', () => {
if (cornersCustomInp.checked) {
tempCfg.cornersMode = 'custom';
cornersUniformWrap.style.display = 'none';
cornersCustomWrap.style.display = 'flex';
scheduleUpdatePreview();
}
});
cornerRadiusInp.addEventListener('input', () => { cornerRadiusVal.textContent = cornerRadiusInp.value; tempCfg.cornerRadius = +cornerRadiusInp.value; scheduleUpdatePreview(); });
cornerTLInp.addEventListener('input', () => { cornerTLVal.textContent = cornerTLInp.value; tempCfg.cornerRadiusTL = +cornerTLInp.value; scheduleUpdatePreview(); });
cornerTRInp.addEventListener('input', () => { cornerTRVal.textContent = cornerTRInp.value; tempCfg.cornerRadiusTR = +cornerTRInp.value; scheduleUpdatePreview(); });
cornerBLInp.addEventListener('input', () => { cornerBLVal.textContent = cornerBLInp.value; tempCfg.cornerRadiusBL = +cornerBLInp.value; scheduleUpdatePreview(); });
cornerBRInp.addEventListener('input', () => { cornerBRVal.textContent = cornerBRInp.value; tempCfg.cornerRadiusBR = +cornerBRInp.value; scheduleUpdatePreview(); });
cornersUniformAiInp.addEventListener('change', () => {
if (cornersUniformAiInp.checked) {
tempCfg.cornersModeAi = 'uniform';
cornersUniformWrapAi.style.display = 'block';
cornersCustomWrapAi.style.display = 'none';
scheduleUpdatePreview();
}
});
cornersCustomAiInp.addEventListener('change', () => {
if (cornersCustomAiInp.checked) {
tempCfg.cornersModeAi = 'custom';
cornersUniformWrapAi.style.display = 'none';
cornersCustomWrapAi.style.display = 'flex';
scheduleUpdatePreview();
}
});
cornerRadiusAiInp.addEventListener('input', () => { cornerRadiusAiVal.textContent = cornerRadiusAiInp.value; tempCfg.cornerRadiusAi = +cornerRadiusAiInp.value; scheduleUpdatePreview(); });
cornerTLAiInp.addEventListener('input', () => { cornerTLAiVal.textContent = cornerTLAiInp.value; tempCfg.cornerRadiusTLAi = +cornerTLAiInp.value; scheduleUpdatePreview(); });
cornerTRAiInp.addEventListener('input', () => { cornerTRAiVal.textContent = cornerTRAiInp.value; tempCfg.cornerRadiusTRAi = +cornerTRAiInp.value; scheduleUpdatePreview(); });
cornerBLAiInp.addEventListener('input', () => { cornerBLAiVal.textContent = cornerBLAiInp.value; tempCfg.cornerRadiusBLAi = +cornerBLAiInp.value; scheduleUpdatePreview(); });
cornerBRAiInp.addEventListener('input', () => { cornerBRAiVal.textContent = cornerBRAiInp.value; tempCfg.cornerRadiusBRAi = +cornerBRAiInp.value; scheduleUpdatePreview(); });
cornersUniformUserInp.addEventListener('change', () => {
if (cornersUniformUserInp.checked) {
tempCfg.cornersModeUser = 'uniform';
cornersUniformWrapUser.style.display = 'block';
cornersCustomWrapUser.style.display = 'none';
scheduleUpdatePreview();
}
});
cornersCustomUserInp.addEventListener('change', () => {
if (cornersCustomUserInp.checked) {
tempCfg.cornersModeUser = 'custom';
cornersUniformWrapUser.style.display = 'none';
cornersCustomWrapUser.style.display = 'flex';
scheduleUpdatePreview();
}
});
cornerRadiusUserInp.addEventListener('input', () => { cornerRadiusUserVal.textContent = cornerRadiusUserInp.value; tempCfg.cornerRadiusUser = +cornerRadiusUserInp.value; scheduleUpdatePreview(); });
cornerTLUserInp.addEventListener('input', () => { cornerTLUserVal.textContent = cornerTLUserInp.value; tempCfg.cornerRadiusTLUser = +cornerTLUserInp.value; scheduleUpdatePreview(); });
cornerTRUserInp.addEventListener('input', () => { cornerTRUserVal.textContent = cornerTRUserInp.value; tempCfg.cornerRadiusTRUser = +cornerTRUserInp.value; scheduleUpdatePreview(); });
cornerBLUserInp.addEventListener('input', () => { cornerBLUserVal.textContent = cornerBLUserInp.value; tempCfg.cornerRadiusBLUser = +cornerBLUserInp.value; scheduleUpdatePreview(); });
cornerBRUserInp.addEventListener('input', () => { cornerBRUserVal.textContent = cornerBRUserInp.value; tempCfg.cornerRadiusBRUser = +cornerBRUserInp.value; scheduleUpdatePreview(); });
// ─── AVATAR SIZE ───
avatarSizeInp.value = tempCfg.avatarSize !== undefined ? clampAvatarIdx(tempCfg.avatarSize) : DEFAULT_AVATAR_IDX;
avatarSizeVal.textContent = AVATAR_SIZES[+avatarSizeInp.value] + 'px';
avatarSizeInp.addEventListener('input', () => {
avatarSizeVal.textContent = AVATAR_SIZES[+avatarSizeInp.value] + 'px';
tempCfg.avatarSize = clampAvatarIdx(avatarSizeInp.value);
});
avatarForceMobileInp.checked = !!tempCfg.avatarForceMobile;
avatarForceMobileInp.addEventListener('change', () => {
tempCfg.avatarForceMobile = avatarForceMobileInp.checked;
});
hideAvatarsDesktopInp.checked = !!tempCfg.hideAvatarsDesktop;
hideAvatarsDesktopInp.addEventListener('change', () => {
tempCfg.hideAvatarsDesktop = hideAvatarsDesktopInp.checked;
});
hideNamesInp.checked = !!tempCfg.hideNames;
hideNamesInp.addEventListener('change', () => {
tempCfg.hideNames = hideNamesInp.checked;
});
replaceAuthorMobileInp.checked = !!tempCfg.replaceAuthorMobile;
replaceAuthorMobileInp.addEventListener('change', () => {
tempCfg.replaceAuthorMobile = replaceAuthorMobileInp.checked;
});
function updateBgPreviewStyle() {
const op = bgOpacityInp.value / 100;
const br = bgBrightInp.value / 100;
const ct = bgContrastInp.value / 100;
const sa = bgSaturationInp.value / 100;
const bl = bgBlurInp.value;
bgImg.style.opacity = op;
bgImg.style.filter = `brightness(${br}) contrast(${ct}) saturate(${sa}) blur(${bl}px)`;
const fitMap = { cover: 'cover', contain: 'contain', stretch: 'fill' };
bgImg.style.objectFit = fitMap[getBgFit()] || 'cover';
}
function showBgPreview(type, url) {
if (!url) { bgPreview.style.display = 'none'; return; }
bgPreview.style.display = 'block';
bgImg.src = url; bgImg.style.display = 'block';
updateBgPreviewStyle();
}
// BUILD BUBBLE LIST
let gridCSS = '';
const cats = [...new Set(BUBBLES.map(b => b.cat))];
cats.forEach(cat => {
const lbl = document.createElement('div');
lbl.className = 'cai-clabel'; lbl.textContent = cat;
blist.appendChild(lbl);
const grid = document.createElement('div');
grid.className = 'cai-bgrid';
BUBBLES.filter(b => b.cat === cat).forEach(b => {
const item = document.createElement('div');
item.className = 'cai-bitem' + (selId === b.id ? ' selected' : '');
item.dataset.id = b.id;
const demo = document.createElement('div');
demo.className = 'cai-bdemo';
demo.textContent = '#' + b.id + ' ' + b.name.split(' ')[0];
const demoSel = `.cai-bitem[data-id="${b.id}"] .cai-bdemo`;
const aiBgColor = b.aiBg || '#ffffff', aiTxColor = b.aiTx || '#000000', spColor = b.spDef || '#000000';
gridCSS += `${demoSel} { --cai-bg: ${aiBgColor}; --cai-tx: ${aiTxColor}; --cai-sp: ${spColor}; font-family: '${b.font}', sans-serif; color: var(--cai-tx); background: var(--cai-bg); }`;
if (b.emojiCat) {
let eCSS = buildEmojiCSS({ customEmoji: [b.defaultEmoji], emojiAngle: 0, bgAlpha: 0.85, aiColor: aiBgColor }, b);
eCSS = eCSS.replace(/\.cai-ai/g, demoSel).replace(/\.cai-user[\s\S]*?(?=\.cai-|$)/, '');
gridCSS += eCSS;
}
let bStyle = (b.css || '').replace(/\.cai-user[^{]*\{[^}]*\}/g, '').replace(/\.cai-msg/g, demoSel).replace(/\.cai-ai/g, demoSel);
gridCSS += bStyle;
const name = document.createElement('div');
name.className = 'cai-bname'; name.textContent = b.name;
item.appendChild(demo); item.appendChild(name);
item.addEventListener('click', () => selectBubble(b.id));
grid.appendChild(item);
});
blist.appendChild(grid);
});
const gridStyleTag = document.createElement('style');
gridStyleTag.id = 'cai-grid-styles';
gridStyleTag.textContent = gridCSS;
modal.appendChild(gridStyleTag);
const hideEditToggle = $('cai-hide-edited');
if (hideEditToggle) hideEditToggle.checked = tempCfg.hideEditedMarks !== undefined ? !!tempCfg.hideEditedMarks : true;
function resetAdvBubbleOptions() {
// ── Spacing / Style / Alignment ──
tempCfg.msgSpacing = DEFAULT_MSG_SPACING;
mspacingInp.value = DEFAULT_MSG_SPACING; mspacingVal.textContent = DEFAULT_MSG_SPACING;
tempCfg.bubbleStyle = 'classic';
styleClassicInp.checked = true; styleDenseInp.checked = false;
tempCfg.bubbleAlign = 'default';
alignDefaultInp.checked = true; alignLeftInp.checked = false; alignRightInp.checked = false; alignReverseInp.checked = false;
// ── Border ──
tempCfg.borderEnabled = false;
borderToggle.checked = false; borderControls.style.display = 'none';
tempCfg.borderMode = 'dual';
borderModeSel.value = 'dual'; borderDualWrap.style.display = 'flex'; borderIndividualWrap.style.display = 'none';
tempCfg.borderWidth = DEFAULT_BORDER_WIDTH; tempCfg.borderWidthAi = DEFAULT_BORDER_WIDTH; tempCfg.borderWidthUser = DEFAULT_BORDER_WIDTH;
borderWidthInp.value = DEFAULT_BORDER_WIDTH; borderWidthVal.textContent = DEFAULT_BORDER_WIDTH;
borderWidthAiInp.value = DEFAULT_BORDER_WIDTH; borderWidthAiVal.textContent = DEFAULT_BORDER_WIDTH;
borderWidthUserInp.value = DEFAULT_BORDER_WIDTH; borderWidthUserVal.textContent = DEFAULT_BORDER_WIDTH;
tempCfg.borderColor = '#ffffff'; tempCfg.borderColorAi = '#ffffff'; tempCfg.borderColorUser = '#ffffff';
borderColorInp.value = '#ffffff'; borderColorInp.dispatchEvent(new Event('input', { bubbles: true }));
borderColorAiInp.value = '#ffffff'; borderColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
borderColorUserInp.value = '#ffffff'; borderColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
// ── Outer shadow ──
tempCfg.shadowEnabled = false;
shadowToggle.checked = false; shadowControls.style.display = 'none';
tempCfg.shadowMode = 'dual';
shadowModeSel.value = 'dual'; shadowDualWrap.style.display = 'flex'; shadowIndividualWrap.style.display = 'none';
tempCfg.shadowColor = '#000000'; tempCfg.shadowColorAi = '#000000'; tempCfg.shadowColorUser = '#000000';
shadowColorInp.value = '#000000'; shadowColorInp.dispatchEvent(new Event('input', { bubbles: true }));
shadowColorAiInp.value = '#000000'; shadowColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
shadowColorUserInp.value = '#000000'; shadowColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
tempCfg.shadowSpread = DEFAULT_SHADOW_SPREAD; shadowSpreadInp.value = DEFAULT_SHADOW_SPREAD; shadowSpreadVal.textContent = DEFAULT_SHADOW_SPREAD;
tempCfg.shadowBlur = DEFAULT_SHADOW_BLUR; shadowBlurInp.value = DEFAULT_SHADOW_BLUR; shadowBlurVal.textContent = DEFAULT_SHADOW_BLUR;
tempCfg.shadowOffsetX = DEFAULT_SHADOW_OFFSET_X; shadowOffxInp.value = DEFAULT_SHADOW_OFFSET_X; shadowOffxVal.textContent = DEFAULT_SHADOW_OFFSET_X;
tempCfg.shadowOffsetY = DEFAULT_SHADOW_OFFSET_Y; shadowOffyInp.value = DEFAULT_SHADOW_OFFSET_Y; shadowOffyVal.textContent = DEFAULT_SHADOW_OFFSET_Y;
// ── Inner Shadow ──
tempCfg.innerShadowEnabled = false;
innerShadowToggle.checked = false; innerShadowControls.style.display = 'none';
tempCfg.innerShadowMode = 'dual';
innerShadowModeSel.value = 'dual'; innerShadowDualWrap.style.display = 'flex'; innerShadowIndividualWrap.style.display = 'none';
tempCfg.innerShadowColor = '#000000'; tempCfg.innerShadowColorAi = '#000000'; tempCfg.innerShadowColorUser = '#000000';
innerShadowColorInp.value = '#000000'; innerShadowColorInp.dispatchEvent(new Event('input', { bubbles: true }));
innerShadowColorAiInp.value = '#000000'; innerShadowColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
innerShadowColorUserInp.value = '#000000'; innerShadowColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
tempCfg.innerShadowSpread = DEFAULT_INNER_SHADOW_SPREAD; innerShadowSpreadInp.value = DEFAULT_INNER_SHADOW_SPREAD; innerShadowSpreadVal.textContent = DEFAULT_INNER_SHADOW_SPREAD;
tempCfg.innerShadowBlur = DEFAULT_INNER_SHADOW_BLUR; innerShadowBlurInp.value = DEFAULT_INNER_SHADOW_BLUR; innerShadowBlurVal.textContent = DEFAULT_INNER_SHADOW_BLUR;
tempCfg.innerShadowOffsetX = DEFAULT_INNER_SHADOW_OFFSET_X; innerShadowOffxInp.value = DEFAULT_INNER_SHADOW_OFFSET_X; innerShadowOffxVal.textContent = DEFAULT_INNER_SHADOW_OFFSET_X;
tempCfg.innerShadowOffsetY = DEFAULT_INNER_SHADOW_OFFSET_Y; innerShadowOffyInp.value = DEFAULT_INNER_SHADOW_OFFSET_Y; innerShadowOffyVal.textContent = DEFAULT_INNER_SHADOW_OFFSET_Y;
// ── Corners ──
tempCfg.cornersEnabled = false;
cornersToggle.checked = false; cornersControls.style.display = 'none';
tempCfg.cornersScope = 'dual';
cornersScopeSel.value = 'dual'; cornersDualWrap.style.display = 'flex'; cornersIndividualWrap.style.display = 'none';
tempCfg.cornersMode = 'uniform';
cornersUniformInp.checked = true; cornersCustomInp.checked = false; cornersUniformWrap.style.display = 'block'; cornersCustomWrap.style.display = 'none';
tempCfg.cornerRadius = DEFAULT_CORNER_RADIUS; cornerRadiusInp.value = DEFAULT_CORNER_RADIUS; cornerRadiusVal.textContent = DEFAULT_CORNER_RADIUS;
tempCfg.cornerRadiusTL = tempCfg.cornerRadiusTR = tempCfg.cornerRadiusBL = tempCfg.cornerRadiusBR = DEFAULT_CORNER_RADIUS;
cornerTLInp.value = cornerTRInp.value = cornerBLInp.value = cornerBRInp.value = DEFAULT_CORNER_RADIUS;
cornerTLVal.textContent = cornerTRVal.textContent = cornerBLVal.textContent = cornerBRVal.textContent = DEFAULT_CORNER_RADIUS;
tempCfg.cornersModeAi = 'uniform';
cornersUniformAiInp.checked = true; cornersCustomAiInp.checked = false; cornersUniformWrapAi.style.display = 'block'; cornersCustomWrapAi.style.display = 'none';
tempCfg.cornerRadiusAi = DEFAULT_CORNER_RADIUS; cornerRadiusAiInp.value = DEFAULT_CORNER_RADIUS; cornerRadiusAiVal.textContent = DEFAULT_CORNER_RADIUS;
tempCfg.cornerRadiusTLAi = tempCfg.cornerRadiusTRAi = tempCfg.cornerRadiusBLAi = tempCfg.cornerRadiusBRAi = DEFAULT_CORNER_RADIUS;
cornerTLAiInp.value = cornerTRAiInp.value = cornerBLAiInp.value = cornerBRAiInp.value = DEFAULT_CORNER_RADIUS;
cornerTLAiVal.textContent = cornerTRAiVal.textContent = cornerBLAiVal.textContent = cornerBRAiVal.textContent = DEFAULT_CORNER_RADIUS;
tempCfg.cornersModeUser = 'uniform';
cornersUniformUserInp.checked = true; cornersCustomUserInp.checked = false; cornersUniformWrapUser.style.display = 'block'; cornersCustomWrapUser.style.display = 'none';
tempCfg.cornerRadiusUser = DEFAULT_CORNER_RADIUS; cornerRadiusUserInp.value = DEFAULT_CORNER_RADIUS; cornerRadiusUserVal.textContent = DEFAULT_CORNER_RADIUS;
tempCfg.cornerRadiusTLUser = tempCfg.cornerRadiusTRUser = tempCfg.cornerRadiusBLUser = tempCfg.cornerRadiusBRUser = DEFAULT_CORNER_RADIUS;
cornerTLUserInp.value = cornerTRUserInp.value = cornerBLUserInp.value = cornerBRUserInp.value = DEFAULT_CORNER_RADIUS;
cornerTLUserVal.textContent = cornerTRUserVal.textContent = cornerBLUserVal.textContent = cornerBRUserVal.textContent = DEFAULT_CORNER_RADIUS;
// ── Font tab ──
tempCfg.fontScope = 'dual';
fontScopeSel.value = 'dual'; fontDualWrap.style.display = 'flex'; fontIndividualWrap.style.display = 'none';
tempCfg.lineHeight = DEFAULT_LINE_HEIGHT; lhInp.value = DEFAULT_LINE_HEIGHT; lhVal.textContent = DEFAULT_LINE_HEIGHT.toFixed(1);
tempCfg.letterSpacing = DEFAULT_LETTER_SPACING; lsInp.value = DEFAULT_LETTER_SPACING; lsVal.textContent = DEFAULT_LETTER_SPACING.toFixed(1) + 'px';
tempCfg.textAlign = 'left'; talignLeftInp.checked = true; talignCenterInp.checked = false; talignRightInp.checked = false;
tempCfg.fontAi = ''; tempCfg.fontUser = ''; fontAiInp.value = ''; fontUserInp.value = '';
tempCfg.fontSizeAi = DEFAULT_FONT_SIZE; tempCfg.fontSizeUser = DEFAULT_FONT_SIZE;
fsAiVal.value = DEFAULT_FONT_SIZE; fsUserVal.value = DEFAULT_FONT_SIZE;
tempCfg.lineHeightAi = DEFAULT_LINE_HEIGHT; tempCfg.lineHeightUser = DEFAULT_LINE_HEIGHT;
lhAiInp.value = DEFAULT_LINE_HEIGHT; lhAiVal.textContent = DEFAULT_LINE_HEIGHT.toFixed(1);
lhUserInp.value = DEFAULT_LINE_HEIGHT; lhUserVal.textContent = DEFAULT_LINE_HEIGHT.toFixed(1);
tempCfg.letterSpacingAi = DEFAULT_LETTER_SPACING; tempCfg.letterSpacingUser = DEFAULT_LETTER_SPACING;
lsAiInp.value = DEFAULT_LETTER_SPACING; lsAiVal.textContent = DEFAULT_LETTER_SPACING.toFixed(1) + 'px';
lsUserInp.value = DEFAULT_LETTER_SPACING; lsUserVal.textContent = DEFAULT_LETTER_SPACING.toFixed(1) + 'px';
tempCfg.textAlignAi = 'left'; tempCfg.textAlignUser = 'left';
talignAiLeftInp.checked = true; talignAiCenterInp.checked = false; talignAiRightInp.checked = false;
talignUserLeftInp.checked = true; talignUserCenterInp.checked = false; talignUserRightInp.checked = false;
tempCfg.italicBoldEnabled = false; italicBoldToggle.checked = false; italicBoldColors.style.display = 'none';
tempCfg.italicColor = '#ffffff'; tempCfg.boldColor = '#ffffff';
italicColorInp.value = '#ffffff'; italicColorInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorInp.value = '#ffffff'; boldColorInp.dispatchEvent(new Event('input', { bubbles: true }));
tempCfg.italicBoldEnabledAi = false; italicBoldToggleAi.checked = false; italicBoldColorsAi.style.display = 'none';
tempCfg.italicColorAi = '#ffffff'; tempCfg.boldColorAi = '#ffffff';
italicColorAiInp.value = '#ffffff'; italicColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorAiInp.value = '#ffffff'; boldColorAiInp.dispatchEvent(new Event('input', { bubbles: true }));
tempCfg.italicBoldEnabledUser = false; italicBoldToggleUser.checked = false; italicBoldColorsUser.style.display = 'none';
tempCfg.italicColorUser = '#ffffff'; tempCfg.boldColorUser = '#ffffff';
italicColorUserInp.value = '#ffffff'; italicColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
boldColorUserInp.value = '#ffffff'; boldColorUserInp.dispatchEvent(new Event('input', { bubbles: true }));
}
function selectBubble(id) {
selId = id;
updateAdvBubbleTabVisibility(id); // ← línea nueva
modal.querySelectorAll('.cai-bitem').forEach(el => el.classList.toggle('selected', +el.dataset.id === id));
modal.querySelectorAll('.cai-bitem').forEach(el => el.classList.toggle('selected', +el.dataset.id === id));
const sel = modal.querySelector(`.cai-bitem[data-id="${id}"]`);
if (sel) sel.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
const def = BUBBLES.find(b => b.id === id); if (!def) return;
const useSaved = (id === (loadStyle() || {}).bubbleId);
if (!useSaved) resetAdvBubbleOptions();
colorBlock.style.display = '';
fontInp.value = useSaved && tempCfg.font ? tempCfg.font : (def.font || '');
fsVal.value = useSaved && tempCfg.fontSize ? tempCfg.fontSize : DEFAULT_FONT_SIZE;
const bgLocked = !!def.colorLock;
bgLockedNote.style.display = bgLocked ? 'block' : 'none';
bgPickers.style.display = bgLocked ? 'none' : 'flex';
blurWrap.style.display = bgLocked ? 'none' : '';
if (!bgLocked) {
aiBgInp.value = useSaved && tempCfg.aiColor ? tempCfg.aiColor : (def.aiBg || '#ffffff');
uBgInp.value = useSaved && tempCfg.userColor ? tempCfg.userColor : (def.uBg || '#ffffff');
}
aiTxInp.disabled = false;
uTxInp.disabled = false;
aiTxInp.style.opacity = '1';
uTxInp.style.opacity = '1';
aiTxInp.value = useSaved && tempCfg.aiTextColor ? tempCfg.aiTextColor : (def.aiTx || '#ffffff');
uTxInp.value = useSaved && tempCfg.userTextColor ? tempCfg.userTextColor : (def.uTx || '#ffffff');
tempCfg.font = fontInp.value;
tempCfg.fontSize = clampFontSize(fsVal.value);
if (!bgLocked) { tempCfg.aiColor = aiBgInp.value; tempCfg.userColor = uBgInp.value; }
tempCfg.aiTextColor = aiTxInp.value;
tempCfg.userTextColor = uTxInp.value;
if (def.spName) {
spSec.style.display = 'flex'; spLabel.textContent = def.spName;
spInp.value = useSaved && tempCfg.spColor ? tempCfg.spColor : (def.spDef || '#000000');
tempCfg.spColor = spInp.value;
} else { spSec.style.display = 'none'; }
if (def.spName) {
spSec.style.display = 'flex'; spLabel.textContent = def.spName;
spInp.value = useSaved && tempCfg.spColor ? tempCfg.spColor : (def.spDef || '#000000');
tempCfg.spColor = spInp.value;
spInp.dispatchEvent(new Event('input', { bubbles: true }));
} else { spSec.style.display = 'none'; }
if (!bgLocked) {
aiBgInp.dispatchEvent(new Event('input', { bubbles: true }));
uBgInp.dispatchEvent(new Event('input', { bubbles: true }));
}
aiTxInp.dispatchEvent(new Event('input', { bubbles: true }));
uTxInp.dispatchEvent(new Event('input', { bubbles: true }));
if (def.emojiCat) {
eSec.style.display = 'block';
customEmojis = useSaved && tempCfg.customEmoji && tempCfg.customEmoji.length ? tempCfg.customEmoji : [def.defaultEmoji || '⭐'];
tempCfg.customEmoji = customEmojis; renderEmoji();
} else { eSec.style.display = 'none'; }
updatePreview();
}
let cai_previewRAF = null;
function scheduleUpdatePreview() {
if (cai_previewRAF) return;
cai_previewRAF = requestAnimationFrame(() => { cai_previewRAF = null; updatePreview(); });
}
function updatePreview() {
const def = BUBBLES.find(b => b.id === selId); if (!def) return;
const fontScopeVal = fontScopeSel.value === 'individual' ? 'individual' : 'dual';
const font = fontInp.value || def.font || '';
const fontSizeVal = clampFontSize(fsVal.value);
const lineHeightVal = clampLineHeight(lhInp.value);
const letterSpacingVal = clampLetterSpacing(lsInp.value);
const textAlignVal = talignCenterInp.checked ? 'center' : (talignRightInp.checked ? 'right' : 'left');
const fontFaceCSS = font ? `font-family: '${font}', sans-serif;` : '';
const fontAiVal = fontScopeVal === 'individual' ? (fontAiInp.value || def.font || '') : font;
const fontUserVal = fontScopeVal === 'individual' ? (fontUserInp.value || def.font || '') : font;
const fontFaceAiCSS = fontAiVal ? `font-family: '${fontAiVal}', sans-serif;` : '';
const fontFaceUserCSS = fontUserVal ? `font-family: '${fontUserVal}', sans-serif;` : '';
const fontSizeAiVal = fontScopeVal === 'individual' ? clampFontSize(fsAiVal.value) : fontSizeVal;
const fontSizeUserVal = fontScopeVal === 'individual' ? clampFontSize(fsUserVal.value) : fontSizeVal;
const lineHeightAiVal = fontScopeVal === 'individual' ? clampLineHeight(lhAiInp.value) : lineHeightVal;
const lineHeightUserVal = fontScopeVal === 'individual' ? clampLineHeight(lhUserInp.value) : lineHeightVal;
const letterSpacingAiVal = fontScopeVal === 'individual' ? clampLetterSpacing(lsAiInp.value) : letterSpacingVal;
const letterSpacingUserVal = fontScopeVal === 'individual' ? clampLetterSpacing(lsUserInp.value) : letterSpacingVal;
const textAlignAiVal = fontScopeVal === 'individual' ? (talignAiCenterInp.checked ? 'center' : (talignAiRightInp.checked ? 'right' : 'left')) : textAlignVal;
const textAlignUserVal = fontScopeVal === 'individual' ? (talignUserCenterInp.checked ? 'center' : (talignUserRightInp.checked ? 'right' : 'left')) : textAlignVal;
const aiBgVal = def.colorLock ? (def.aiBg || '#ffffff') : aiBgInp.value;
const uBgVal = def.colorLock ? (def.uBg || '#ffffff') : uBgInp.value;
const aiTxVal = aiTxInp.value;
const uTxVal = uTxInp.value;
const blurCSS = blurToggle.checked ? `backdrop-filter: blur(${blurAmountInp.value}px) !important; -webkit-backdrop-filter: blur(${blurAmountInp.value}px) !important;` : '';
const borderAiCSS = borderToggle.checked ? `border: ${borderModeSel.value === 'individual' ? borderWidthAiInp.value : borderWidthInp.value}px solid ${borderModeSel.value === 'individual' ? borderColorAiInp.value : borderColorInp.value} !important;` : '';
const borderUserCSS = borderToggle.checked ? `border: ${borderModeSel.value === 'individual' ? borderWidthUserInp.value : borderWidthInp.value}px solid ${borderModeSel.value === 'individual' ? borderColorUserInp.value : borderColorInp.value} !important;` : '';
const shadowAiParts = [];
const shadowUserParts = [];
if (shadowToggle.checked) {
shadowAiParts.push(`${shadowOffxInp.value}px ${shadowOffyInp.value}px ${shadowBlurInp.value}px ${shadowSpreadInp.value}px ${shadowModeSel.value === 'individual' ? shadowColorAiInp.value : shadowColorInp.value}`);
shadowUserParts.push(`${shadowOffxInp.value}px ${shadowOffyInp.value}px ${shadowBlurInp.value}px ${shadowSpreadInp.value}px ${shadowModeSel.value === 'individual' ? shadowColorUserInp.value : shadowColorInp.value}`);
}
if (innerShadowToggle.checked) {
shadowAiParts.push(`inset ${innerShadowOffxInp.value}px ${innerShadowOffyInp.value}px ${innerShadowBlurInp.value}px ${innerShadowSpreadInp.value}px ${innerShadowModeSel.value === 'individual' ? innerShadowColorAiInp.value : innerShadowColorInp.value}`);
shadowUserParts.push(`inset ${innerShadowOffxInp.value}px ${innerShadowOffyInp.value}px ${innerShadowBlurInp.value}px ${innerShadowSpreadInp.value}px ${innerShadowModeSel.value === 'individual' ? innerShadowColorUserInp.value : innerShadowColorInp.value}`);
}
const shadowAiCSS = shadowAiParts.length ? `box-shadow: ${shadowAiParts.join(', ')} !important;` : '';
const shadowUserCSS = shadowUserParts.length ? `box-shadow: ${shadowUserParts.join(', ')} !important;` : '';
let prevStyle = document.getElementById('cai-modal-prev-css');
if (!prevStyle) { prevStyle = document.createElement('style'); prevStyle.id = 'cai-modal-prev-css'; document.head.appendChild(prevStyle); }
const msgSpacingVal = clampMsgSpacing(mspacingInp.value);
let cssStr = `
#cai-pai { --cai-bg: ${aiBgVal}; --cai-tx: ${aiTxVal}; --cai-sp: ${spInp.value}; ${fontFaceAiCSS} font-size: ${fontSizeAiVal}px !important; line-height: ${lineHeightAiVal} !important; letter-spacing: ${letterSpacingAiVal}px !important; text-align: ${textAlignAiVal} !important; ${blurCSS} ${borderAiCSS} ${shadowAiCSS} }
#cai-puser{ --cai-bg: ${uBgVal}; --cai-tx: ${uTxVal}; --cai-sp: ${spInp.value}; ${fontFaceUserCSS} font-size: ${fontSizeUserVal}px !important; line-height: ${lineHeightUserVal} !important; letter-spacing: ${letterSpacingUserVal}px !important; text-align: ${textAlignUserVal} !important; ${blurCSS} ${borderUserCSS} ${shadowUserCSS} margin-top: ${msgSpacingVal}px !important; }
`;
if (fontScopeVal === 'individual') {
if (italicBoldToggleAi.checked) cssStr += `#cai-pai em { color: ${italicColorAiInp.value} !important; } #cai-pai strong { color: ${boldColorAiInp.value} !important; }`;
if (italicBoldToggleUser.checked) cssStr += `#cai-puser em { color: ${italicColorUserInp.value} !important; } #cai-puser strong { color: ${boldColorUserInp.value} !important; }`;
} else if (italicBoldToggle.checked) {
cssStr += `:is(#cai-pai, #cai-puser) em { color: ${italicColorInp.value} !important; } :is(#cai-pai, #cai-puser) strong { color: ${boldColorInp.value} !important; }`;
}
let shared = (def.css || '').replace(/\.cai-msg/g, ':is(#cai-pai, #cai-puser)').replace(/\.cai-ai/g, '#cai-pai').replace(/\.cai-user/g, '#cai-puser');
cssStr += shared;
if (selId !== 95) {
cssStr += `#cai-pai { color: ${aiTxVal} !important; } #cai-puser { color: ${uTxVal} !important; }`;
}
if (cornersToggle.checked && cornersScopeSel.value === 'dual') {
cssStr += cornersUniformInp.checked
? `:is(#cai-pai, #cai-puser) { border-radius: ${cornerRadiusInp.value}px !important; }`
: `:is(#cai-pai, #cai-puser) { border-radius: ${cornerTLInp.value}px ${cornerTRInp.value}px ${cornerBRInp.value}px ${cornerBLInp.value}px !important; }`;
} else if (cornersToggle.checked && cornersScopeSel.value === 'individual') {
cssStr += cornersUniformAiInp.checked
? `#cai-pai { border-radius: ${cornerRadiusAiInp.value}px !important; }`
: `#cai-pai { border-radius: ${cornerTLAiInp.value}px ${cornerTRAiInp.value}px ${cornerBRAiInp.value}px ${cornerBLAiInp.value}px !important; }`;
cssStr += cornersUniformUserInp.checked
? `#cai-puser { border-radius: ${cornerRadiusUserInp.value}px !important; }`
: `#cai-puser { border-radius: ${cornerTLUserInp.value}px ${cornerTRUserInp.value}px ${cornerBRUserInp.value}px ${cornerBLUserInp.value}px !important; }`;
}
if (def.emojiCat) {
let eCSS = buildEmojiCSS({ ...tempCfg, aiColor: aiBgVal, userColor: uBgVal }, def)
.replace(/\.cai-ai/g, '#cai-pai').replace(/\.cai-user/g, '#cai-puser');
cssStr += eCSS;
}
prevStyle.textContent = cssStr;
}
function renderEmoji() {
eGrid.innerHTML = '';
customEmojis.forEach((e, i) => {
const chip = document.createElement('div'); chip.className = 'cai-echip';
chip.innerHTML = e + ' <span>✕</span>';
chip.addEventListener('click', () => { customEmojis.splice(i,1); tempCfg.customEmoji = customEmojis; renderEmoji(); updatePreview(); });
eGrid.appendChild(chip);
});
}
eAdd.addEventListener('click', () => {
const v = eInput.value.trim();
if (!v || customEmojis.length >= 20) return;
customEmojis.push(v); tempCfg.customEmoji = customEmojis; eInput.value = ''; renderEmoji(); updatePreview();
});
eInput.addEventListener('keydown', e => { if (e.key === 'Enter') eAdd.click(); });
angleSlider.addEventListener('input', () => { angleVal.textContent = angleSlider.value + '°'; tempCfg.emojiAngle = +angleSlider.value; scheduleUpdatePreview(); });
alphaSlider.addEventListener('input', () => { alphaValEl.textContent = alphaSlider.value + '%'; tempCfg.bgAlpha = +alphaSlider.value / 100; scheduleUpdatePreview(); });
if (tempCfg.emojiAngle !== undefined) { angleSlider.value = tempCfg.emojiAngle; angleVal.textContent = tempCfg.emojiAngle + '°'; }
if (tempCfg.bgAlpha !== undefined) { alphaSlider.value = Math.round(tempCfg.bgAlpha*100); alphaValEl.textContent = alphaSlider.value + '%'; }
fsDec.addEventListener('click', () => {
fsVal.value = clampFontSize((+fsVal.value || DEFAULT_FONT_SIZE) - 1);
fsVal.dispatchEvent(new Event('input', { bubbles: true }));
});
fsInc.addEventListener('click', () => {
fsVal.value = clampFontSize((+fsVal.value || DEFAULT_FONT_SIZE) + 1);
fsVal.dispatchEvent(new Event('input', { bubbles: true }));
});
fsVal.addEventListener('keydown', e => { if (e.key === 'Enter') fsVal.blur(); });
fsVal.addEventListener('change', () => {
fsVal.value = clampFontSize(fsVal.value);
fsVal.dispatchEvent(new Event('input', { bubbles: true }));
});
[aiBgInp, aiTxInp, uBgInp, uTxInp, spInp, fontInp, fsVal].forEach(el => el.addEventListener('input', () => {
const def = BUBBLES.find(b => b.id === selId);
tempCfg.font = fontInp.value;
if (def && !def.colorLock) { tempCfg.aiColor = aiBgInp.value; tempCfg.userColor = uBgInp.value; }
tempCfg.aiTextColor = aiTxInp.value;
tempCfg.userTextColor = uTxInp.value;
tempCfg.spColor = spInp.value;
tempCfg.fontSize = clampFontSize(fsVal.value);
scheduleUpdatePreview();
}));
selectBubble(selId);
// ─── SAVED PRESETS ───
const presetSaveBtn = $('cai-preset-save');
const presetDeleteAllBtn = $('cai-preset-delete-all');
const presetList = $('cai-preset-list');
const presetLoadBtn = $('cai-preset-load');
const presetFileInput = $('cai-preset-file-input');
function formatPresetTimestamp() {
const d = new Date();
const p = n => String(n).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}.${ms}`;
}
function downloadPresetFile(name, payload) {
const content = JSON.stringify(payload, null, 2);
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (name || 'cai_preset') + '.json';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function addPresetRow(id, name) {
const row = document.createElement('div');
row.className = 'cai-preset-row';
row.dataset.id = id;
row.innerHTML = `
<button type="button" class="cai-preset-row-btn cai-preset-apply" title="Load"><span class="material-icons-outlined">download_done</span></button>
<button type="button" class="cai-preset-row-btn cai-preset-download" title="Download"><span class="material-icons-outlined">save_alt</span></button>
<input type="text" class="cai-preset-row-input" value="${name}" maxlength="60"/>
<button type="button" class="cai-preset-row-btn cai-preset-delete" title="Delete"><span class="material-icons-outlined">delete</span></button>
`;
row.querySelector('.cai-preset-apply').addEventListener('click', () => {
const entry = loadPresets().find(p => p.id === id);
if (entry) applyPresetToActiveBot(entry.payload);
});
row.querySelector('.cai-preset-download').addEventListener('click', () => {
const entry = loadPresets().find(p => p.id === id);
if (entry) downloadPresetFile(entry.name, entry.payload);
});
row.querySelector('.cai-preset-delete').addEventListener('click', () => {
savePresets(loadPresets().filter(p => p.id !== id));
row.remove();
});
row.querySelector('.cai-preset-row-input').addEventListener('change', (e) => {
const list = loadPresets();
const entry = list.find(p => p.id === id);
if (entry) { entry.name = e.target.value; savePresets(list); }
});
presetList.prepend(row);
}
function renderSavedPresets() {
presetList.innerHTML = '';
loadPresets().forEach(p => addPresetRow(p.id, p.name));
}
renderSavedPresets();
presetSaveBtn.addEventListener('click', () => {
const id = 'preset_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
const name = formatPresetTimestamp();
const payload = {
style: buildStyleCfg()
};
const list = loadPresets();
list.push({ id, name, payload });
savePresets(list);
addPresetRow(id, name);
caiToast('Preset saved!');
});
presetDeleteAllBtn.addEventListener('click', () => {
if (!confirm('Delete all saved presets? This action cannot be undone.')) return;
savePresets([]);
presetList.innerHTML = '';
});
const close = () => {
$('cai-mbox') ? ($('cai-mbox').style.animation = 'caiModalOut 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards') : null;
document.removeEventListener('mousemove', handleAvatarMouseMove);
document.removeEventListener('mouseup', handleAvatarMouseUp);
document.removeEventListener('mousemove', handleAvatarUserMouseMove);
document.removeEventListener('mouseup', handleAvatarUserMouseUp);
setTimeout(() => { const p = document.getElementById('cai-modal-prev-css'); if (p) p.remove(); modal.remove(); }, 200);
};
closeBtn.addEventListener('click', close);
cancelBtn.addEventListener('click', close);
modal.addEventListener('click', e => { if (e.target === modal) close(); });
resetBtn.addEventListener('click', () => {
const all = loadAllStyles(); delete all[getCharKey()];
GM_setValue(STORAGE_KEY, JSON.stringify(all));
// ─── RESET AVATAR (BOT + USER) ───
if (avatarActiveBotId) {
localStorage.removeItem('customBotAvatarConfig_' + avatarActiveBotId);
localStorage.removeItem('customBotNameConfig_' + avatarActiveBotId);
localStorage.removeItem('customUserAvatarConfig_' + avatarActiveBotId);
localStorage.removeItem('customUserNameConfig_' + avatarActiveBotId);
const linkedCharId = localStorage.getItem('charId_for_' + avatarActiveBotId);
if (linkedCharId) {
localStorage.removeItem('customBotAvatarConfig_' + linkedCharId);
localStorage.removeItem('customBotNameConfig_' + linkedCharId);
localStorage.removeItem('customUserAvatarConfig_' + linkedCharId);
localStorage.removeItem('customUserNameConfig_' + linkedCharId);
localStorage.removeItem('charId_for_' + avatarActiveBotId);
}
}
applyStyle(null); triggerReplacementSafely(); caiToast('Reseted.'); close();
});
function buildStyleCfg() {
const def = BUBBLES.find(b => b.id === selId);
let finalBgType = currentBgMode;
if (currentBgMode === 'none' && bgColorInp.value.trim()) finalBgType = 'color';
const cfg = {
bubbleId: selId,
font: fontInp.value || def.font,
bgType: finalBgType,
bgUrl: bgUrl.value.trim(),
bgColor: bgColorInp.value.trim(),
bgOpacity: +bgOpacityInp.value / 100,
bgBrightness: +bgBrightInp.value / 100,
bgContrast: +bgContrastInp.value / 100,
bgSaturation: +bgSaturationInp.value / 100,
bgBlur: +bgBlurInp.value,
bgFit: getBgFit(),
customEmoji: [...customEmojis],
emojiAngle: +angleSlider.value,
bgAlpha: +alphaSlider.value / 100,
aiTextColor: aiTxInp.value,
userTextColor: uTxInp.value,
fontSize: clampFontSize(fsVal.value),
lineHeight: clampLineHeight(lhInp.value),
msgSpacing: clampMsgSpacing(mspacingInp.value),
letterSpacing: clampLetterSpacing(lsInp.value),
textAlign: talignCenterInp.checked ? 'center' : (talignRightInp.checked ? 'right' : 'left'),
fontScope: fontScopeSel.value === 'individual' ? 'individual' : 'dual',
fontAi: fontAiInp.value,
fontUser: fontUserInp.value,
fontSizeAi: clampFontSize(fsAiVal.value),
fontSizeUser: clampFontSize(fsUserVal.value),
lineHeightAi: clampLineHeight(lhAiInp.value),
lineHeightUser: clampLineHeight(lhUserInp.value),
letterSpacingAi: clampLetterSpacing(lsAiInp.value),
letterSpacingUser: clampLetterSpacing(lsUserInp.value),
textAlignAi: talignAiCenterInp.checked ? 'center' : (talignAiRightInp.checked ? 'right' : 'left'),
textAlignUser: talignUserCenterInp.checked ? 'center' : (talignUserRightInp.checked ? 'right' : 'left'),
italicBoldEnabled: italicBoldToggle.checked,
italicColor: italicColorInp.value,
boldColor: boldColorInp.value,
italicBoldEnabledAi: italicBoldToggleAi.checked,
italicColorAi: italicColorAiInp.value,
boldColorAi: boldColorAiInp.value,
italicBoldEnabledUser: italicBoldToggleUser.checked,
italicColorUser: italicColorUserInp.value,
boldColorUser: boldColorUserInp.value,
blurEnabled: blurToggle.checked,
blurAmount: +blurAmountInp.value,
borderEnabled: borderToggle.checked,
borderMode: borderModeSel.value,
borderWidth: +borderWidthInp.value,
borderWidthAi: +borderWidthAiInp.value,
borderWidthUser: +borderWidthUserInp.value,
borderColor: borderColorInp.value,
borderColorAi: borderColorAiInp.value,
borderColorUser: borderColorUserInp.value,
shadowEnabled: shadowToggle.checked,
shadowMode: shadowModeSel.value,
shadowColor: shadowColorInp.value,
shadowColorAi: shadowColorAiInp.value,
shadowColorUser: shadowColorUserInp.value,
shadowSpread: clampShadowSpread(shadowSpreadInp.value),
shadowBlur: clampShadowBlur(shadowBlurInp.value),
shadowOffsetX: clampShadowOffset(shadowOffxInp.value),
shadowOffsetY: clampShadowOffset(shadowOffyInp.value),
innerShadowEnabled: innerShadowToggle.checked,
innerShadowMode: innerShadowModeSel.value,
innerShadowColor: innerShadowColorInp.value,
innerShadowColorAi: innerShadowColorAiInp.value,
innerShadowColorUser: innerShadowColorUserInp.value,
innerShadowSpread: clampInnerShadowSpread(innerShadowSpreadInp.value),
innerShadowBlur: clampInnerShadowBlur(innerShadowBlurInp.value),
innerShadowOffsetX: clampInnerShadowOffset(innerShadowOffxInp.value),
innerShadowOffsetY: clampInnerShadowOffset(innerShadowOffyInp.value),
cornersEnabled: cornersToggle.checked,
cornersScope: cornersScopeSel.value,
cornersMode: cornersUniformInp.checked ? 'uniform' : 'custom',
cornerRadius: clampCornerRadius(cornerRadiusInp.value),
cornerRadiusTL: clampCornerRadius(cornerTLInp.value),
cornerRadiusTR: clampCornerRadius(cornerTRInp.value),
cornerRadiusBL: clampCornerRadius(cornerBLInp.value),
cornerRadiusBR: clampCornerRadius(cornerBRInp.value),
cornersModeAi: cornersUniformAiInp.checked ? 'uniform' : 'custom',
cornersModeUser: cornersUniformUserInp.checked ? 'uniform' : 'custom',
cornerRadiusAi: clampCornerRadius(cornerRadiusAiInp.value),
cornerRadiusUser: clampCornerRadius(cornerRadiusUserInp.value),
cornerRadiusTLAi: clampCornerRadius(cornerTLAiInp.value),
cornerRadiusTRAi: clampCornerRadius(cornerTRAiInp.value),
cornerRadiusBLAi: clampCornerRadius(cornerBLAiInp.value),
cornerRadiusBRAi: clampCornerRadius(cornerBRAiInp.value),
cornerRadiusTLUser: clampCornerRadius(cornerTLUserInp.value),
cornerRadiusTRUser: clampCornerRadius(cornerTRUserInp.value),
cornerRadiusBLUser: clampCornerRadius(cornerBLUserInp.value),
cornerRadiusBRUser: clampCornerRadius(cornerBRUserInp.value),
avatarSize: clampAvatarIdx(avatarSizeInp.value),
avatarForceMobile: avatarForceMobileInp.checked,
hideAvatarsDesktop: hideAvatarsDesktopInp.checked,
hideNames: hideNamesInp.checked,
replaceAuthorMobile: replaceAuthorMobileInp.checked,
hideEditedMarks: hideEditToggle.checked,
enterKeySend: enterKeySendInp.checked,
bubbleStyle: tempCfg.bubbleStyle === 'dense' ? 'dense' : 'classic',
bubbleAlign: ['left','right','reverse'].includes(tempCfg.bubbleAlign) ? tempCfg.bubbleAlign : 'default',
};
if (!def.colorLock) {
cfg.aiColor = aiBgInp.value;
cfg.userColor = uBgInp.value;
}
if (def.spName) { cfg.spColor = spInp.value; }
return cfg;
}
function applyPresetToActiveBot(payload) {
if (!BUBBLES.some(b => b.id === payload.style.bubbleId)) {
caiToast('Invalid preset: unknown theme');
return;
}
saveStyle(payload.style);
applyStyle(payload.style);
triggerReplacementSafely();
caiToast('Preset loaded!');
close();
setTimeout(buildModal, 250);
}
presetLoadBtn.addEventListener('click', () => presetFileInput.click());
presetFileInput.addEventListener('change', () => {
const file = presetFileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
let payload;
try { payload = JSON.parse(reader.result.trim()); }
catch { caiToast('Invalid preset file'); presetFileInput.value = ''; return; }
if (!payload || !payload.style) { caiToast('Invalid preset file'); presetFileInput.value = ''; return; }
const id = 'preset_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
const name = file.name.replace(/\.json$/i, '') || formatPresetTimestamp();
const list = loadPresets();
list.push({ id, name, payload });
savePresets(list);
addPresetRow(id, name);
applyPresetToActiveBot(payload);
presetFileInput.value = '';
};
reader.readAsText(file);
});
saveBtn.addEventListener('click', () => {
const cfg = buildStyleCfg();
saveStyle(cfg); applyStyle(cfg);
GM_setValue(DRAFT_ENABLED_KEY, draftToggle.checked);
lastDraftBotId = null;
// ─── SAVE AVATAR (BOT) ───
if (avatarActiveBotId) {
const avatarNameVal = avatarNameInp.value.trim();
if (avatarNameVal) localStorage.setItem('customBotNameConfig_' + avatarActiveBotId, avatarNameVal);
else localStorage.removeItem('customBotNameConfig_' + avatarActiveBotId);
const avatarUrlVal = avatarUrlInp.value.trim();
if (avatarUrlVal) {
const avatarCfgData = {
url: avatarUrlVal,
posX: Math.round(avatarPosX), posY: Math.round(avatarPosY),
brightness: parseInt(avatarBrightInp.value), contrast: parseInt(avatarContrastInp.value),
saturation: parseInt(avatarSatInp.value),
zoom: parseInt(avatarZoomInp.value),
panX: parseFloat(avatarPanX.toFixed(2)), panY: parseFloat(avatarPanY.toFixed(2))
};
localStorage.setItem('customBotAvatarConfig_' + avatarActiveBotId, JSON.stringify(avatarCfgData));
botConfigCache.delete(avatarActiveBotId);
if (!isProfilePage()) {
const charId = getCharacterIdFromDom();
if (charId && charId !== avatarActiveBotId) {
localStorage.setItem('customBotAvatarConfig_' + charId, JSON.stringify(avatarCfgData));
localStorage.setItem('customBotNameConfig_' + charId, avatarNameVal);
localStorage.setItem('charId_for_' + avatarActiveBotId, charId);
botConfigCache.delete(charId);
}
}
} else {
localStorage.removeItem('customBotAvatarConfig_' + avatarActiveBotId);
botConfigCache.delete(avatarActiveBotId);
const linkedCharId = localStorage.getItem('charId_for_' + avatarActiveBotId);
if (linkedCharId) {
localStorage.removeItem('customBotAvatarConfig_' + linkedCharId);
localStorage.removeItem('customBotNameConfig_' + linkedCharId);
localStorage.removeItem('charId_for_' + avatarActiveBotId);
botConfigCache.delete(linkedCharId);
}
}
triggerReplacementSafely();
}
// ─── SAVE AVATAR (USER, per chat) ───
if (avatarActiveBotId) {
const userNameVal = avatarUserNameInp.value.trim();
if (userNameVal) localStorage.setItem('customUserNameConfig_' + avatarActiveBotId, userNameVal);
else localStorage.removeItem('customUserNameConfig_' + avatarActiveBotId);
const userUrlVal = avatarUserUrlInp.value.trim();
if (userUrlVal) {
const userCfgData = {
url: userUrlVal,
posX: Math.round(avatarUserPosX), posY: Math.round(avatarUserPosY),
brightness: parseInt(avatarUserBrightInp.value), contrast: parseInt(avatarUserContrastInp.value),
saturation: parseInt(avatarUserSatInp.value),
zoom: parseInt(avatarUserZoomInp.value),
panX: parseFloat(avatarUserPanX.toFixed(2)), panY: parseFloat(avatarUserPanY.toFixed(2))
};
localStorage.setItem('customUserAvatarConfig_' + avatarActiveBotId, JSON.stringify(userCfgData));
userConfigCache.delete(avatarActiveBotId);
if (!isProfilePage()) {
const charId = getCharacterIdFromDom();
if (charId && charId !== avatarActiveBotId) {
localStorage.setItem('customUserAvatarConfig_' + charId, JSON.stringify(userCfgData));
localStorage.setItem('customUserNameConfig_' + charId, userNameVal);
localStorage.setItem('charId_for_' + avatarActiveBotId, charId);
userConfigCache.delete(charId);
}
}
} else {
localStorage.removeItem('customUserAvatarConfig_' + avatarActiveBotId);
userConfigCache.delete(avatarActiveBotId);
const linkedCharId = localStorage.getItem('charId_for_' + avatarActiveBotId);
if (linkedCharId) {
localStorage.removeItem('customUserAvatarConfig_' + linkedCharId);
localStorage.removeItem('customUserNameConfig_' + linkedCharId);
userConfigCache.delete(linkedCharId);
}
}
triggerReplacementSafely();
}
caiToast('Saved!');
close();
});
}
// ─── TOAST ───
function caiToast(msg) {
let c = document.getElementById('cai-toast-container');
if (!c) {
c = document.createElement('div'); c.id = 'cai-toast-container';
c.style.cssText = 'position:fixed;bottom:32px;left:50%;transform:translateX(-50%);z-index:999999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
document.body.appendChild(c);
}
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'background:#ffffff;color:#000000;padding:12px 24px;border-radius:99px;font-family:"Onest",sans-serif;font-size:14px;font-weight:500;box-shadow:0 10px 25px rgba(0,0,0,0.2);animation:caiToastIn 0.3s cubic-bezier(0.16,1,0.3,1) forwards;';
c.appendChild(t);
if (!document.getElementById('cai-toast-css')) {
const s = document.createElement('style'); s.id = 'cai-toast-css';
s.textContent = '@keyframes caiToastIn{from{opacity:0;transform:translateY(20px) scale(0.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes caiToastOut{from{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(10px) scale(0.95)}}';
document.head.appendChild(s);
}
setTimeout(() => { t.style.animation = 'caiToastOut 0.3s cubic-bezier(0.16,1,0.3,1) forwards'; setTimeout(() => t.remove(), 300); }, 3000);
}
// ─── MESSAGE DRAFT ───
let lastDraftBotId = null;
let draftSaveTimer = null;
let draftGen = 0;
let caiWasGenerating = false;
let draftGuardUntil = 0;
let draftClearObserver = null;
let draftClearArmed = false;
let draftClearBaseline = null;
let draftClearStableTimer = null;
function restoreDraftIfNeeded() {
if (!isDraftEnabled()) { lastDraftBotId = null; return; }
const botId = getDraftBotId();
if (!botId || botId === lastDraftBotId) return;
const textarea = document.getElementById('chat-input-textarea');
if (!textarea) return;
lastDraftBotId = botId;
const draft = loadDraft(botId);
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(textarea, draft || '');
textarea.dispatchEvent(new Event('input', { bubbles: true }));
draftGuardUntil = Date.now() + 2000;
}
function setupDraftHandler() {
document.addEventListener('input', function(e) {
if (e.target.id !== 'chat-input-textarea') return;
if (!isDraftEnabled()) return;
const botId = getDraftBotId();
if (!botId) return;
const val = e.target.value;
const gen = draftGen;
clearTimeout(draftSaveTimer);
draftSaveTimer = setTimeout(() => { if (gen === draftGen) saveDraft(botId, val); }, 300);
}, true);
}
// ─── ENTER KEY BEHAVIOR ───
function insertTextAtCursor(textarea, text) {
const start = textarea.selectionStart, end = textarea.selectionEnd;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(textarea, textarea.value.slice(0, start) + text + textarea.value.slice(end));
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
function setupEnterKeyHandler() {
document.addEventListener('keydown', function(e) {
if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
if (e.target.id !== 'chat-input-textarea') return;
const style = loadStyle();
const sendOnEnter = !!(style && style.enterKeySend);
e.preventDefault();
e.stopPropagation();
if (sendOnEnter) {
const sendBtn = document.querySelector('button[data-radix-popper-side]') || document.querySelector('#chat-input-box button');
if (sendBtn) sendBtn.click();
setTimeout(clearDraftAndInputNow, 0);
} else {
insertTextAtCursor(e.target, '\n');
}
}, true);
}
// ─── SEND BUTTON CLICK: CLEAR DRAFT IMMEDIATELY ───
function clearDraftAndInputNow() {
draftGen++;
clearTimeout(draftSaveTimer);
const botId = getDraftBotId();
if (botId) saveDraft(botId, '');
const doClear = () => {
const textarea = document.getElementById('chat-input-textarea');
if (textarea && textarea.value) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(textarea, '');
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
};
doClear();
setTimeout(doClear, 50);
setTimeout(doClear, 300);
setTimeout(doClear, 800);
setTimeout(doClear, 1500);
}
function setupDraftClearObserver() {
const container = document.querySelector('#chat-messages');
if (!container) return;
if (draftClearObserver) draftClearObserver.disconnect();
clearTimeout(draftClearStableTimer);
draftClearArmed = false;
draftClearBaseline = null;
const arm = () => {
draftClearBaseline = container.children.length;
draftClearArmed = true;
};
draftClearObserver = new MutationObserver(() => {
if (!draftClearArmed) {
clearTimeout(draftClearStableTimer);
draftClearStableTimer = setTimeout(arm, 900);
return;
}
if (!isDraftEnabled()) return;
const count = container.children.length;
if (count > draftClearBaseline) clearDraftAndInputNow();
draftClearBaseline = count;
});
draftClearObserver.observe(container, { childList: true });
draftClearStableTimer = setTimeout(arm, 900);
}
let sendTransitionObserver = null;
let sendTransitionContainer = null;
function checkSendTransitionInstant() {
const sendBtn = document.querySelector('button[data-radix-popper-side]') || document.querySelector('#chat-input-box button');
const isGenerating = !!(sendBtn && sendBtn.querySelector('svg rect'));
if (isGenerating !== caiWasGenerating) console.log('[CAI draft debug]', !!sendBtn, 'isGenerating=', isGenerating, 'guarded=', Date.now() < draftGuardUntil);
if (isGenerating && !caiWasGenerating && Date.now() >= draftGuardUntil) clearDraftAndInputNow();
caiWasGenerating = isGenerating;
}
function setupSendButtonHandler() {
setInterval(() => {
const container = document.querySelector(selInputBox);
if (!container || container === sendTransitionContainer) return;
if (sendTransitionObserver) sendTransitionObserver.disconnect();
sendTransitionObserver = new MutationObserver(checkSendTransitionInstant);
sendTransitionObserver.observe(container, { childList: true, subtree: true, attributes: true });
sendTransitionContainer = container;
checkSendTransitionInstant();
}, 500);
}
function setupSendClickHandler() {
document.addEventListener('pointerdown', function(e) {
const atPoint = document.elementFromPoint(e.clientX, e.clientY);
const btn = atPoint ? atPoint.closest('button') : null;
if (!btn) return;
const label = (btn.getAttribute('aria-label') || '').toLowerCase();
if (!label.includes('send') || !label.includes('message')) return;
setTimeout(clearDraftAndInputNow, 0);
}, true);
}
// ─── BUTTON INJECTION ───
function setupGlobalInterceptor() {
document.addEventListener('click', function(e) {
const btn = e.target.closest('button');
if (!btn) return;
if (btn.closest('#cai-mbox')) return;
if (btn.textContent.includes('Customize') || btn.textContent.includes('Customize')) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
setTimeout(() => {
document.querySelectorAll('[role="dialog"]').forEach(d => {
if (d.id === 'chat-details') return;
if (!d.id || !d.id.startsWith('cai-')) d.remove();
});
buildModal();
}, 0);
}
}, true);
}
// ─── INIT ───
function init() {
injectBaseCSS();
const saved = loadStyle();
if (saved) applyStyle(saved);
//setupGlobalInterceptor(); // Disabled due to cause redirect to Home page when open modal from Customize on right panel. Floating button used only.
avatarObserver.observe(document.body, { childList: true, subtree: true });
triggerReplacementSafely();
setTimeout(trackMessageCountForActiveChat, 500);
setTimeout(setupGhostBlurTriggerObserver, 500);
updateCustomizeButtonVisibility();
setupEnterKeyHandler();
setupDraftHandler();
setupSendButtonHandler();
setupSendClickHandler();
setInterval(forcePatchGeneratingAvatar, 250);
window.addEventListener('resize', applyAuthorMobileReplacement);
window.addEventListener('resize', () => burstGhostBlurSync());
window.addEventListener('resize', () => triggerReplacementSafely());
let lastPath = location.pathname;
setInterval(() => {
if (location.pathname !== lastPath) {
lastPath = location.pathname;
stopGhostBlurLoop();
setTimeout(() => {
const s = loadStyle(); applyStyle(s);
updateCustomizeButtonVisibility();
trackMessageCountForActiveChat();
setupGhostBlurTriggerObserver();
}, 400);
}
}, 200);
}
function updateCustomizeButtonVisibility() {
const toggleBtn = document.getElementById('cai-customize-toggle');
if (isChatPage()) {
initCustomizeButton();
} else if (toggleBtn) {
toggleBtn.remove();
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();