Full: customization with advanced options (font, immersion, wallpaper, themes, video).
// ==UserScript==
// @name Character.AI Customization Full
// @namespace http://tampermonkey.net/
// @version 2026.07.24
// @description Full: customization with advanced options (font, immersion, wallpaper, themes, video).
// @match https://character.ai/*
// @match https://www.character.ai/*
// @author Arachne Project - discord.gg/yEwpaUTEhT
// @icon https://cdn.discordapp.com/attachments/1485377643233808505/1525689073371709590/nwq6htznkbss2z659sqwr2b00nbvph9bp756vn40nr6m2r4nwj.png?ex=6a544c2d&is=6a52faad&hm=fcfc1bc965c252452087006881ce278a18d0e47375373312b9c35cca1b9b960a&animated=true
// @grant window.onurlchange
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'cai_custom_data';
const themes = [
{ name: 'Default', userColor: 'rgba(38,39,43,0.85)', charColor: 'rgba(48,49,54,0.9)' },
{ name: 'Charcoal', userColor: 'rgb(30,30,30)', charColor: 'rgb(51,51,51)' },
{ name: 'Cobalt', userColor: 'rgb(30,30,30)', charColor: 'rgb(26,94,255)' },
{ name: 'Red', userColor: 'rgb(41,33,34)', charColor: 'rgb(159,45,60)' },
{ name: 'Peach', userColor: 'rgb(51,43,41)', charColor: 'rgb(249,106,93)' },
{ name: 'Clay', userColor: 'rgb(31,28,24)', charColor: 'rgb(79,45,0)' },
{ name: 'Honey', userColor: 'rgb(41,37,33)', charColor: 'rgb(153,89,0)' },
{ name: 'Yellow', userColor: 'rgb(56,47,0)', charColor: 'rgb(255,213,5)' },
{ name: 'Green', userColor: 'rgb(42,61,42)', charColor: 'rgb(10,104,8)' },
{ name: 'Pine', userColor: 'rgb(42,61,55)', charColor: 'rgb(0,82,57)' },
{ name: 'Aqua', userColor: 'rgb(42,61,61)', charColor: 'rgb(0,127,127)' },
{ name: 'Blue', userColor: 'rgb(42,50,61)', charColor: 'rgb(20,100,204)' },
{ name: 'Indigo', userColor: 'rgb(49,49,61)', charColor: 'rgb(77,77,191)' },
{ name: 'Purple', userColor: 'rgb(55,42,61)', charColor: 'rgb(137,41,172)' },
{ name: 'Silver', userColor: 'rgb(61,61,61)', charColor: 'rgb(227,226,230)' },
{ name: 'Grey', userColor: 'rgb(77,77,77)', charColor: 'rgb(42,44,46)' },
{ name: 'Black', userColor: 'rgb(61,61,61)', charColor: 'rgb(0,0,0)' }
];
// ============================================
// BUBBLE STYLE PACKS (imported from third-party bubbles set, adapted to native DOM)
// ============================================
const BUBBLES = [
// ── CAT 1: CLASSICS
{ id:1, name:'iOS Light', cat:'1. The Classics', font:'Poppins', 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:'Poppins', 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:'Poppins', 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: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:100%; 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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', 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:'Poppins', aiBg:'transparent', aiTx:'#333333', uBg:'transparent', uTx:'#333333', 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:'Poppins', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', 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:'Poppins', aiBg:'#ffffff', aiTx:'#333333', uBg:'#ffffff', uTx:'#333333', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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', 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:'Poppins', aiBg:'#111111', aiTx:'#ffffff', uBg:'#111111', uTx:'#ffffff', 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:'Poppins', aiBg:'#111111', aiTx:'#ffffff', uBg:'#111111', uTx:'#ffffff', 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:'Poppins', 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:'Poppins', 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:'Poppins', aiBg:'#c084fc', aiTx:'#ffffff', uBg:'#c084fc', uTx:'#ffffff', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', aiBg:'#000000', aiTx:'#ffffff', uBg:'#000000', uTx:'#ffffff', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Sparkle Stars', cat:'11. Emoji Backgrounds (iOS Style)', emojiCat:true, defaultEmoji:'✨', font:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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:'Poppins', 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); }' },
];
// Native DOM targets: char bubble = elevation-3, user bubble = elevation-2 (per FINDINGS)
const PACK_CHAR = '#chat-messages .bg-surface-elevation-3';
const PACK_USER = '#chat-messages .bg-surface-elevation-2';
const PACK_MSG = PACK_CHAR + ', ' + PACK_USER;
function packBuildEmojiCSS(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; }`;
}
let bubblePackStyleEl = null;
function applyBubbleStyle(id) {
if (bubblePackStyleEl) { bubblePackStyleEl.remove(); bubblePackStyleEl = null; }
if (!id) {
// Reassert the user's font — pack CSS forced its own family, so restore on clear.
try { applyFont({ fontName: getFontSetting() }); } catch (e) {}
updatePackPreview(null);
return;
}
const def = BUBBLES.find(b => b.id === id);
if (!def) return;
const font = def.font || 'Poppins';
// Load pack font under its own link so it doesn't evict the user's chat font.
try {
const packFontUrl = getGoogleFontUrl(font);
let pl = document.getElementById('cai-pack-font-link');
if (!pl) {
pl = document.createElement('link');
pl.id = 'cai-pack-font-link';
pl.rel = 'stylesheet';
pl.crossOrigin = 'anonymous';
document.head.appendChild(pl);
}
pl.href = packFontUrl;
} catch (e) {}
const aiBg = def.aiBg || '#ffffff', aiTx = def.aiTx || '#000000';
const uBg = def.uBg || '#ffffff', uTx = def.uTx || '#000000';
const sp = def.spDef || '#000000';
let css = `
${PACK_CHAR} { --cai-bg:${aiBg}; --cai-tx:${aiTx}; --cai-sp:${sp}; background:var(--cai-bg) !important; font-family:'${font}',sans-serif !important; }
${PACK_USER} { --cai-bg:${uBg}; --cai-tx:${uTx}; --cai-sp:${sp}; background:var(--cai-bg) !important; font-family:'${font}',sans-serif !important; }
${PACK_CHAR} .prose, ${PACK_CHAR} .prose *, ${PACK_CHAR} p, ${PACK_CHAR} span { color:var(--cai-tx) !important; font-family:'${font}',sans-serif !important; }
${PACK_USER} .prose, ${PACK_USER} .prose *, ${PACK_USER} p, ${PACK_USER} span { color:var(--cai-tx) !important; font-family:'${font}',sans-serif !important; }
`;
let shared = (def.css || '')
.replace(/\.cai-msg/g, ':is(' + PACK_MSG + ')')
.replace(/\.cai-ai/g, PACK_CHAR)
.replace(/\.cai-user/g, PACK_USER);
css += shared;
if (def.emojiCat) {
let eCSS = packBuildEmojiCSS({ customEmoji: [def.defaultEmoji], emojiAngle: 0, bgAlpha: 0.85, aiColor: aiBg, userColor: uBg }, def)
.replace(/\.cai-ai/g, PACK_CHAR).replace(/\.cai-user/g, PACK_USER);
css += eCSS;
}
css += `
@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)}}
`;
bubblePackStyleEl = document.createElement('style');
bubblePackStyleEl.id = 'cai-bubble-pack';
bubblePackStyleEl.textContent = css;
document.head.appendChild(bubblePackStyleEl);
updatePackPreview(id);
}
// Paint the modal's little preview bubbles with the selected pack's colors/font/shape.
function updatePackPreview(id) {
const charBubble = document.querySelector('#preview-char');
const userBubble = document.querySelector('#preview-user');
if (!charBubble || !userBubble) return;
[charBubble, userBubble].forEach(b => {
b.style.backgroundImage = '';
b.style.border = '';
b.style.fontFamily = '';
});
if (!id) {
// Revert to whatever the active color theme wants.
const s = getCurrentSettings();
updatePreview(s.theme || 'Default');
return;
}
const def = BUBBLES.find(b => b.id === id);
if (!def) return;
const aiBg = def.aiBg || '#ffffff', uBg = def.uBg || '#ffffff';
// Gradient packs: pull the first gradient from the css blob if present.
const grabBg = (side) => {
const m = new RegExp('\\.cai-' + side + '\\s*\\{[^}]*background:\\s*(linear-gradient\\([^;!]*\\))', 'i').exec(def.css || '');
return m ? m[1] : null;
};
const charGrad = grabBg('ai'), userGrad = grabBg('user');
// PACK_CHAR targets elevation-3 (user bubble), PACK_USER targets elevation-2 (bot bubble).
// Actual chat: bot gets uBg, user gets aiBg — preview must match this swap.
charBubble.style.background = userGrad || uBg;
userBubble.style.background = charGrad || aiBg;
}
// Curated subset shown in the picker. c1 = user-side swatch, c2 = char-side swatch
// (mirrors native chat-color circle: user color left, char color right).
const CURATED_PACKS = [
{ id:1, c1:'#0b93f6', c2:'#e5e5ea' }, // iOS Light
{ id:9, c1:'#5865F2', c2:'#5865F2' }, // Discord Blurple
{ id:11, c1:'#ff9a9e', c2:'#fecfef' }, // Sunset
{ id:12, c1:'#a1c4fd', c2:'#c2e9fb' }, // Ocean
{ id:15, c1:'#a18cd1', c2:'#fbc2eb' }, // Lavender
{ id:17, c1:'#30cfd0', c2:'#330867' }, // Twilight
{ id:22, c1:'#2b2b2b', c2:'#2b2b2b' }, // Neumorphic Dark
{ id:28, c1:'#87ceeb', c2:'#87ceeb' }, // Clay Blue
{ id:31, c1:'#bfdbfe', c2:'#fbcfe8' }, // Cotton Candy
{ id:32, c1:'#fdf2f8', c2:'#fdf2f8' }, // Strawberry Milk
{ id:37, c1:'#fbcfe8', c2:'#fbcfe8' }, // Bubblegum Gloss
{ id:65, c1:'#34d399', c2:'#34d399' }, // Leaf Cut
{ id:66, c1:'#f472b6', c2:'#f472b6' }, // Pill Shape
{ id:71, c1:'#e2e8f0', c2:'#e2e8f0' }, // Frosted Glass
{ id:74, c1:'#fcf6ba', c2:'#bf953f' }, // Gold Foil
{ id:83, c1:'#ff71ce', c2:'#01cdfe' }, // Vaporwave
{ id:84, c1:'#000000', c2:'#000000' }, // Terminal Green
{ id:89, c1:'#111111', c2:'#00ffff' }, // Neon Cyan
{ id:91, c1:'#0f172a', c2:'#0f172a' }, // Cosmic Space
{ id:92, c1:'#3b82f6', c2:'#8b5cf6' }, // Aurora Borealis
];
function buildBubblePackListHTML(activeId) {
let html = `<div class="flex flex-row flex-wrap w-full gap-4" style="justify-content:flex-start;">`;
// None swatch
html += `<div class="relative w-10 h-10 rounded-full cursor-pointer flex items-center justify-center cai-pack-swatch ${!activeId ? 'ring-1 ring-white ring-offset-1' : ''}" data-id="" title="None">
<div class="w-9 h-9 rounded-full overflow-hidden flex items-center justify-center" style="background: var(--surface-elevation-2); color: var(--muted-foreground); font-size: 0.8rem;">✕</div>
</div>`;
CURATED_PACKS.forEach(p => {
const def = BUBBLES.find(b => b.id === p.id);
if (!def) return;
const sel = activeId === p.id;
html += `<div class="relative w-10 h-10 rounded-full cursor-pointer flex items-center justify-center cai-pack-swatch ${sel ? 'ring-1 ring-white ring-offset-1' : ''}" data-id="${p.id}" title="${def.name}">
<div class="w-9 h-9 rounded-full overflow-hidden" style="background-image: linear-gradient(135deg, ${p.c1} 50%, ${p.c2} 50%);"></div>
</div>`;
});
html += `</div>`;
return html;
}
function getDefaultSettings() {
return {
theme: 'Default',
bgType: 'file',
wallpaper: null,
backgroundColor: '#00000000',
wallpaperOpacity: 100,
opacity: 100,
hideInputButtons: true,
hideWarning: true,
hideCharacterInfo: true,
hideBadgeAndVoice: true,
fontName: '',
immersionMode: false,
videoSpeed: 1,
videoSound: false,
inputWidth: 150,
bubbleOutlines: false,
textSize: 100,
bubbleRadius: 16,
bubbleStyle: null,
customCss: '',
audioWallpaperUrl: null,
audioWallpaperVolume: 100,
audioWallpaperLoop: true,
particlesEnabled: false,
particlesEffect: 'rain',
particlesDensity: 50,
particlesSpeed: 50,
particlesOpacity: 100
};
}
function getChatIdFromUrl() {
const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
return match ? match[1] : null;
}
function getStorageData() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const data = JSON.parse(raw);
if (!data.global) data.global = getDefaultSettings();
if (!data.chats) data.chats = {};
const defaults = getDefaultSettings();
for (const key in defaults) {
if (!(key in data.global)) data.global[key] = defaults[key];
}
for (const chatId in data.chats) {
for (const key in defaults) {
if (!(key in data.chats[chatId])) data.chats[chatId][key] = defaults[key];
}
}
function migrateFont(s) {
if (s.fontFamily && s.fontFamily !== 'default' && !s.fontName) {
s.fontName = s.fontFamily === 'custom' && s.customFontUrl ? s.customFontUrl : s.fontFamily;
}
if (!s.fontName) s.fontName = '';
delete s.fontFamily;
delete s.customFontUrl;
}
migrateFont(data.global);
for (const chatId in data.chats) migrateFont(data.chats[chatId]);
return data;
}
} catch (e) {}
return { global: getDefaultSettings(), chats: {} };
}
function saveStorageData(data) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
return true;
} catch (e) {
if (e.name === 'QuotaExceededError' || e.code === 22) {
showAlertDialog('File is too large! The data exceeds browser localStorage limits. Use a smaller file or a URL link instead (under 2MB recommended).');
} else {
throw e;
}
return false;
}
}
function getCurrentSettings() {
const data = getStorageData();
const chatId = getChatIdFromUrl();
if (chatId && data.chats[chatId]) {
return data.chats[chatId];
}
return data.global;
}
function getFontSetting(chatData) {
if (chatData) {
return (chatData.perChatFont && chatData.fontName) || '';
}
const data = getStorageData();
const chatId = getChatIdFromUrl();
const chat = chatId && data.chats[chatId];
if (chat && chat.perChatFont) {
return chat.fontName || '';
}
return data.global.fontName || '';
}
function saveCurrentSettings(settings) {
const data = getStorageData();
const chatId = getChatIdFromUrl();
if (chatId) {
data.chats[chatId] = settings;
} else {
data.global = settings;
}
return saveStorageData(data);
}
function closeDialog(modal, overlay) {
modal.style.opacity = '0';
modal.style.transform = 'translate(-50%, -50%) scale(0.95)';
overlay.style.opacity = '0';
setTimeout(() => { modal.remove(); overlay.remove(); }, 200);
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function showAlertDialog(message) {
document.querySelector('.cai-alert-dialog')?.remove();
document.querySelector('.cai-alert-overlay')?.remove();
const overlay = document.createElement('div');
overlay.className = 'cai-alert-overlay fixed inset-0 bg-black/50 z-[60]';
overlay.style.transition = 'opacity 0.2s ease';
document.body.appendChild(overlay);
const dialog = document.createElement('div');
dialog.className = 'cai-alert-dialog fixed z-[60] p-6 shadow-lg border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] sm:rounded-spacing-l max-w-sm w-11/12 flex flex-col gap-4 bg-surface-elevation-1 text-foreground';
dialog.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
dialog.innerHTML = `<p class="text-sm text-muted-foreground">${escapeHtml(message)}</p><div class="flex justify-end gap-2 mt-1"><button class="ok-btn px-4 py-2 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">OK</button></div>`;
document.body.appendChild(dialog);
const close = () => closeDialog(dialog, overlay);
dialog.querySelector('.ok-btn').addEventListener('click', close);
overlay.addEventListener('click', close);
}
function showConfirmDialog(message) {
return new Promise(resolve => {
document.querySelector('.cai-confirm-dialog')?.remove();
document.querySelector('.cai-confirm-overlay')?.remove();
const overlay = document.createElement('div');
overlay.className = 'cai-confirm-overlay fixed inset-0 bg-black/50 z-[60]';
overlay.style.transition = 'opacity 0.2s ease';
document.body.appendChild(overlay);
const dialog = document.createElement('div');
dialog.className = 'cai-confirm-dialog fixed z-[60] p-6 shadow-lg border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] sm:rounded-spacing-l max-w-sm w-11/12 flex flex-col gap-4 bg-surface-elevation-1 text-foreground';
dialog.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
dialog.innerHTML = `<p class="text-sm text-muted-foreground">${escapeHtml(message)}</p><div class="flex justify-end gap-2 mt-1"><button class="cancel-btn px-4 py-2 text-sm rounded-md border border-input text-muted-foreground hover:text-foreground transition-colors">Cancel</button><button class="confirm-btn px-4 py-2 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Continue</button></div>`;
document.body.appendChild(dialog);
function close(val) {
closeDialog(dialog, overlay);
resolve(val);
}
dialog.querySelector('.cancel-btn').addEventListener('click', () => close(false));
dialog.querySelector('.confirm-btn').addEventListener('click', () => close(true));
overlay.addEventListener('click', () => close(false));
});
}
function getWallpaperSettings() {
const data = getStorageData();
const chatId = getChatIdFromUrl();
if (chatId) {
if (!data.chats[chatId]) {
data.chats[chatId] = Object.assign({}, getDefaultSettings());
saveStorageData(data);
}
return data.chats[chatId];
}
return data.global;
}
function saveGlobalSetting(key, value) {
const data = getStorageData();
data.global[key] = value;
saveStorageData(data);
}
function parseRGB(rgbStr) {
const match = rgbStr.match(/\d+/g);
if (!match) return null;
const [r, g, b] = match.map(Number);
return { r, g, b };
}
function getLuminance(r, g, b) {
const srgb = [r, g, b].map(c => c / 255);
const gamma = srgb.map(c => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
return 0.2126 * gamma[0] + 0.7152 * gamma[1] + 0.0722 * gamma[2];
}
function getContrastColor(rgbStr) {
const parsed = parseRGB(rgbStr);
if (!parsed) return 'var(--foreground)';
const lum = getLuminance(parsed.r, parsed.g, parsed.b);
return lum > 0.5 ? '#000000' : '#ffffff';
}
function isVideoUrl(url) {
if (!url) return false;
if (url.startsWith('data:video/')) return true;
return /\.(mp4|webm|mov|ogg)(\?|#|$)/i.test(url);
}
function isAudioUrl(url) {
if (!url) return false;
if (url.startsWith('data:audio/')) return true;
return /\.(mp3|wav|ogg|m4a|flac|aac|wma|webm)(\?|#|$)/i.test(url);
}
let injectedFontLinkId = 'cai-google-font-link';
function loadGoogleFont(url) {
const existing = document.getElementById(injectedFontLinkId);
if (existing) existing.remove();
if (!url || !url.includes('fonts.googleapis.com')) return;
const link = document.createElement('link');
link.id = injectedFontLinkId;
link.rel = 'stylesheet';
link.href = url;
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
}
function getGoogleFontUrl(fontName) {
return `https://fonts.googleapis.com/css2?family=${fontName.replace(/ /g, '+')}:wght@400;500;600;700&display=swap`;
}
function applyFont(settings) {
const containers = [
document.querySelector('#chat-body'),
document.querySelector('#chat-messages')
].filter(Boolean);
if (!containers.length) return;
const fontName = (settings.fontName || '').trim();
let fontValue = '';
if (fontName) {
if (fontName.includes('fonts.googleapis.com') || fontName.startsWith('http')) {
if (fontName.includes('fonts.googleapis.com')) loadGoogleFont(fontName);
const m = fontName.match(/family=([^:&]+)/);
fontValue = m ? `'${m[1].replace(/\+/g, ' ')}', sans-serif` : fontName;
} else {
loadGoogleFont(getGoogleFontUrl(fontName));
fontValue = `'${fontName}', sans-serif`;
}
}
document.documentElement.style.setProperty('--cai-chat-font', fontValue || 'inherit');
containers.forEach(el => {
el.style.setProperty('--cai-chat-font', fontValue || 'inherit');
});
const chatMessages = document.querySelector('#chat-messages');
if (chatMessages) {
const bubbles = chatMessages.querySelectorAll('.prose, .prose *');
bubbles.forEach(el => {
el.style.fontFamily = fontValue || '';
});
}
}
let immersionExitButton = null;
function applyImmersion(settings) {
const enabled = settings.immersionMode || false;
const leftSidebar = document.querySelector('aside.fixed.top-0.left-0.z-50.h-full');
const spacer = document.querySelector('div.transition-width.w-64');
const rightSidebar = document.querySelector('#chat-details');
const mainContent = document.querySelector('#main-content');
const mainContainer = document.querySelector('main.flex-1');
if (enabled) {
if (leftSidebar) leftSidebar.style.display = 'none';
if (spacer) spacer.style.display = 'none';
if (rightSidebar) {
rightSidebar.style.display = 'none';
rightSidebar.classList.add('cai-immersion-hidden');
}
if (mainContent) {
mainContent.style.maxWidth = '100%';
mainContent.style.width = '100%';
mainContent.style.flex = '1';
}
if (mainContainer) {
mainContainer.style.maxWidth = '100%';
mainContainer.style.width = '100%';
}
const parent = mainContent?.parentElement;
if (parent) {
parent.style.maxWidth = '100%';
parent.style.width = '100%';
}
showImmersionExitButton();
} else {
if (leftSidebar) leftSidebar.style.display = '';
if (spacer) spacer.style.display = '';
if (rightSidebar) {
rightSidebar.style.display = '';
rightSidebar.classList.remove('cai-immersion-hidden');
}
if (mainContent) {
mainContent.style.maxWidth = '';
mainContent.style.width = '';
mainContent.style.flex = '';
}
if (mainContainer) {
mainContainer.style.maxWidth = '';
mainContainer.style.width = '';
}
const parent = mainContent?.parentElement;
if (parent) {
parent.style.maxWidth = '';
parent.style.width = '';
}
hideImmersionExitButton();
}
}
function showImmersionExitButton() {
if (immersionExitButton) {
immersionExitButton.style.display = 'flex';
return;
}
const btn = document.createElement('div');
btn.id = 'cai-immersion-exit-btn';
btn.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
z-index: 99999;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #fff;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
transition: transform 0.2s, opacity 0.2s;
font-family: system-ui, -apple-system, sans-serif;
user-select: none;
`;
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 24px; height: 24px;">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
`;
btn.title = 'Exit immersion mode (Esc)';
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.1)';
btn.style.opacity = '1';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1)';
btn.style.opacity = '0.85';
});
btn.style.opacity = '0.85';
btn.addEventListener('click', () => {
saveGlobalSetting('immersionMode', false);
applyImmersion(getStorageData().global);
updateImmersionToggleInModal(false);
});
document.body.appendChild(btn);
immersionExitButton = btn;
}
function hideImmersionExitButton() {
if (immersionExitButton) {
immersionExitButton.style.display = 'none';
}
}
function updateImmersionToggleInModal(state) {
const toggle = document.querySelector('#toggle-immersion');
if (toggle) {
const isChecked = state;
toggle.setAttribute('aria-checked', isChecked);
toggle.classList.toggle('bg-blue', isChecked);
toggle.classList.toggle('bg-muted', !isChecked);
const thumb = toggle.querySelector('span');
if (thumb) {
thumb.classList.toggle('translate-x-5', isChecked);
thumb.classList.toggle('translate-x-0', !isChecked);
}
}
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const data = getStorageData();
if (data.global.immersionMode) {
data.global.immersionMode = false;
saveStorageData(data);
applyImmersion(data.global);
updateImmersionToggleInModal(false);
}
}
});
function injectAdaptiveStyles() {
const style = document.createElement('style');
style.id = 'cai-adaptive-text-styles';
style.textContent = `
#chat-messages .bg-surface-elevation-2 .prose,
#chat-messages .bg-surface-elevation-2 .prose *,
#chat-messages .bg-surface-elevation-3 .prose,
#chat-messages .bg-surface-elevation-3 .prose * {
color: var(--bubble-text-char, var(--foreground)) !important;
font-family: var(--cai-chat-font, inherit) !important;
}
#chat-messages .bg-surface-elevation-3 .prose,
#chat-messages .bg-surface-elevation-3 .prose * {
color: var(--bubble-text-user, var(--foreground)) !important;
font-family: var(--cai-chat-font, inherit) !important;
}
#chat-messages .bg-surface-elevation-2,
#chat-messages .bg-surface-elevation-3 {
opacity: var(--bubble-opacity, 1) !important;
background-color: var(--bubble-bg-user, var(--surface-elevation-2)) !important;
}
#chat-messages .bg-surface-elevation-3 {
background-color: var(--bubble-bg-char, var(--surface-elevation-3)) !important;
}
#chat-body {
position: relative;
background-color: var(--cai-bg-color, transparent);
}
#chat-body::before {
content: '';
position: absolute;
inset: 0;
background-image: var(--cai-wallpaper-image, none);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: var(--cai-wallpaper-opacity, 1);
pointer-events: none;
z-index: 0;
}
#chat-body > * {
position: relative;
z-index: 1;
}
.cai-preview-bg::before {
content: '';
position: absolute;
inset: 0;
background-image: var(--cai-preview-wallpaper, none);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: var(--cai-preview-opacity, 1);
pointer-events: none;
z-index: 0;
border-radius: inherit;
}
.cai-preview-bg > * {
position: relative;
z-index: 1;
}
#cai-bg-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
z-index: 0;
opacity: var(--cai-wallpaper-opacity, 1);
}
.cai-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: var(--surface-elevation-2);
outline: none;
transition: background 0.2s ease;
cursor: pointer;
}
.cai-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--background);
box-shadow: 0 1px 3px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.08);
border: 1px solid var(--border-outline);
transition: transform 0.15s ease, box-shadow 0.15s ease;
cursor: pointer;
}
.cai-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.cai-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--background);
box-shadow: 0 1px 3px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.08);
border: 1px solid var(--border-outline);
transition: transform 0.15s ease, box-shadow 0.15s ease;
cursor: pointer;
}
.cai-slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.cai-slider:focus-visible::-webkit-slider-thumb {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.cai-slider:focus-visible::-moz-range-thumb {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.cai-custom-modal {
transition: opacity 0.2s ease, transform 0.2s ease;
will-change: opacity, transform;
max-height: 90vh;
overflow-y: auto;
}
.cai-custom-modal.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
.cai-custom-overlay {
transition: opacity 0.2s ease;
will-change: opacity;
}
.cai-custom-overlay.closing {
opacity: 0;
}
.cai-wallpaper-dialog.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.cai-toggle-track {
display: inline-flex; flex-shrink: 0; cursor: pointer;
align-items: center; width: 36px; height: 20px;
border-radius: 9999px; border: 2px solid transparent;
transition: background 0.15s ease;
}
.cai-toggle-thumb {
pointer-events: none; display: block;
width: 16px; height: 16px; border-radius: 9999px;
background: var(--background);
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
transition: transform 0.15s ease;
}
.cai-bg-type-group {
display: flex;
gap: 2px;
background: var(--surface-elevation-2);
border-radius: 8px;
padding: 2px;
margin-top: 8px;
}
.cai-bg-type-group button {
flex: 1;
padding: 4px 12px;
border-radius: 6px;
border: none;
background: transparent;
color: var(--muted-foreground);
font-size: 0.75rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
font-weight: 500;
}
.cai-bg-type-group button.active {
background: var(--background);
color: var(--foreground);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.cai-bg-type-group button:hover:not(.active) {
background: var(--surface-elevation-1);
}
.cai-bg-input-area {
margin-top: 8px;
width: 100%;
}
.cai-bg-input-area input[type="text"] {
width: 100%;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid var(--border-outline);
background: var(--background);
color: var(--foreground);
font-size: 0.875rem;
}
.cai-bg-upload-btn {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px dashed var(--border-outline);
background: var(--surface-elevation-1);
color: var(--muted-foreground);
font-size: 0.875rem;
cursor: pointer;
transition: background 0.15s;
text-align: center;
}
.cai-bg-upload-btn:hover {
background: var(--surface-elevation-2);
}
.cai-bg-upload-btn.has-image {
border-color: var(--primary);
color: var(--primary);
}
.cai-bg-remove-btn {
margin-top: 4px;
padding: 4px 12px;
border-radius: 6px;
border: 1px solid var(--error);
background: transparent;
color: var(--error);
font-size: 0.75rem;
cursor: pointer;
width: 100%;
}
.cai-bg-remove-btn:hover {
background: var(--error-container);
}
#chat-messages .absolute.-bottom-0 {
background: rgba(0, 0, 0, 0.5) !important;
backdrop-filter: blur(12px) !important;
-webkit-backdrop-filter: blur(12px) !important;
border-radius: 9999px !important;
padding: 2px 4px !important;
gap: 2px !important;
box-shadow: 0 2px 12px rgba(0,0,0,0.3) !important;
margin-bottom: 2px !important;
transform: translateY(4px) !important;
}
#chat-messages .absolute.-bottom-0 button {
color: #fff !important;
min-width: 24px !important;
width: 24px !important;
height: 24px !important;
min-height: 24px !important;
}
#chat-messages .absolute.-bottom-0 button svg {
stroke: #fff !important;
fill: none !important;
width: 12px !important;
height: 12px !important;
}
#chat-messages .absolute.-bottom-0 button:hover {
background: rgba(255,255,255,0.15) !important;
}
#chat-messages .absolute.-bottom-0 .flex.h-6.items-center {
height: 24px !important;
}
#chat-messages .absolute.-bottom-0 .text-sm {
font-size: 10px !important;
}
.cai-custom-color-picker {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
align-items: center;
}
.cai-custom-color-picker .color-swatch {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid var(--border-outline);
cursor: pointer;
transition: transform 0.15s, border-color 0.15s;
flex-shrink: 0;
}
.cai-custom-color-picker .color-swatch:hover {
transform: scale(1.1);
}
.cai-custom-color-picker .color-swatch.active {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--primary);
}
.cai-custom-color-picker .color-hex-input {
width: 70px;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--border-outline);
background: var(--background);
color: var(--foreground);
font-size: 12px;
font-family: monospace;
text-transform: uppercase;
outline: none;
}
.cai-custom-color-picker .color-hex-input:focus {
border-color: var(--primary);
}
[role="tab"][data-state="active"].bg-accent {
background: #18181b !important;
}
.bg-primary-foreground {
background: #18181b !important;
}
.cai-immersion-hidden {
display: none !important;
}
#chat-messages span.rounded-full {
filter: drop-shadow(0 0 2px rgba(255,255,255,0.5));
}
#chat-messages .flex.flex-row.items-center.gap-2.font-light > div.text-sm {
-webkit-text-stroke: 1.5px rgba(0,0,0,0.85);
paint-order: stroke fill;
font-weight: 600;
}
.cai-font-selector {
width: 100%;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid var(--border-outline);
background: var(--background);
color: var(--foreground);
font-size: 0.875rem;
outline: none;
cursor: pointer;
}
.cai-font-selector:focus {
border-color: var(--primary);
}
.cai-font-input {
width: 100%;
padding: 10px 12px;
border-radius: var(--spacing-s, 8px);
border: 1px solid var(--border-input, var(--border-outline));
background: var(--surface-elevation-3, var(--surface-elevation-1));
color: var(--foreground);
font-size: 0.875rem;
outline: none;
margin-top: 4px;
transition: border-color 0.15s ease;
}
.cai-font-input:focus {
border-color: var(--primary);
}
.cai-font-input::placeholder {
color: var(--muted-foreground);
}
.cai-custom-css-input {
width: 100%;
padding: 10px 12px;
border-radius: var(--spacing-s, 8px);
border: 1px solid var(--border-input, var(--border-outline));
background: var(--surface-elevation-3, var(--surface-elevation-1));
color: var(--foreground);
font-size: 0.875rem;
font-family: monospace;
outline: none;
resize: none;
transition: border-color 0.15s ease;
}
.cai-custom-css-input:focus {
border-color: var(--primary);
}
.cai-custom-css-input::placeholder {
color: var(--muted-foreground);
}
.cai-font-hint {
font-size: 0.65rem;
color: var(--muted-foreground);
margin-top: 2px;
}
.cai-pack-swatch {
transition: transform 0.12s ease;
}
.cai-pack-swatch:hover {
transform: scale(1.08);
}
body[data-cai-hide-input="true"] button:has(svg path[d*="M6.754 3"]),
body[data-cai-hide-input="true"] button:has(svg path[d*="M5.22287 3"]) {
display: none !important;
}
body[data-cai-hide-warning="true"] button[aria-label="Common.expand"] {
display: none !important;
}
body[data-cai-hide-charinfo="true"] .group.relative.max-w-3xl.m-auto.w-full .flex.flex-col.items-center.justify-center.text-center.gap-1.px-16.pb-6.pt-12 {
display: none !important;
}
body[data-cai-hide-badge="true"] div.flex.items-center:has(> div.rounded-2xl.text-sm.bg-secondary.px-2.font-light.h-fit) {
display: none !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) {
background: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(16px) !important;
-webkit-backdrop-filter: blur(16px) !important;
border-radius: 16px !important;
border-color: rgba(255, 255, 255, 0.08) !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 24px rgba(0,0,0,0.4) !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) textarea {
background: transparent !important;
color: #fff !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) textarea::placeholder {
color: rgba(255,255,255,0.35) !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) button[aria-label="Send a message..."] {
background: #3a3a40 !important;
border-radius: 10px !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) button[aria-label="Send a message..."]:hover {
background: #4a4a52 !important;
}
div.bg-surface-elevation-1:has(textarea#chat-input-textarea) button[aria-label="Send a message..."] svg {
color: #fff !important;
}
#chat-messages {
padding-bottom: 40px !important;
}
div.flex.md\\:ml-14.absolute.\\-bottom-0.items-center {
bottom: 4px !important;
}
/* ---- BUBBLE OUTLINES ---- */
#chat-messages .bg-surface-elevation-2 {
box-shadow: var(--outline-user, none) !important;
}
#chat-messages .bg-surface-elevation-3 {
box-shadow: var(--outline-bot, none) !important;
}
/* ---- TEXT SIZE ---- */
#chat-messages .prose {
font-size: var(--text-scale, 1rem) !important;
}
/* ---- BUBBLE ROUNDEDNESS ---- */
#chat-messages .bg-surface-elevation-2,
#chat-messages .bg-surface-elevation-3 {
border-radius: var(--bubble-radius, 16px) !important;
}
/* ---- CUSTOM CSS placeholder (injected dynamically) ---- */
/* ---- AUDIO WALLPAPER ---- */
.cai-audio-dialog.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.cai-audio-upload-row {
display: flex;
flex-direction: row;
gap: 10px;
align-items: stretch;
}
.cai-audio-visualizer {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
width: 52px;
padding: 10px 0;
border-radius: 8px;
flex-shrink: 0;
background: linear-gradient(135deg, rgba(83,109,198,0.08), rgba(120,80,200,0.05));
}
.cai-audio-visualizer.active .cai-wave-bar {
animation: caiWave 1.2s ease-in-out infinite;
}
.cai-wave-bar {
width: 5px;
height: 12px;
border-radius: 2px;
background: linear-gradient(to top, var(--blue,#536dc6), #7860c8);
animation: none;
transform-origin: bottom;
}
@keyframes caiWave {
0%, 100% { height: 8px; }
25% { height: 28px; }
50% { height: 16px; }
75% { height: 36px; }
}
.cai-audio-upload-area {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
min-width: 0;
}
.cai-audio-upload-btn {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: 1px dashed var(--border-outline);
background: linear-gradient(135deg, rgba(83,109,198,0.06), rgba(120,80,200,0.04));
color: var(--muted-foreground);
font-size: 0.8rem;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.cai-audio-upload-btn:hover {
background: linear-gradient(135deg, rgba(83,109,198,0.12), rgba(120,80,200,0.08));
border-color: var(--blue,#536dc6);
color: var(--foreground);
}
.cai-audio-link-area {
display: flex;
flex-direction: column;
gap: 4px;
}
.cai-audio-link-label {
font-size: 0.7rem;
color: var(--muted-foreground);
}
.cai-audio-link-input {
width: 100%;
padding: 8px 10px;
border-radius: 6px;
border: 1px solid var(--border-outline);
background: var(--background);
color: var(--foreground);
font-size: 0.8rem;
outline: none;
transition: border-color 0.15s;
}
.cai-audio-link-input:focus {
border-color: var(--blue,#536dc6);
}
.cai-audio-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.cai-audio-controls.muted {
opacity: 0.4;
pointer-events: none;
}
.cai-audio-player-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
background: var(--surface-elevation-2);
}
.cai-audio-play-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: var(--blue,#536dc6);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.15s;
}
.cai-audio-play-btn:hover {
background: #6577d6;
}
.cai-audio-play-btn svg {
display: block;
}
.cai-audio-seek-track {
flex: 1;
position: relative;
height: 4px;
border-radius: 2px;
background: var(--surface-elevation-3);
min-width: 0;
}
.cai-audio-seek-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
border-radius: 2px;
background: linear-gradient(to right, var(--blue,#536dc6), #7860c8);
pointer-events: none;
}
.cai-audio-seek-slider {
position: absolute;
inset: -6px 0;
width: 100%;
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
z-index: 1;
margin: 0;
opacity: 0;
}
.cai-audio-seek-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--background);
border: 2px solid var(--blue,#536dc6);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
cursor: pointer;
}
.cai-audio-time {
font-size: 0.7rem;
color: var(--muted-foreground);
font-variant-numeric: tabular-nums;
white-space: nowrap;
flex-shrink: 0;
min-width: 70px;
text-align: right;
}
.cai-audio-vol-row {
display: flex;
align-items: center;
gap: 8px;
}
.cai-audio-vol-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.8rem;
color: var(--muted-foreground);
min-width: 70px;
flex-shrink: 0;
}
.cai-audio-vol-track {
flex: 1;
position: relative;
height: 4px;
border-radius: 2px;
background: var(--surface-elevation-2);
}
.cai-audio-vol-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
border-radius: 2px;
background: linear-gradient(to right, var(--blue,#536dc6), #7860c8);
pointer-events: none;
}
.cai-audio-vol-slider {
position: absolute;
inset: -6px 0;
width: 100%;
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
z-index: 1;
margin: 0;
opacity: 0;
}
.cai-audio-vol-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--background);
border: 2px solid var(--blue,#536dc6);
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
cursor: pointer;
}
.cai-audio-vol-pct {
font-size: 0.75rem;
color: var(--muted-foreground);
min-width: 32px;
text-align: right;
flex-shrink: 0;
}
.cai-audio-loop-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.cai-audio-loop-label {
font-size: 0.8rem;
color: var(--muted-foreground);
}
.cai-audio-loop-toggle {
width: 36px;
height: 20px;
border-radius: 9999px;
background: var(--surface-elevation-2);
cursor: pointer;
position: relative;
transition: background 0.15s;
flex-shrink: 0;
}
.cai-audio-loop-toggle.on {
background: linear-gradient(to right, var(--blue,#536dc6), #7860c8);
}
.cai-audio-loop-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--background);
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
transition: transform 0.15s;
}
.cai-audio-loop-toggle.on .cai-audio-loop-thumb {
transform: translateX(16px);
}
.cai-audio-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 4px;
border-top: 1px solid var(--border-outline);
}
.cai-audio-play-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: var(--muted-foreground);
}
.cai-audio-playing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
animation: caiPulse 1.2s ease-in-out infinite;
}
@keyframes caiPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.cai-audio-paused-label {
color: var(--muted-foreground);
}
.cai-audio-idle-label {
color: var(--muted-foreground);
opacity: 0.6;
}
.cai-audio-stop-btn {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid var(--error,#ef4444);
background: transparent;
color: var(--error,#ef4444);
font-size: 0.75rem;
cursor: pointer;
transition: background 0.15s;
}
.cai-audio-stop-btn:hover {
background: rgba(239,68,68,0.1);
}
#cai-particles-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.cai-particle-select {
width: 100%;
padding: 8px 10px;
border-radius: 6px;
border: 1px solid var(--border-outline);
background: var(--background);
color: var(--foreground);
font-size: 0.85rem;
outline: none;
cursor: pointer;
transition: border-color 0.15s;
}
.cai-particle-select:focus {
border-color: var(--blue,#536dc6);
}
`;
document.head.appendChild(style);
}
function applyTheme(themeName) {
const containers = [
document.querySelector('#chat-body'),
document.querySelector('#chat-messages')
].filter(Boolean);
if (!containers.length) return;
const theme = themes.find(t => t.name === themeName);
if (!theme) return;
if (themeName === 'Default') {
containers.forEach(el => {
el.style.removeProperty('--surface-elevation-2');
el.style.removeProperty('--surface-elevation-3');
el.style.removeProperty('--bubble-bg-char');
el.style.removeProperty('--bubble-bg-user');
el.style.removeProperty('--bubble-text-char');
el.style.removeProperty('--bubble-text-user');
});
} else {
const textChar = getContrastColor(theme.charColor);
const textUser = getContrastColor(theme.userColor);
containers.forEach(el => {
el.style.setProperty('--bubble-bg-char', theme.charColor);
el.style.setProperty('--bubble-bg-user', theme.userColor);
el.style.setProperty('--bubble-text-char', textChar);
el.style.setProperty('--bubble-text-user', textUser);
});
}
updatePreview(themeName);
}
function updatePreview(themeName) {
const charBubble = document.querySelector('#preview-char');
const userBubble = document.querySelector('#preview-user');
if (!charBubble || !userBubble) return;
const theme = themes.find(t => t.name === themeName);
if (!theme) return;
if (themeName === 'Default') {
charBubble.style.background = 'var(--surface-elevation-2)';
userBubble.style.background = 'var(--surface-elevation-3)';
} else {
charBubble.style.background = theme.userColor;
userBubble.style.background = theme.charColor;
}
const s = getCurrentSettings();
if (s.bubbleOutlines) {
updatePreviewOutlines(true, s);
}
}
function updatePreviewOutlines(enabled, settings) {
const charBubble = document.querySelector('#preview-char');
const userBubble = document.querySelector('#preview-user');
if (!charBubble || !userBubble) return;
if (!enabled) {
charBubble.style.boxShadow = '';
userBubble.style.boxShadow = '';
return;
}
const theme = themes.find(t => t.name === (settings.theme || 'Default'));
let charColor, userColor;
if (theme && theme.name !== 'Default') {
charColor = theme.charColor;
userColor = theme.userColor;
} else {
charColor = 'var(--surface-elevation-3)';
userColor = 'var(--surface-elevation-2)';
}
charBubble.style.boxShadow = `0 0 0 2px ${charColor}`;
userBubble.style.boxShadow = `0 0 0 2px ${userColor}`;
}
function updatePreviewBackground(settings) {
const previewContainer = document.querySelector('.cai-custom-modal .w-full.h-full.rounded-\\[13\\.5px\\].relative.border.border-foreground-300.bg-surface.overflow-hidden');
if (!previewContainer) return;
const existingPreviewVideo = previewContainer.querySelector('.cai-preview-video');
if (existingPreviewVideo) existingPreviewVideo.remove();
const bgType = settings.bgType || 'file';
const wallpaper = settings.wallpaper;
const backgroundColor = settings.backgroundColor || 'transparent';
const opacity = (settings.wallpaperOpacity !== undefined ? settings.wallpaperOpacity : 100) / 100;
previewContainer.style.setProperty('--cai-preview-wallpaper', 'none');
previewContainer.style.setProperty('--cai-preview-opacity', '1');
previewContainer.style.backgroundImage = '';
previewContainer.style.backgroundSize = '';
previewContainer.style.backgroundPosition = '';
previewContainer.style.backgroundRepeat = '';
previewContainer.style.backgroundColor = '';
if (bgType === 'color') {
previewContainer.style.backgroundColor = backgroundColor;
previewContainer.style.setProperty('--cai-preview-wallpaper', 'none');
} else if (bgType === 'file' || bgType === 'link') {
if (wallpaper && isVideoUrl(wallpaper)) {
const video = document.createElement('video');
video.className = 'cai-preview-video';
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.src = wallpaper;
video.playbackRate = settings.videoSpeed || 1;
video.style.cssText = `position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:-1;opacity:${opacity};`;
previewContainer.style.backgroundColor = 'transparent';
previewContainer.style.setProperty('--cai-preview-wallpaper', 'none');
previewContainer.appendChild(video);
} else if (wallpaper) {
previewContainer.style.setProperty('--cai-preview-wallpaper', `url("${wallpaper}")`);
previewContainer.style.setProperty('--cai-preview-opacity', String(opacity));
previewContainer.style.backgroundColor = 'transparent';
}
}
}
function applyBackground(settings) {
const chatBody = document.querySelector('#chat-body');
if (!chatBody) return;
const bgType = settings.bgType || 'file';
const wallpaper = settings.wallpaper;
const backgroundColor = settings.backgroundColor || 'transparent';
const opacityVal = (settings.wallpaperOpacity !== undefined ? settings.wallpaperOpacity : 100) / 100;
chatBody.style.removeProperty('--cai-wallpaper-image');
chatBody.style.removeProperty('--cai-wallpaper-opacity');
chatBody.style.removeProperty('--cai-bg-color');
const existingVideo = chatBody.querySelector('#cai-bg-video');
if (existingVideo) existingVideo.remove();
if (bgType === 'color') {
chatBody.style.setProperty('--cai-bg-color', backgroundColor);
chatBody.style.setProperty('--cai-wallpaper-image', 'none');
} else if (bgType === 'file' || bgType === 'link') {
if (wallpaper && isVideoUrl(wallpaper)) {
chatBody.style.setProperty('--cai-wallpaper-image', 'none');
chatBody.style.setProperty('--cai-bg-color', 'transparent');
chatBody.style.setProperty('--cai-wallpaper-opacity', opacityVal);
const speed = settings.videoSpeed || 1;
const sound = settings.videoSound || false;
const video = document.createElement('video');
video.id = 'cai-bg-video';
video.autoplay = true;
video.loop = true;
video.muted = !sound;
video.playsInline = true;
video.src = wallpaper;
video.playbackRate = speed;
chatBody.prepend(video);
} else if (wallpaper) {
chatBody.style.setProperty('--cai-wallpaper-image', `url("${wallpaper}")`);
chatBody.style.setProperty('--cai-wallpaper-opacity', opacityVal);
chatBody.style.setProperty('--cai-bg-color', 'transparent');
} else {
chatBody.style.setProperty('--cai-wallpaper-image', 'none');
chatBody.style.setProperty('--cai-bg-color', 'transparent');
}
}
updatePreviewBackground(settings);
}
function applyAudioWallpaper(settings) {
const url = settings.audioWallpaperUrl;
const volume = (settings.audioWallpaperVolume !== undefined ? settings.audioWallpaperVolume : 100) / 100;
const loop = settings.audioWallpaperLoop !== undefined ? settings.audioWallpaperLoop : true;
const existing = document.getElementById('cai-bg-audio');
if (!url) {
if (existing) {
existing.pause();
existing.remove();
}
return;
}
if (existing) {
if (existing.src !== url) {
existing.src = url;
existing.load();
existing.play().catch(() => {});
}
existing.volume = volume;
existing.loop = loop;
} else {
const audio = document.createElement('audio');
audio.id = 'cai-bg-audio';
audio.src = url;
audio.loop = loop;
audio.volume = volume;
audio.style.display = 'none';
document.body.appendChild(audio);
audio.play().catch(() => {});
}
}
let _particleEngine = null;
function applyParticles(settings) {
const enabled = settings.particlesEnabled;
if (!enabled) {
if (_particleEngine) { _particleEngine.stop(); _particleEngine = null; }
const existing = document.getElementById('cai-particles-canvas');
if (existing) existing.remove();
return;
}
const chatBody = document.querySelector('#chat-body');
if (!chatBody) return;
chatBody.style.position = 'relative';
let canvas = document.getElementById('cai-particles-canvas');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = 'cai-particles-canvas';
const rect = chatBody.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
chatBody.appendChild(canvas);
}
if (!_particleEngine) {
const ParticleEngine = (() => {
const EFFECTS = {
rain: { countRange: [20,200], create(rng) { return { x: Math.random() * rng.w, y: Math.random() * rng.h * -1 - 20, length: 8 + Math.random() * 16, speed: 6 + Math.random() * 8, opacity: 0.3 + Math.random() * 0.5 }; }, update(p, dt, rng) { p.y += p.speed * dt * 60; p.x += 1.5 * dt * 60; if (p.y > rng.h + 20) { p.y = -20; p.x = Math.random() * rng.w; } }, draw(p, ctx) { ctx.strokeStyle = `rgba(180,210,255,${p.opacity})`; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + 2, p.y + p.length); ctx.stroke(); } },
snow: { countRange: [15,120], create(rng) { return { x: Math.random() * rng.w, y: Math.random() * rng.h * -1 - 10, radius: 1.5 + Math.random() * 3, speed: 1 + Math.random() * 3, drift: 0.3 + Math.random() * 0.8, opacity: 0.4 + Math.random() * 0.6, phase: Math.random() * Math.PI * 2 }; }, update(p, dt, rng) { p.y += p.speed * dt * 60; p.x += Math.sin(p.phase + Date.now() * 0.001 * p.drift) * 0.3 * dt * 60; if (p.y > rng.h + 10) { p.y = -10; p.x = Math.random() * rng.w; p.phase = Math.random() * Math.PI * 2; } }, draw(p, ctx) { ctx.fillStyle = `rgba(255,255,255,${p.opacity})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fill(); } },
leaves: { countRange: [8,60], create(rng) { const colors = ['#5a8f4c','#7ab363','#c4a34a','#bf7f3f','#8b5e3c','#4a7c59']; return { x: Math.random() * rng.w, y: Math.random() * rng.h * -1 - 30, size: 6 + Math.random() * 10, speed: 1 + Math.random() * 2.5, swayAmp: 20 + Math.random() * 40, swayFreq: 0.3 + Math.random() * 0.4, rotation: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.1, phase: Math.random() * Math.PI * 2, color: colors[Math.floor(Math.random() * colors.length)] }; }, update(p, dt, rng) { p.y += p.speed * dt * 60; p.x += Math.sin(p.phase + Date.now() * 0.001 * p.swayFreq) * 0.5 * dt * 60; p.rotation += p.rotSpeed * dt * 60; if (p.y > rng.h + 30) { p.y = -30; p.x = Math.random() * rng.w; p.phase = Math.random() * Math.PI * 2; } }, draw(p, ctx) { ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rotation); ctx.fillStyle = p.color; ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.ellipse(0, 0, p.size * 0.5, p.size * 0.25, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } },
cherry_blossom: { countRange: [8,50], create(rng) { const colors = ['#f8d7e0','#f2b5c4','#e891a8','#f0c0d0','#fce4ec']; return { x: Math.random() * rng.w, y: Math.random() * rng.h * -1 - 30, size: 5 + Math.random() * 8, speed: 0.8 + Math.random() * 2, swayAmp: 30 + Math.random() * 50, swayFreq: 0.2 + Math.random() * 0.3, rotation: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.08, phase: Math.random() * Math.PI * 2, color: colors[Math.floor(Math.random() * colors.length)] }; }, update(p, dt, rng) { p.y += p.speed * dt * 60; p.x += Math.sin(p.phase + Date.now() * 0.001 * p.swayFreq) * 0.4 * dt * 60; p.rotation += p.rotSpeed * dt * 60; if (p.y > rng.h + 30) { p.y = -30; p.x = Math.random() * rng.w; p.phase = Math.random() * Math.PI * 2; } }, draw(p, ctx) { ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rotation); ctx.fillStyle = p.color; ctx.globalAlpha = 0.8; ctx.beginPath(); for (let i = 0; i < 5; i++) { const a = (i / 5) * Math.PI * 2 - Math.PI / 2; const r = p.size * 0.4; const x = Math.cos(a) * r; const y = Math.sin(a) * r; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.closePath(); ctx.fill(); ctx.restore(); } },
bubbles: { countRange: [10,80], create(rng) { return { x: Math.random() * rng.w, y: rng.h + 10 + Math.random() * 50, radius: 3 + Math.random() * 8, speed: 1 + Math.random() * 3, wobble: 0.5 + Math.random() * 1.5, opacity: 0.15 + Math.random() * 0.35, phase: Math.random() * Math.PI * 2 }; }, update(p, dt, rng) { p.y -= p.speed * dt * 60; p.x += Math.sin(p.phase + Date.now() * 0.001 * p.wobble) * 0.2 * dt * 60; if (p.y < -20) { p.y = rng.h + 10; p.x = Math.random() * rng.w; p.phase = Math.random() * Math.PI * 2; } }, draw(p, ctx) { ctx.save(); ctx.globalAlpha = p.opacity; ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = 'rgba(255,255,255,0.08)'; ctx.fill(); ctx.restore(); } },
stars: { countRange: [10,60], create(rng) { return { x: Math.random() * rng.w, y: Math.random() * rng.h, size: 1 + Math.random() * 2.5, baseOpacity: 0.2 + Math.random() * 0.5, twinkleSpeed: 0.5 + Math.random() * 2, phase: Math.random() * Math.PI * 2 }; }, update(p, dt) { }, draw(p, ctx) { const alpha = p.baseOpacity + Math.sin(Date.now() * 0.001 * p.twinkleSpeed + p.phase) * 0.3; ctx.fillStyle = `rgba(255,255,255,${Math.max(0, alpha)})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); } }
};
return class {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.particles = [];
this.running = false;
this.rafId = null;
this.config = { effect: 'rain', density: 50, speed: 50, opacity: 100 };
this._onResize = () => { const p = this.canvas.parentElement; if (p) { const r = p.getBoundingClientRect(); this.canvas.width = r.width; this.canvas.height = r.height; } };
window.addEventListener('resize', this._onResize);
}
setConfig(config) {
const effectChanged = config.effect && config.effect !== this.config.effect;
Object.assign(this.config, config);
const effect = EFFECTS[this.config.effect];
if (!effect) return;
const [min, max] = effect.countRange;
const targetCount = min + Math.floor((max - min) * (this.config.density / 100));
const speedMul = 0.5 + (this.config.speed / 100) * 1.5;
const rng = { w: this.canvas.width, h: this.canvas.height };
if (effectChanged) {
this.particles = [];
for (let i = 0; i < targetCount; i++) this.particles.push(effect.create(rng));
} else if (this.config.effect === 'stars') {
this.particles = [];
for (let i = 0; i < targetCount; i++) this.particles.push(effect.create(rng));
} else {
while (this.particles.length < targetCount) this.particles.push(effect.create(rng));
if (this.particles.length > targetCount) this.particles.length = targetCount;
this.particles.forEach(p => { if (!('_baseSpeed' in p)) p._baseSpeed = p.speed; p.speed = p._baseSpeed * speedMul; });
}
}
start() {
if (this.running) return;
this.running = true;
const ctx = this.ctx;
const canvas = this.canvas;
const effect = EFFECTS[this.config.effect];
if (!effect) return;
let lastTime = performance.now();
const rng = { w: canvas.width, h: canvas.height };
const loop = (now) => {
if (!this.running) return;
const pEl = canvas.parentElement;
if (pEl) { const pr = pEl.getBoundingClientRect(); if (canvas.width !== pr.width || canvas.height !== pr.height) { canvas.width = pr.width; canvas.height = pr.height; } }
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = this.config.opacity / 100;
const e = EFFECTS[this.config.effect];
rng.w = canvas.width;
rng.h = canvas.height;
for (const p of this.particles) {
e.update(p, dt, rng);
e.draw(p, ctx);
}
ctx.globalAlpha = 1;
this.rafId = requestAnimationFrame(loop);
};
this.rafId = requestAnimationFrame(loop);
}
stop() {
this.running = false;
if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; }
this.particles = [];
if (this.ctx) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
destroy() {
this.stop();
window.removeEventListener('resize', this._onResize);
}
};
})();
_particleEngine = new ParticleEngine(canvas);
}
_particleEngine.setConfig({
effect: settings.particlesEffect || 'rain',
density: settings.particlesDensity !== undefined ? settings.particlesDensity : 50,
speed: settings.particlesSpeed !== undefined ? settings.particlesSpeed : 50,
opacity: settings.particlesOpacity !== undefined ? settings.particlesOpacity : 100
});
if (!_particleEngine.running) _particleEngine.start();
}
function applyOpacity(value) {
const containers = [
document.querySelector('#chat-body'),
document.querySelector('#chat-messages')
].filter(Boolean);
containers.forEach(el => {
el.style.setProperty('--bubble-opacity', (value / 100).toString());
});
const previewBubbles = document.querySelectorAll('#preview-char, #preview-user');
previewBubbles.forEach(b => {
b.style.opacity = (value / 100).toString();
});
}
function applyToggles() {
document.body.dataset.caiHideInput = 'true';
document.body.dataset.caiHideWarning = 'true';
document.body.dataset.caiHideBadge = 'true';
}
function applyCharacterInfoVisibility() {
document.body.dataset.caiHideCharinfo = 'true';
}
function applyInputWidth(value) {
const chatBody = document.querySelector('#chat-body');
if (!chatBody) return;
const outer = chatBody.querySelector('.max-w-2xl');
const inner = outer ? outer.querySelector('.max-w-3xl') : null;
if (outer) {
outer.style.maxWidth = 'none';
outer.style.width = '100%';
}
if (inner) {
const rem = (value / 100) * 42;
inner.style.maxWidth = rem + 'rem';
}
}
function applyOutlines(enabled, settings) {
const containers = [
document.querySelector('#chat-body'),
document.querySelector('#chat-messages')
].filter(Boolean);
if (!enabled) {
containers.forEach(el => {
el.style.removeProperty('--outline-bot');
el.style.removeProperty('--outline-user');
});
return;
}
const theme = themes.find(t => t.name === (settings.theme || 'Default'));
let charColor, userColor;
if (theme && theme.name !== 'Default') {
charColor = theme.charColor;
userColor = theme.userColor;
} else {
charColor = 'var(--surface-elevation-3)';
userColor = 'var(--surface-elevation-2)';
}
containers.forEach(el => {
el.style.setProperty('--outline-bot', `0 0 0 2px ${userColor}`);
el.style.setProperty('--outline-user', `0 0 0 2px ${charColor}`);
});
}
function applyTextSize(value) {
document.documentElement.style.setProperty('--text-scale', (value / 100) + 'rem');
const target = document.querySelector('#chat-messages');
if (target) {
target.style.setProperty('--text-scale', (value / 100) + 'rem');
}
}
function applyBubbleRadius(value) {
document.documentElement.style.setProperty('--bubble-radius', value + 'px');
const chatMessages = document.querySelector('#chat-messages');
if (chatMessages) {
chatMessages.style.setProperty('--bubble-radius', value + 'px');
}
const charBubble = document.querySelector('#preview-char');
const userBubble = document.querySelector('#preview-user');
if (charBubble && userBubble) {
const r = value + 'px';
charBubble.style.borderRadius = r;
userBubble.style.borderRadius = r;
}
}
let customCssStyleEl = null;
function applyCustomCss(css) {
if (customCssStyleEl) customCssStyleEl.remove();
if (!css || !css.trim()) return;
customCssStyleEl = document.createElement('style');
customCssStyleEl.id = 'cai-custom-css';
customCssStyleEl.textContent = css;
document.head.appendChild(customCssStyleEl);
}
function loadSavedSettings(retries = 20) {
const settings = getCurrentSettings();
_chatMessagesRetries = 40;
applyChatIndependentSettings(settings);
const body = document.querySelector('#chat-body');
if (!body) {
if (retries > 0) {
setTimeout(() => loadSavedSettings(retries - 1), 500);
}
return;
}
applyTheme(settings.theme || 'Default');
applyOpacity(settings.opacity !== undefined ? settings.opacity : 100);
applyBackground(settings);
applyToggles();
applyCharacterInfoVisibility();
applyFont({fontName: getFontSetting()});
applyInputWidth(settings.inputWidth !== undefined ? settings.inputWidth : 150);
applyOutlines(settings.bubbleOutlines || false, settings);
applyTextSize(settings.textSize !== undefined ? settings.textSize : 100);
applyBubbleRadius(settings.bubbleRadius !== undefined ? settings.bubbleRadius : 16);
applyBubbleStyle(settings.bubbleStyle || null);
applyCustomCss(settings.customCss || '');
if (settings.audioWallpaperUrl && settings.audioWallpaperUrl.startsWith('blob:')) {
const data = getStorageData();
const chatId = getChatIdFromUrl();
const scope = chatId && data.chats[chatId] ? data.chats[chatId] : data.global;
delete scope.audioWallpaperUrl;
delete scope.audioWallpaperVolume;
delete scope.audioWallpaperLoop;
saveStorageData(data);
settings.audioWallpaperUrl = undefined;
}
applyAudioWallpaper(settings);
applyParticles(settings);
const g = getStorageData().global;
applyImmersion(g);
}
let _chatMessagesTimer = null;
let _chatMessagesRetries = 40;
function applyChatIndependentSettings(settings) {
clearTimeout(_chatMessagesTimer);
const chatMessages = document.querySelector('#chat-messages');
if (!chatMessages) {
if (_chatMessagesRetries > 0) {
_chatMessagesRetries--;
_chatMessagesTimer = setTimeout(() => applyChatIndependentSettings(settings), 300);
}
return;
}
applyTextSize(settings.textSize !== undefined ? settings.textSize : 100);
applyBubbleRadius(settings.bubbleRadius !== undefined ? settings.bubbleRadius : 16);
applyFont({fontName: getFontSetting()});
}
function removeOriginalCustomize() {
const rows = document.querySelectorAll('#chat-details .flex.justify-between.items-center.w-full');
for (const row of rows) {
const btn = row.querySelector('button');
if (!btn) continue;
const svg = btn.querySelector('svg[viewBox="0 0 512 512"]');
if (svg) {
row.remove();
}
}
}
function injectCustomButton() {
const detailsPanel = document.querySelector('#chat-details');
if (!detailsPanel) return;
const targetContainer = detailsPanel.querySelector('.flex.flex-col.gap-3.h-fit');
if (!targetContainer) return;
const existing = targetContainer.querySelector('.cai-custom-button');
if (existing) {
const wrapper = existing.closest('.flex.justify-between.items-center.w-full');
if (wrapper) wrapper.remove();
else existing.remove();
}
const wrapper = document.createElement('div');
wrapper.className = 'flex justify-between items-center w-full';
const button = document.createElement('button');
button.className = 'cai-custom-button z-0 group relative items-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 hover:bg-surface-elevation-1 px-unit-4 min-w-unit-20 h-unit-10 text-md gap-unit-2 rounded-md [&>svg]:max-w-[theme(spacing.unit-8)] data-[pressed=true]:scale-[0.97] transition-transform-colors-opacity motion-reduce:transition-none bg-ghost text-primary data-[hover=true]:opacity-hover w-full flex justify-between rounded-spacing-xs';
button.type = 'button';
button.innerHTML = `
<div class="flex items-center w-fit gap-3">
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" class="size-5 p-1" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M167.02 309.34c-40.12 2.58-76.53 17.86-97.19 72.3-2.35 6.21-8 9.98-14.59 9.98-11.11 0-45.46-27.67-55.25-34.35C0 439.62 37.93 512 128 512c75.86 0 128-43.77 128-120.19 0-3.11-.65-6.08-.97-9.13l-88.01-73.34zM457.89 0c-15.16 0-29.37 6.71-40.21 16.45C213.27 199.05 192 203.34 192 257.09c0 13.7 3.25 26.76 8.73 38.7l63.82 53.18c7.21 1.8 14.64 3.03 22.39 3.03 62.11 0 98.11-45.47 211.16-256.46 7.38-14.35 13.9-29.85 13.9-45.99C512 20.64 486 0 457.89 0z"></path>
</svg>
<span>Customize</span>
</div>
<div class="flex gap-1 items-center">
<svg viewBox="0 0 24 24" fill="none" class="text-muted-foreground size-3">
<path d="m9 4 8 8-8 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</div>
`;
wrapper.appendChild(button);
button.addEventListener('click', openCustomModal);
let historyBtn = null;
for (const child of targetContainer.children) {
if (child.tagName === 'BUTTON') {
const text = child.textContent.trim();
if (text === 'History' || text.includes('History')) {
historyBtn = child;
break;
}
}
}
if (!historyBtn) {
const buttons = targetContainer.querySelectorAll('button');
for (const btn of buttons) {
const svg = btn.querySelector('svg');
if (svg && svg.innerHTML && svg.innerHTML.includes('M15 4.582a8 8 0 0 0-6.5 14.614')) {
historyBtn = btn;
break;
}
}
}
if (historyBtn && historyBtn.parentNode === targetContainer) {
targetContainer.insertBefore(wrapper, historyBtn.nextSibling);
} else {
let pinnedWrapper = null;
for (const child of targetContainer.children) {
if (child.tagName === 'DIV' && child.classList.contains('flex') && child.classList.contains('justify-between')) {
const btn = child.querySelector('button');
if (btn && btn.textContent && btn.textContent.trim() === 'Pinned') {
pinnedWrapper = child;
break;
}
}
}
if (pinnedWrapper) {
targetContainer.insertBefore(wrapper, pinnedWrapper);
} else {
targetContainer.appendChild(wrapper);
}
}
}
function createCustomColorPicker(initialColor, onChange) {
const container = document.createElement('div');
container.className = 'cai-custom-color-picker';
const palette = [
'#000000', '#1a1a1a', '#333333', '#4d4d4d', '#666666', '#808080', '#999999', '#b3b3b3', '#cccccc', '#e6e6e6', '#ffffff',
'#ff0000', '#cc0000', '#990000', '#660000', '#330000',
'#ff6600', '#cc4400', '#993300', '#662200', '#331100',
'#ffcc00', '#cc9900', '#997700', '#664400', '#332200',
'#00ff00', '#00cc00', '#009900', '#006600', '#003300',
'#00ccff', '#0099cc', '#006699', '#003366', '#001133',
'#0000ff', '#0000cc', '#000099', '#000066', '#000033',
'#cc00ff', '#9900cc', '#660099', '#330066', '#1a0033',
'#ff00cc', '#cc0099', '#990066', '#660033', '#33001a',
'#ff0066', '#cc0052', '#99003d', '#660029', '#330014'
];
palette.forEach(color => {
const swatch = document.createElement('div');
swatch.className = 'color-swatch' + (color === initialColor ? ' active' : '');
swatch.style.backgroundColor = color;
swatch.dataset.color = color;
swatch.addEventListener('click', function() {
container.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
this.classList.add('active');
const hex = this.dataset.color;
hexInput.value = hex.toUpperCase();
onChange(hex);
});
container.appendChild(swatch);
});
const hexInput = document.createElement('input');
hexInput.type = 'text';
hexInput.className = 'color-hex-input';
hexInput.maxLength = 7;
hexInput.value = initialColor.toUpperCase();
hexInput.addEventListener('input', function() {
let val = this.value.trim();
if (val.startsWith('#')) {
val = val.substring(1);
}
if (/^[0-9a-fA-F]{6}$/.test(val)) {
const full = '#' + val.toUpperCase();
container.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
let found = false;
container.querySelectorAll('.color-swatch').forEach(s => {
if (s.dataset.color.toUpperCase() === full.toUpperCase()) {
s.classList.add('active');
found = true;
}
});
if (!found) {
// custom color
}
onChange(full);
}
});
hexInput.addEventListener('blur', function() {
let val = this.value.trim();
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
this.value = val.toUpperCase();
} else {
const current = container.querySelector('.color-swatch.active')?.dataset.color || '#000000';
this.value = current.toUpperCase();
}
});
container.appendChild(hexInput);
const label = document.createElement('span');
label.textContent = 'hex';
label.style.fontSize = '10px';
label.style.color = 'var(--muted-foreground)';
label.style.marginLeft = '4px';
container.appendChild(label);
const initialSwatch = container.querySelector(`.color-swatch[data-color="${initialColor}"]`);
if (initialSwatch) initialSwatch.classList.add('active');
return {
container: container,
setColor: function(color) {
container.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
const swatch = container.querySelector(`.color-swatch[data-color="${color}"]`);
if (swatch) swatch.classList.add('active');
hexInput.value = color.toUpperCase();
}
};
}
function openAudioWallpaperDialog() {
const settings = getCurrentSettings();
const url = settings.audioWallpaperUrl || '';
const volume = settings.audioWallpaperVolume !== undefined ? settings.audioWallpaperVolume : 100;
const loop = settings.audioWallpaperLoop !== undefined ? settings.audioWallpaperLoop : true;
document.querySelector('.cai-audio-dialog')?.remove();
const dialog = document.createElement('div');
dialog.className = 'cai-audio-dialog fixed max-w-full z-[60] shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-spacing-l w-11/12 sm:max-w-md p-0 flex flex-col gap-0 bg-surface-elevation-1 text-foreground';
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('data-state', 'open');
dialog.style.pointerEvents = 'auto';
dialog.tabIndex = -1;
const isActive = !!url;
const audioEl = document.getElementById('cai-bg-audio');
const isPlaying = isActive && audioEl && !audioEl.paused;
dialog.innerHTML = `
<div class="w-full flex flex-row justify-between items-center px-4 py-3 sm:px-6 sm:py-4 border-b border-border-outline">
<h2 class="text-base sm:text-lg font-medium">Audio</h2>
<button type="button" class="close-dialog-btn rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
</div>
<div class="cai-audio-body flex flex-col px-4 pb-4 pt-4 sm:px-6 sm:pb-6 sm:pt-4 gap-4">
<div class="cai-audio-upload-row">
<div class="cai-audio-visualizer ${isActive ? 'active' : ''}">
<div class="cai-wave-bar" style="animation-delay:0s"></div>
<div class="cai-wave-bar" style="animation-delay:0.1s"></div>
<div class="cai-wave-bar" style="animation-delay:0.2s"></div>
<div class="cai-wave-bar" style="animation-delay:0.3s"></div>
<div class="cai-wave-bar" style="animation-delay:0.4s"></div>
</div>
<div class="cai-audio-upload-area">
<button class="cai-audio-upload-btn" id="cai-audio-upload-btn">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span>${url ? 'Change' : 'Upload'}</span>
</button>
<div class="cai-audio-link-area">
<div class="cai-audio-link-label">or paste a link</div>
<input type="text" class="cai-audio-link-input" id="cai-audio-link-input" placeholder="https://example.com/audio.mp3" value="${escapeHtml(url)}" />
</div>
</div>
</div>
<div class="cai-audio-player-bar" id="cai-audio-player-bar" style="display: ${isActive ? 'flex' : 'none'};">
<button type="button" class="cai-audio-play-btn" id="cai-audio-play-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
${isPlaying ? '<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>' : '<path d="M8 5v14l11-7z"/>'}
</svg>
</button>
<div class="cai-audio-seek-track">
<div class="cai-audio-seek-fill" id="cai-audio-seek-fill" style="width:0%"></div>
<input type="range" min="0" max="100" value="0" class="cai-audio-seek-slider" id="cai-audio-seek-slider" />
</div>
<span class="cai-audio-time" id="cai-audio-time">0:00 / 0:00</span>
</div>
<div class="cai-audio-controls ${isActive ? '' : 'muted'}">
<div class="cai-audio-vol-row">
<label class="cai-audio-vol-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
</svg>
Volume
</label>
<div class="cai-audio-vol-track">
<div class="cai-audio-vol-fill" style="width:${volume}%"></div>
<input type="range" min="0" max="100" value="${volume}" class="cai-audio-vol-slider" id="cai-audio-vol-slider" />
</div>
<span class="cai-audio-vol-pct" id="cai-audio-vol-label">${volume}%</span>
</div>
<div class="cai-audio-loop-row">
<label class="cai-audio-loop-label">Loop</label>
<div class="cai-audio-loop-toggle ${loop ? 'on' : ''}" id="cai-audio-loop-toggle" role="switch" aria-checked="${loop}" tabindex="0">
<div class="cai-audio-loop-thumb"></div>
</div>
</div>
<div class="cai-audio-actions">
${isActive ? '<button class="cai-audio-stop-btn" id="cai-audio-stop-btn">Stop & Remove</button>' : ''}
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
function closeDialog() {
updateAudioModalStatus(!!getCurrentSettings().audioWallpaperUrl);
dialog.classList.add('closing');
const onEnd = () => {
dialog.removeEventListener('transitionend', onEnd);
dialog.remove();
};
dialog.addEventListener('transitionend', onEnd);
setTimeout(() => {
if (document.body.contains(dialog)) {
dialog.removeEventListener('transitionend', onEnd);
dialog.remove();
}
}, 300);
}
dialog.querySelector('.close-dialog-btn').addEventListener('click', closeDialog);
document.addEventListener('click', function outsideHandler(e) {
if (!dialog.contains(e.target) && dialog.parentNode) {
closeDialog();
document.removeEventListener('click', outsideHandler);
}
});
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
closeDialog();
document.removeEventListener('keydown', escHandler);
}
});
const uploadBtn = dialog.querySelector('#cai-audio-upload-btn');
if (uploadBtn) {
uploadBtn.addEventListener('click', function() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'audio/*';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const maxPermSize = 1024 * 1024;
if (file.size <= maxPermSize) {
const reader = new FileReader();
reader.onload = function(ev) {
const s = getCurrentSettings();
s.audioWallpaperUrl = ev.target.result;
saveCurrentSettings(s);
applyAudioWallpaper(s);
updateAudioModalStatus(true);
closeDialog();
};
reader.readAsDataURL(file);
} else {
const objectUrl = URL.createObjectURL(file);
const s = getCurrentSettings();
s.audioWallpaperUrl = objectUrl;
saveCurrentSettings(s);
applyAudioWallpaper(s);
updateAudioModalStatus(true);
closeDialog();
setTimeout(() => {
showAlertDialog('Audio file exceeds 1MB and cannot be saved permanently. It will play for this session only. Use a URL link for persistent playback.');
}, 100);
}
};
input.click();
});
}
const linkInput = dialog.querySelector('#cai-audio-link-input');
if (linkInput) {
let linkTimer;
linkInput.addEventListener('input', function() {
clearTimeout(linkTimer);
linkTimer = setTimeout(() => {
const val = this.value.trim();
if (val && isAudioUrl(val)) {
const s = getCurrentSettings();
s.audioWallpaperUrl = val;
saveCurrentSettings(s);
applyAudioWallpaper(s);
} else if (!val) {
const s = getCurrentSettings();
s.audioWallpaperUrl = null;
saveCurrentSettings(s);
applyAudioWallpaper(s);
}
}, 600);
});
}
function formatTime(s) {
s = Math.floor(s) || 0;
const m = Math.floor(s / 60);
const sec = s % 60;
return m + ':' + (sec < 10 ? '0' : '') + sec;
}
const playBtn = dialog.querySelector('#cai-audio-play-btn');
const seekSlider = dialog.querySelector('#cai-audio-seek-slider');
const seekFill = dialog.querySelector('#cai-audio-seek-fill');
const timeLabel = dialog.querySelector('#cai-audio-time');
const playerAudio = document.getElementById('cai-bg-audio');
function updatePlayerUI() {
if (!playerAudio || !playBtn || !timeLabel) return;
const paused = playerAudio.paused;
const svg = playBtn.querySelector('svg');
if (svg) {
svg.innerHTML = paused
? '<path d="M8 5v14l11-7z"/>'
: '<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>';
}
const cur = playerAudio.currentTime || 0;
const dur = playerAudio.duration || 0;
timeLabel.textContent = formatTime(cur) + ' / ' + formatTime(dur);
if (seekSlider && !seekSlider.dataset.dragging) {
const pct = dur > 0 ? (cur / dur) * 100 : 0;
seekSlider.value = pct;
if (seekFill) seekFill.style.width = pct + '%';
}
}
if (playBtn && playerAudio) {
playBtn.addEventListener('click', function() {
if (playerAudio.paused) {
playerAudio.play().catch(() => {});
} else {
playerAudio.pause();
}
});
playerAudio.addEventListener('play', updatePlayerUI);
playerAudio.addEventListener('pause', updatePlayerUI);
playerAudio.addEventListener('timeupdate', updatePlayerUI);
playerAudio.addEventListener('loadedmetadata', updatePlayerUI);
playerAudio.addEventListener('ended', updatePlayerUI);
}
if (seekSlider && playerAudio) {
seekSlider.addEventListener('input', function() {
this.dataset.dragging = '1';
const dur = playerAudio.duration || 0;
const pct = parseFloat(this.value);
if (seekFill) seekFill.style.width = pct + '%';
if (dur > 0) {
const t = (pct / 100) * dur;
playerAudio.currentTime = t;
if (timeLabel) timeLabel.textContent = formatTime(t) + ' / ' + formatTime(dur);
}
});
seekSlider.addEventListener('change', function() {
delete this.dataset.dragging;
});
}
const volSlider = dialog.querySelector('#cai-audio-vol-slider');
const volFill = dialog.querySelector('.cai-audio-vol-fill');
const volLabel = dialog.querySelector('#cai-audio-vol-label');
if (volSlider) {
volSlider.addEventListener('input', function() {
const val = parseInt(this.value);
if (volFill) volFill.style.width = val + '%';
if (volLabel) volLabel.textContent = val + '%';
const s = getCurrentSettings();
s.audioWallpaperVolume = val;
saveCurrentSettings(s);
applyAudioWallpaper(s);
});
}
const loopToggle = dialog.querySelector('#cai-audio-loop-toggle');
if (loopToggle) {
loopToggle.addEventListener('click', function() {
const now = this.classList.toggle('on');
this.setAttribute('aria-checked', now);
const s = getCurrentSettings();
s.audioWallpaperLoop = now;
saveCurrentSettings(s);
applyAudioWallpaper(s);
});
}
const stopBtn = dialog.querySelector('#cai-audio-stop-btn');
if (stopBtn) {
stopBtn.addEventListener('click', function() {
const s = getCurrentSettings();
s.audioWallpaperUrl = null;
saveCurrentSettings(s);
applyAudioWallpaper(s);
updateAudioModalStatus(false);
closeDialog();
});
}
function updateAudioModalStatus(hasAudio) {
const mainModal = document.querySelector('.cai-custom-modal');
if (!mainModal) return;
const statusEl = mainModal.querySelector('.cai-audio-status');
const btnEl = mainModal.querySelector('#cai-audio-add-btn');
if (statusEl) {
if (!hasAudio) {
statusEl.textContent = 'No audio set';
} else {
const audio = document.getElementById('cai-bg-audio');
statusEl.textContent = audio && !audio.paused ? 'Now playing' : 'Paused';
}
}
if (btnEl) btnEl.textContent = hasAudio ? 'Edit' : 'Add';
}
}
function openWallpaperDialog() {
const settings = getWallpaperSettings();
const bgType = settings.bgType || 'file';
const wallpaper = settings.wallpaper;
const backgroundColor = settings.backgroundColor || '#00000000';
const wallpaperOpacity = settings.wallpaperOpacity !== undefined ? settings.wallpaperOpacity : 100;
document.querySelector('.cai-wallpaper-dialog')?.remove();
const dialog = document.createElement('div');
dialog.className = 'cai-wallpaper-dialog fixed max-w-full z-[60] shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-spacing-l w-11/12 sm:max-w-md p-0 flex flex-col gap-0 bg-surface-elevation-1 text-foreground';
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('data-state', 'open');
dialog.style.pointerEvents = 'auto';
dialog.tabIndex = -1;
dialog.innerHTML = `
<div class="w-full flex flex-row justify-between items-center px-4 py-3 sm:px-6 sm:py-4 border-b border-border-outline">
<h2 class="text-base sm:text-lg font-medium">Wallpaper</h2>
<button type="button" class="close-dialog-btn rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
<span class="sr-only">Close</span>
</button>
</div>
<div class="flex flex-col px-4 pb-4 pt-4 sm:px-6 sm:pb-6 sm:pt-4 gap-4">
<div class="w-full flex flex-col gap-2">
<p class="text-sm text-muted-foreground">Choose an image or solid color for your chat background.</p>
<div class="cai-bg-type-group" role="group" aria-label="Background type">
<button class="bg-type-btn ${bgType === 'file' ? 'active' : ''}" data-type="file">File</button>
<button class="bg-type-btn ${bgType === 'link' ? 'active' : ''}" data-type="link">Link</button>
<button class="bg-type-btn ${bgType === 'color' ? 'active' : ''}" data-type="color">Color</button>
</div>
<div class="cai-bg-input-area" id="cai-bg-input-area">
<div class="cai-bg-file-input" style="display: ${bgType === 'file' ? 'block' : 'none'};">
<button class="cai-bg-upload-btn ${wallpaper ? 'has-image' : ''}" id="cai-wallpaper-upload-btn">
${wallpaper ? '✓ Wallpaper uploaded' : 'Click to upload an image or video'}
</button>
${wallpaper && bgType === 'file' ? `<button class="cai-bg-remove-btn" id="cai-wallpaper-remove-btn">Remove</button>` : ''}
</div>
<div class="cai-bg-link-input" style="display: ${bgType === 'link' ? 'block' : 'none'};">
<input type="text" id="cai-bg-link-input" placeholder="Enter image URL..." value="${bgType === 'link' && wallpaper ? escapeHtml(wallpaper) : ''}" />
</div>
<div class="cai-bg-color-input" style="display: ${bgType === 'color' ? 'block' : 'none'};">
<div id="cai-bg-color-picker-container"></div>
</div>
</div>
<div id="cai-bg-advanced" style="display: ${bgType !== 'color' ? 'block' : 'none'}; margin-top: 8px;">
<div class="flex items-center gap-2 text-sm mt-2">
<label class="text-xs text-muted-foreground">Opacity</label>
<div class="flex items-center gap-1 flex-1">
<input type="range" min="10" max="100" value="${wallpaperOpacity}" class="cai-wallpaper-opacity-slider flex-1 cai-slider" />
<span class="cai-wallpaper-opacity-label text-xs w-8">${wallpaperOpacity}%</span>
</div>
</div>
<div id="cai-bg-video-options" style="display: ${wallpaper && isVideoUrl(wallpaper) ? 'block' : 'none'}; margin-top: 8px;">
<div class="flex items-center gap-2 text-sm mt-2">
<label class="text-xs text-muted-foreground">Speed</label>
<div class="flex items-center gap-1 flex-1">
<input type="range" min="0.25" max="2" step="0.25" value="${settings.videoSpeed || 1}" class="cai-video-speed-slider flex-1 cai-slider" />
<span class="cai-video-speed-label text-xs w-8">${(settings.videoSpeed || 1).toFixed(2).replace(/\.?0+$/, '')}x</span>
</div>
</div>
<div class="flex items-center justify-between text-sm mt-2">
<label class="text-xs text-muted-foreground">Sound</label>
<button type="button" role="switch" aria-checked="${settings.videoSound || false}" id="cai-video-sound-toggle" class="cai-toggle-track ${settings.videoSound ? 'bg-blue' : 'bg-muted'}">
<span class="cai-toggle-thumb" style="transform: ${settings.videoSound ? 'translateX(16px)' : 'translateX(0)'}"></span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
const bgPickerContainer = document.getElementById('cai-bg-color-picker-container');
if (bgPickerContainer) {
const bgPicker = createCustomColorPicker(backgroundColor, function(color) {
const settingsNow = getWallpaperSettings();
settingsNow.bgType = 'color';
settingsNow.backgroundColor = color;
saveCurrentSettings(settingsNow);
applyBackground(settingsNow);
});
bgPickerContainer.appendChild(bgPicker.container);
}
function closeDialog() {
dialog.classList.add('closing');
const onTransitionEnd = () => {
dialog.removeEventListener('transitionend', onTransitionEnd);
dialog.remove();
};
dialog.addEventListener('transitionend', onTransitionEnd);
setTimeout(() => {
if (document.body.contains(dialog)) {
dialog.removeEventListener('transitionend', onTransitionEnd);
dialog.remove();
}
}, 300);
}
dialog.querySelector('.close-dialog-btn').addEventListener('click', closeDialog);
document.addEventListener('click', function outsideClickHandler(e) {
if (!dialog.contains(e.target) && dialog.parentNode) {
closeDialog();
document.removeEventListener('click', outsideClickHandler);
}
});
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
closeDialog();
document.removeEventListener('keydown', escHandler);
}
});
const typeBtns = dialog.querySelectorAll('.bg-type-btn');
const fileInputArea = dialog.querySelector('.cai-bg-file-input');
const linkInputArea = dialog.querySelector('.cai-bg-link-input');
const colorInputArea = dialog.querySelector('.cai-bg-color-input');
const advancedSection = dialog.querySelector('#cai-bg-advanced');
typeBtns.forEach(btn => {
btn.addEventListener('click', function() {
const type = this.dataset.type;
typeBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
const settingsNow = getWallpaperSettings();
settingsNow.bgType = type;
fileInputArea.style.display = type === 'file' ? 'block' : 'none';
linkInputArea.style.display = type === 'link' ? 'block' : 'none';
colorInputArea.style.display = type === 'color' ? 'block' : 'none';
if (advancedSection) advancedSection.style.display = type !== 'color' ? 'block' : 'none';
const videoOpts = dialog.querySelector('#cai-bg-video-options');
if (videoOpts) videoOpts.style.display = (type !== 'color' && settingsNow.wallpaper && isVideoUrl(settingsNow.wallpaper)) ? 'block' : 'none';
if (type === 'file') {
const hasImg = !!settingsNow.wallpaper;
uploadBtn.textContent = hasImg ? '✓ Wallpaper uploaded' : 'Click to upload an image or video';
uploadBtn.classList.toggle('has-image', hasImg);
let removeBtn = dialog.querySelector('#cai-wallpaper-remove-btn');
if (hasImg && !removeBtn) {
removeBtn = document.createElement('button');
removeBtn.id = 'cai-wallpaper-remove-btn';
removeBtn.className = 'cai-bg-remove-btn';
removeBtn.textContent = 'Remove';
removeBtn.addEventListener('click', function() {
const s = getWallpaperSettings();
s.wallpaper = null;
saveCurrentSettings(s);
applyBackground(s);
uploadBtn.textContent = 'Click to upload an image or video';
uploadBtn.classList.remove('has-image');
this.remove();
});
fileInputArea.appendChild(removeBtn);
} else if (!hasImg && removeBtn) {
removeBtn.remove();
}
} else if (type === 'color') {
settingsNow.wallpaper = null;
} else if (type === 'link') {
const linkInput = dialog.querySelector('#cai-bg-link-input');
if (settingsNow.wallpaper) {
linkInput.value = settingsNow.wallpaper;
settingsNow.wallpaper = linkInput.value;
} else {
linkInput.value = '';
settingsNow.wallpaper = null;
}
}
saveCurrentSettings(settingsNow);
applyBackground(settingsNow);
});
});
const uploadBtn = dialog.querySelector('#cai-wallpaper-upload-btn');
if (uploadBtn) {
uploadBtn.addEventListener('click', function() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*,video/mp4,video/webm,video/ogg';
input.onchange = async function(e) {
const file = e.target.files[0];
if (!file) return;
const maxSize = file.type.startsWith('video/') ? 10 : file.type === 'image/gif' ? 5 : 3;
if (file.size > maxSize * 1024 * 1024) {
const proceed = await showConfirmDialog(`File is larger than ${maxSize}MB. Large files may fail to save due to browser storage limits. Continue anyway?`);
if (!proceed) return;
}
const reader = new FileReader();
reader.onload = function(ev) {
const dataUrl = ev.target.result;
const settingsNow = getWallpaperSettings();
settingsNow.bgType = 'file';
settingsNow.wallpaper = dataUrl;
const saved = saveCurrentSettings(settingsNow);
applyBackground(settingsNow);
const isVideo = isVideoUrl(dataUrl);
if (!saved && isVideo) {
showAlertDialog('Video wallpaper is too large for permanent storage and will only last until the page is refreshed.');
}
uploadBtn.textContent = '✓ Wallpaper uploaded';
uploadBtn.classList.add('has-image');
typeBtns.forEach(b => b.classList.remove('active'));
dialog.querySelector('.bg-type-btn[data-type="file"]').classList.add('active');
fileInputArea.style.display = 'block';
linkInputArea.style.display = 'none';
colorInputArea.style.display = 'none';
const videoOpts = dialog.querySelector('#cai-bg-video-options');
if (videoOpts) videoOpts.style.display = isVideo ? 'block' : 'none';
const existingRemove = dialog.querySelector('#cai-wallpaper-remove-btn');
if (!existingRemove) {
const removeBtn = document.createElement('button');
removeBtn.id = 'cai-wallpaper-remove-btn';
removeBtn.className = 'cai-bg-remove-btn';
removeBtn.textContent = 'Remove';
fileInputArea.appendChild(removeBtn);
removeBtn.addEventListener('click', function() {
const settingsNow2 = getWallpaperSettings();
settingsNow2.wallpaper = null;
saveCurrentSettings(settingsNow2);
applyBackground(settingsNow2);
uploadBtn.textContent = 'Click to upload an image or video';
uploadBtn.classList.remove('has-image');
if (videoOpts) videoOpts.style.display = 'none';
removeBtn.remove();
});
}
};
reader.onerror = function() {
showAlertDialog('Failed to read the file. Please try again.');
uploadBtn.textContent = 'Click to upload an image or video';
};
reader.readAsDataURL(file);
};
input.click();
});
}
const removeBtn = dialog.querySelector('#cai-wallpaper-remove-btn');
if (removeBtn) {
removeBtn.addEventListener('click', function() {
const settingsNow = getWallpaperSettings();
settingsNow.wallpaper = null;
saveCurrentSettings(settingsNow);
applyBackground(settingsNow);
const uploadBtn2 = dialog.querySelector('#cai-wallpaper-upload-btn');
if (uploadBtn2) {
uploadBtn2.textContent = 'Click to upload an image or video';
uploadBtn2.classList.remove('has-image');
}
const videoOpts = dialog.querySelector('#cai-bg-video-options');
if (videoOpts) videoOpts.style.display = 'none';
this.remove();
});
}
const linkInput = dialog.querySelector('#cai-bg-link-input');
if (linkInput) {
let linkTimeout;
linkInput.addEventListener('input', function() {
clearTimeout(linkTimeout);
linkTimeout = setTimeout(() => {
const url = this.value.trim();
const settingsNow = getWallpaperSettings();
if (url) {
settingsNow.bgType = 'link';
settingsNow.wallpaper = url;
} else {
settingsNow.wallpaper = null;
}
saveCurrentSettings(settingsNow);
applyBackground(settingsNow);
const videoOpts = dialog.querySelector('#cai-bg-video-options');
if (videoOpts) videoOpts.style.display = (url && isVideoUrl(url)) ? 'block' : 'none';
}, 500);
});
}
const wOpacitySlider = dialog.querySelector('.cai-wallpaper-opacity-slider');
const wOpacityLabel = dialog.querySelector('.cai-wallpaper-opacity-label');
if (wOpacitySlider && wOpacityLabel) {
wOpacitySlider.addEventListener('input', function() {
const val = parseInt(this.value);
wOpacityLabel.textContent = val + '%';
const settingsNow = getWallpaperSettings();
settingsNow.wallpaperOpacity = val;
saveCurrentSettings(settingsNow);
const opacity = val / 100;
document.querySelector('#chat-body')?.style.setProperty('--cai-wallpaper-opacity', opacity);
const chatVid = document.querySelector('#cai-bg-video');
if (chatVid) chatVid.style.opacity = opacity;
const previewVid = document.querySelector('.cai-preview-video');
if (previewVid) previewVid.style.opacity = opacity;
const previewBg = document.querySelector('.cai-preview-bg');
if (previewBg) previewBg.style.setProperty('--cai-preview-opacity', opacity);
});
}
const speedSlider = dialog.querySelector('.cai-video-speed-slider');
const speedLabel = dialog.querySelector('.cai-video-speed-label');
if (speedSlider && speedLabel) {
speedSlider.addEventListener('input', function() {
const val = parseFloat(this.value);
speedLabel.textContent = val.toFixed(2).replace(/\.?0+$/, '') + 'x';
const settingsNow = getWallpaperSettings();
settingsNow.videoSpeed = val;
saveCurrentSettings(settingsNow);
const chatVid = document.querySelector('#cai-bg-video');
if (chatVid) chatVid.playbackRate = val;
const previewVid = document.querySelector('.cai-preview-video');
if (previewVid) previewVid.playbackRate = val;
});
}
const soundToggle = dialog.querySelector('#cai-video-sound-toggle');
if (soundToggle) {
soundToggle.addEventListener('click', function() {
const isChecked = this.getAttribute('aria-checked') === 'true';
const newState = !isChecked;
this.setAttribute('aria-checked', newState);
this.classList.toggle('bg-blue', newState);
this.classList.toggle('bg-muted', !newState);
const thumb = this.querySelector('.cai-toggle-thumb');
if (thumb) thumb.style.transform = newState ? 'translateX(16px)' : 'translateX(0)';
const settingsNow = getWallpaperSettings();
settingsNow.videoSound = newState;
saveCurrentSettings(settingsNow);
const chatVid = document.querySelector('#cai-bg-video');
if (chatVid) chatVid.muted = !newState;
const previewVid = document.querySelector('.cai-preview-video');
if (previewVid) previewVid.muted = !newState;
});
}
}
function openCustomModal() {
document.querySelector('.cai-custom-modal')?.remove();
document.querySelector('.cai-custom-overlay')?.remove();
const data = getStorageData();
const globalData = data.global;
const chatId = getChatIdFromUrl();
const settings = chatId && data.chats[chatId] ? data.chats[chatId] : data.global;
const currentTheme = settings.theme || 'Default';
const currentOpacity = settings.opacity !== undefined ? settings.opacity : 100;
const hasWallpaper = !!(chatId && data.chats[chatId] && data.chats[chatId].wallpaper);
const hasAudioWallpaper = !!(settings.audioWallpaperUrl) && !settings.audioWallpaperUrl.startsWith('blob:');
const isAudioPlaying = hasAudioWallpaper && document.getElementById('cai-bg-audio') && !document.getElementById('cai-bg-audio').paused;
const fontName = getFontSetting();
const isPerChatFont = chatId && data.chats[chatId] && data.chats[chatId].perChatFont === true;
const immersionMode = globalData.immersionMode || false;
const modal = document.createElement('div');
modal.className = 'cai-custom-modal fixed p-6 shadow-lg data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] sm:rounded-spacing-l pt-[14px] max-w-lg overflow-y-auto flex flex-col gap-4 bg-surface-elevation-1 text-foreground px-2';
modal.style.width = '410px';
modal.style.zIndex = '52';
modal.setAttribute('role', 'dialog');
modal.setAttribute('data-state', 'open');
modal.style.pointerEvents = 'auto';
modal.style.scrollbarGutter = 'stable';
modal.tabIndex = -1;
modal.innerHTML = `
<div class="space-y-1.5 text-center sm:text-left text-display min-h-14 h-fit w-full flex flex-row justify-between items-center px-5 pt-1 -mb-4">
<div class="flex flex-row gap-4 text-md items-center">
<button type="button" class="close-modal-btn">
<svg viewBox="0 0 24 24" fill="none" height="1.25em" color="var(--icon-secondary)">
<path d="m5 5 14 14m0-14L5 19" stroke="currentColor" stroke-linecap="round" stroke-width="2"></path>
</svg>
<span class="sr-only">Close</span>
</button>
<h2 class="text-lg leading-none">Customize</h2>
</div>
<button type="button" id="cai-reset-btn" style="color:#ef4444;">
<svg viewBox="0 0 24 24" fill="none" height="1.25em" stroke="currentColor" stroke-linecap="round" stroke-width="2">
<path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6"/>
</svg>
</button>
</div>
<div class="w-full max-w-sm mx-auto">
<div class="overflow-hidden flex flex-col justify-center items-center h-48">
<div class="w-[180px] h-[120px] flex justify-center">
<div class="w-full h-full rounded-[13.5px] relative border border-foreground-300 bg-surface overflow-hidden cai-preview-bg">
<div class="relative flex flex-col justify-center items-center w-full h-full gap-3">
<div class="w-[65%] flex flex-row">
<div class="w-[5.6px] h-[5.6px] rounded-full" style="background: rgb(217, 217, 217);"></div>
<div id="preview-char" class="w-[67.5px] h-[21.5px] mx-1 self-start" style="border-radius: 4.5px; background: var(--surface-elevation-2);"></div>
</div>
<div class="w-[65%] flex flex-row-reverse">
<div class="w-[5.6px] h-[5.6px] rounded-full" style="background: rgb(217, 217, 217);"></div>
<div id="preview-user" class="w-[67.5px] h-[21.5px] mx-1 self-end" style="border-radius: 4.5px; background: var(--surface-elevation-3);"></div>
</div>
</div>
</div>
</div>
<p class="text-sm text-muted-foreground p-3">Preview</p>
</div>
</div>
<div class="w-full flex flex-col items-center px-3">
<div class="w-full px-4 py-2 rounded-spacing-s flex justify-between items-center border-1 border-foreground-200">
<div>
<p class="m-0 font-md-semibold text-foreground">Wallpaper</p>
<p class="m-0 text-muted-foreground text-md">Customize your chat with a uploaded wallpaper of your own.</p>
</div>
<div class="flex flex-col items-center gap-2 ml-2">
<button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 hover:bg-primary/90 px-unit-4 h-unit-10 text-md gap-unit-2 rounded-md [&>&svg]:max-w-[theme(spacing.unit-8)] data-[pressed=true]:scale-[0.97] transition-transform-colors-opacity motion-reduce:transition-none bg-primary text-primary-foreground data-[hover=true]:opacity-hover border-1 border-foreground-200 mb-2 min-w-[100px]" type="button" id="cai-wallpaper-add-btn">${hasWallpaper ? 'Edit' : 'Add'}</button>
</div>
</div>
</div>
<div class="w-full flex flex-col items-center px-3">
<div class="w-full px-4 py-2 rounded-spacing-s flex justify-between items-center border-1 border-foreground-200">
<div>
<p class="m-0 font-md-semibold text-foreground">Audio</p>
<p class="cai-audio-status m-0 text-muted-foreground text-md">${hasAudioWallpaper ? (isAudioPlaying ? 'Now playing' : 'Paused') : 'No audio set'}</p>
</div>
<div class="flex flex-col items-center gap-2 ml-2">
<button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 hover:bg-primary/90 px-unit-4 h-unit-10 text-md gap-unit-2 rounded-md data-[pressed=true]:scale-[0.97] transition-transform-colors-opacity motion-reduce:transition-none bg-primary text-primary-foreground data-[hover=true]:opacity-hover border-1 border-foreground-200 mb-2 min-w-[100px]" type="button" id="cai-audio-add-btn">${hasAudioWallpaper ? 'Edit' : 'Add'}</button>
</div>
</div>
</div>
<div class="w-full flex flex-col items-center px-3">
<div class="w-full px-4 py-2 rounded-spacing-s border-1 border-foreground-200">
<div class="w-full flex items-center justify-between">
<div class="flex flex-col">
<label class="text-sm">Particles</label>
<p class="text-xs text-muted-foreground">Ambient particle effects behind messages.</p>
</div>
<div class="flex items-start shrink-0">
<button type="button" role="switch" aria-checked="${settings.particlesEnabled ? 'true' : 'false'}" id="toggle-particles" class="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${settings.particlesEnabled ? 'bg-blue' : 'bg-muted'}">
<span class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${settings.particlesEnabled ? 'translate-x-5' : 'translate-x-0'}"></span>
</button>
</div>
</div>
<div id="cai-particles-controls" class="w-full flex-col gap-3 pt-2" style="display: ${settings.particlesEnabled ? 'flex' : 'none'};">
<div class="flex flex-col gap-1">
<label class="text-xs text-muted-foreground">Effect</label>
<select id="cai-particle-effect" class="cai-particle-select">
<option value="rain" ${settings.particlesEffect === 'rain' ? 'selected' : ''}>Rain</option>
<option value="snow" ${settings.particlesEffect === 'snow' ? 'selected' : ''}>Snow</option>
<option value="leaves" ${settings.particlesEffect === 'leaves' ? 'selected' : ''}>Leaves</option>
<option value="cherry_blossom" ${settings.particlesEffect === 'cherry_blossom' ? 'selected' : ''}>Cherry Blossom</option>
<option value="bubbles" ${settings.particlesEffect === 'bubbles' ? 'selected' : ''}>Bubbles</option>
<option value="stars" ${settings.particlesEffect === 'stars' ? 'selected' : ''}>Stars</option>
</select>
</div>
<div class="flex justify-between items-center">
<label for="particle-density-slider" class="text-sm">Density</label>
<span class="text-sm" id="particle-density-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.particlesDensity !== undefined ? settings.particlesDensity : 50}%</span>
</div>
<input type="range" min="0" max="100" value="${settings.particlesDensity !== undefined ? settings.particlesDensity : 50}" id="particle-density-slider" class="cai-slider" />
<div class="flex justify-between items-center">
<label for="particle-speed-slider" class="text-sm">Speed</label>
<span class="text-sm" id="particle-speed-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.particlesSpeed !== undefined ? settings.particlesSpeed : 50}%</span>
</div>
<input type="range" min="0" max="100" value="${settings.particlesSpeed !== undefined ? settings.particlesSpeed : 50}" id="particle-speed-slider" class="cai-slider" />
<div class="flex justify-between items-center">
<label for="particle-opacity-slider" class="text-sm">Opacity</label>
<span class="text-sm" id="particle-opacity-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.particlesOpacity !== undefined ? settings.particlesOpacity : 100}%</span>
</div>
<input type="range" min="0" max="100" value="${settings.particlesOpacity !== undefined ? settings.particlesOpacity : 100}" id="particle-opacity-slider" class="cai-slider" />
</div>
</div>
</div>
<div class="w-full px-3 rounded-sm shadow-sm py-2">
<div class="w-full flex flex-col items-center">
<div class="w-full flex justify-between items-center pb-2 mx-auto">
<h4 class="flex items-center">Chat color</h4>
<span class="text-sm flex items-center" id="current-theme-label">${currentTheme}</span>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="flex flex-row flex-wrap w-full gap-5" id="color-theme-picker">
${themes.map(theme => {
const isSelected = theme.name === currentTheme;
return `
<div class="relative w-10 h-10 rounded-full cursor-pointer flex items-center justify-center theme-circle ${isSelected ? 'ring-1 ring-white ring-offset-1' : ''}" data-theme="${theme.name}" style="background: transparent !important; border: none !important;">
<div class="w-9 h-9 rounded-full overflow-hidden" style="background-image: linear-gradient(135deg, ${theme.userColor} 50%, ${theme.charColor} 50%);"></div>
</div>
`;
}).join('')}
</div>
</div>
</div>
</div>
<div class="w-full px-3 rounded-sm shadow-sm py-2">
<div class="w-full flex flex-col items-center">
<div class="w-full flex justify-between items-center pb-2 mx-auto cursor-pointer select-none" id="cai-advanced-toggle">
<h4 class="flex items-center">Custom</h4>
<svg class="cai-chevron transition-transform duration-200" viewBox="0 0 24 24" width="16" height="16" style="transform: rotate(0deg);">
<path d="m9 4 8 8-8 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none"/>
</svg>
</div>
<div id="cai-advanced-content" class="w-full flex flex-col gap-3 overflow-hidden transition-all duration-300 ease-in-out" style="max-height: 0;">
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mt-1">Bubbles</p>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col">
<div class="flex justify-between items-center" style="gap: 16px;">
<label for="opacity-slider" class="text-sm">Opacity</label>
<span class="text-sm" id="opacity-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${currentOpacity}%</span>
</div>
<input type="range" min="10" max="100" value="${currentOpacity}" id="opacity-slider" class="cai-slider" style="margin-top: 6px;" />
</div>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col">
<div class="flex justify-between items-center" style="gap: 16px;">
<label for="bubble-radius-slider" class="text-sm">Roundness</label>
<span class="text-sm" id="bubble-radius-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.bubbleRadius !== undefined ? settings.bubbleRadius : 16}px</span>
</div>
<input type="range" min="0" max="24" value="${settings.bubbleRadius !== undefined ? settings.bubbleRadius : 16}" id="bubble-radius-slider" class="cai-slider" style="margin-top: 6px;" />
</div>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex items-center justify-between">
<div class="flex flex-col">
<label for="toggle-outlines" class="text-sm">Outlines</label>
<p class="text-xs text-muted-foreground">Swap-color outlines on chat bubbles.</p>
</div>
<div class="flex items-start shrink-0">
<button type="button" role="switch" aria-checked="${settings.bubbleOutlines || false}" id="toggle-outlines" class="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${settings.bubbleOutlines ? 'bg-blue' : 'bg-muted'}">
<span class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${settings.bubbleOutlines ? 'translate-x-5' : 'translate-x-0'}"></span>
</button>
</div>
</div>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex flex-col border-input gap-2">
<div class="w-full flex items-center justify-between">
<div class="flex flex-col">
<label class="text-sm">Bubble styles</label>
<p class="text-xs text-muted-foreground">Prebuilt bubble looks. Overrides chat color.</p>
</div>
<div class="flex items-start shrink-0">
<button type="button" role="switch" aria-checked="${settings.bubbleStyle ? 'true' : 'false'}" id="toggle-bubble-styles" class="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${settings.bubbleStyle ? 'bg-blue' : 'bg-muted'}">
<span class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${settings.bubbleStyle ? 'translate-x-5' : 'translate-x-0'}"></span>
</button>
</div>
</div>
<div id="cai-bubble-styles-content" class="w-full flex-col gap-1 pt-1" style="display: ${settings.bubbleStyle ? 'flex' : 'none'};">
${buildBubblePackListHTML(settings.bubbleStyle || null)}
</div>
</div>
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mt-1">Text</p>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col gap-2">
<div class="flex justify-between items-center">
<label for="cai-font-name" class="text-sm">Chat font</label>
<span class="text-xs text-muted-foreground" id="current-font-label">${fontName || 'Default'}</span>
</div>
<input type="text" id="cai-font-name" class="cai-font-input" placeholder="e.g. Roboto, Open Sans, Lora..." value="${fontName}" />
<p class="cai-font-hint">Type any Google Font name. The link loads automatically.</p>
<div class="flex items-center justify-between mt-1">
<span class="text-xs text-muted-foreground">Override for this chat only</span>
<button type="button" role="switch" aria-checked="${isPerChatFont}" id="cai-font-perchat-toggle" class="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${isPerChatFont ? 'bg-blue' : 'bg-muted'}">
<span class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${isPerChatFont ? 'translate-x-5' : 'translate-x-0'}"></span>
</button>
</div>
</div>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col">
<div class="flex justify-between items-center" style="gap: 16px;">
<label for="text-size-slider" class="text-sm">Text size</label>
<span class="text-sm" id="text-size-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.textSize !== undefined ? settings.textSize : 100}%</span>
</div>
<input type="range" min="75" max="150" value="${settings.textSize !== undefined ? settings.textSize : 100}" id="text-size-slider" class="cai-slider" style="margin-top: 6px;" />
</div>
</div>
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mt-1">Layout</p>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col">
<div class="flex justify-between items-center" style="gap: 16px;">
<label for="input-width-slider" class="text-sm">Input box width</label>
<span class="text-sm" id="input-width-value" style="min-width: 44px; text-align: right; flex-shrink: 0;">${settings.inputWidth !== undefined ? settings.inputWidth : 150}%</span>
</div>
<input type="range" min="100" max="200" value="${settings.inputWidth !== undefined ? settings.inputWidth : 150}" id="input-width-slider" class="cai-slider" style="margin-top: 6px;" />
</div>
</div>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex items-center justify-between">
<div class="flex flex-col">
<label for="toggle-immersion" class="text-sm">Full immersion mode</label>
<p class="text-xs text-muted-foreground">Hide both sidebars for full-screen chat. Press Esc to exit.</p>
</div>
<div class="flex items-start shrink-0">
<button type="button" role="switch" aria-checked="${immersionMode}" id="toggle-immersion" class="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${immersionMode ? 'bg-blue' : 'bg-muted'}">
<span class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${immersionMode ? 'translate-x-5' : 'translate-x-0'}"></span>
</button>
</div>
</div>
</div>
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mt-1">Power User</p>
<div class="w-full px-2 py-2 rounded-spacing-s border-1 flex justify-center border-input">
<div class="w-full flex flex-col gap-2">
<label for="custom-css-input" class="text-sm">Custom CSS</label>
<textarea id="custom-css-input" class="cai-custom-css-input" rows="4" placeholder="/* your CSS here */">${settings.customCss || ''}</textarea>
<p class="text-xs text-muted-foreground">Inject your own styles. Be careful.</p>
</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
function closeModal() {
modal.classList.add('closing');
overlay.classList.add('closing');
const onTransitionEnd = () => {
modal.removeEventListener('transitionend', onTransitionEnd);
modal.remove();
overlay.remove();
};
modal.addEventListener('transitionend', onTransitionEnd);
setTimeout(() => {
if (document.body.contains(modal)) {
modal.removeEventListener('transitionend', onTransitionEnd);
modal.remove();
overlay.remove();
}
}, 300);
}
const overlay = document.createElement('div');
overlay.className = 'cai-custom-overlay fixed inset-0 bg-black/50';
overlay.style.zIndex = '51';
overlay.style.pointerEvents = 'auto';
document.body.appendChild(overlay);
modal.querySelector('.close-modal-btn').addEventListener('click', closeModal);
overlay.addEventListener('click', closeModal);
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', escHandler);
}
});
modal.querySelectorAll('.theme-circle').forEach(circle => {
circle.addEventListener('click', function() {
const themeName = this.dataset.theme;
applyTheme(themeName);
const s = getCurrentSettings();
s.theme = themeName;
saveCurrentSettings(s);
modal.querySelectorAll('.theme-circle').forEach(c => {
c.classList.remove('ring-1', 'ring-white', 'ring-offset-1');
});
this.classList.add('ring-1', 'ring-white', 'ring-offset-1');
const label = modal.querySelector('#current-theme-label');
if (label) label.textContent = themeName;
if (s.bubbleStyle) updatePackPreview(s.bubbleStyle);
});
});
const advToggle = modal.querySelector('#cai-advanced-toggle');
const advContent = modal.querySelector('#cai-advanced-content');
const advChevron = modal.querySelector('.cai-chevron');
if (advToggle && advContent) {
advContent.addEventListener('transitionend', function(e) {
if (e.propertyName !== 'max-height') return;
// Once open, drop the fixed cap so nested collapsibles can grow freely.
if (advContent.style.maxHeight !== '0px' && advContent.style.maxHeight !== '0') {
advContent.style.maxHeight = 'none';
}
});
advToggle.addEventListener('click', function() {
const isOpen = advContent.style.maxHeight !== '0px' && advContent.style.maxHeight !== '0';
if (isOpen) {
// Collapse: pin to current height first so the transition has a start value.
advContent.style.maxHeight = advContent.scrollHeight + 'px';
void advContent.offsetHeight;
advContent.style.maxHeight = '0';
} else {
advContent.style.maxHeight = advContent.scrollHeight + 'px';
}
if (advChevron) {
advChevron.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(90deg)';
}
});
}
const slider = modal.querySelector('#opacity-slider');
const opacityLabel = modal.querySelector('#opacity-value');
if (slider) {
slider.addEventListener('input', function() {
const val = parseInt(this.value);
opacityLabel.textContent = val + '%';
applyOpacity(val);
const s = getCurrentSettings();
s.opacity = val;
saveCurrentSettings(s);
});
}
const fontInput = modal.querySelector('#cai-font-name');
const currentFontLabel = modal.querySelector('#current-font-label');
const fontPerchatToggle = modal.querySelector('#cai-font-perchat-toggle');
function saveFontValue(val) {
const d = getStorageData();
const cid = getChatIdFromUrl();
const isPerChat = fontPerchatToggle && fontPerchatToggle.getAttribute('aria-checked') === 'true';
if (cid && isPerChat) {
if (!d.chats[cid]) d.chats[cid] = {};
d.chats[cid].fontName = val;
d.chats[cid].perChatFont = true;
} else {
d.global.fontName = val;
if (cid && d.chats[cid]) {
delete d.chats[cid].fontName;
delete d.chats[cid].perChatFont;
if (!Object.keys(d.chats[cid]).length) delete d.chats[cid];
}
}
if (!saveStorageData(d)) {
console.error('Failed to save font setting');
}
}
if (fontInput) {
fontInput.addEventListener('input', function() {
const val = this.value.trim();
if (currentFontLabel) currentFontLabel.textContent = val || 'Default';
saveFontValue(val);
applyFont({fontName: val});
});
}
if (fontPerchatToggle) {
fontPerchatToggle.addEventListener('click', function() {
const isChecked = this.getAttribute('aria-checked') === 'true';
const newState = !isChecked;
this.setAttribute('aria-checked', newState);
this.classList.toggle('bg-blue', newState);
this.classList.toggle('bg-muted', !newState);
const knob = this.querySelector('span:last-child');
if (knob) {
knob.classList.toggle('translate-x-5', newState);
knob.classList.toggle('translate-x-0', !newState);
}
if (fontInput) {
saveFontValue(fontInput.value.trim());
}
});
}
const immersionToggle = modal.querySelector('#toggle-immersion');
if (immersionToggle) {
immersionToggle.addEventListener('click', function() {
const isChecked = this.getAttribute('aria-checked') === 'true';
const newState = !isChecked;
this.setAttribute('aria-checked', newState);
this.classList.toggle('bg-blue', newState);
this.classList.toggle('bg-muted', !newState);
const thumb = this.querySelector('span');
if (thumb) {
thumb.classList.toggle('translate-x-5', newState);
thumb.classList.toggle('translate-x-0', !newState);
}
saveGlobalSetting('immersionMode', newState);
applyImmersion(getStorageData().global);
if (newState) {
setTimeout(() => {
closeModal();
}, 300);
}
});
}
const inputWidthSlider = modal.querySelector('#input-width-slider');
const inputWidthLabel = modal.querySelector('#input-width-value');
if (inputWidthSlider) {
inputWidthSlider.addEventListener('input', function() {
const val = parseInt(this.value);
inputWidthLabel.textContent = val + '%';
applyInputWidth(val);
const s = getCurrentSettings();
s.inputWidth = val;
saveCurrentSettings(s);
});
}
const outlinesToggle = modal.querySelector('#toggle-outlines');
if (outlinesToggle) {
outlinesToggle.addEventListener('click', function() {
const isChecked = this.getAttribute('aria-checked') === 'true';
const newState = !isChecked;
this.setAttribute('aria-checked', newState);
this.classList.toggle('bg-blue', newState);
this.classList.toggle('bg-muted', !newState);
const thumb = this.querySelector('span');
if (thumb) {
thumb.classList.toggle('translate-x-5', newState);
thumb.classList.toggle('translate-x-0', !newState);
}
const s = getCurrentSettings();
s.bubbleOutlines = newState;
saveCurrentSettings(s);
applyOutlines(newState, s);
updatePreviewOutlines(newState, s);
});
}
const bubbleStylesToggle = modal.querySelector('#toggle-bubble-styles');
const bubbleStylesContent = modal.querySelector('#cai-bubble-styles-content');
// Keep the Advanced panel tall enough to show newly revealed content (no clipping).
function reflowAdvanced() {
if (!advContent) return;
const isOpen = advContent.style.maxHeight !== '0px' && advContent.style.maxHeight !== '0';
if (isOpen) advContent.style.maxHeight = 'none';
}
function setBubbleStylesOpen(open) {
if (!bubbleStylesContent) return;
bubbleStylesContent.style.display = open ? 'flex' : 'none';
reflowAdvanced();
}
if (settings.bubbleStyle) setBubbleStylesOpen(true);
function setBubbleToggleUI(on) {
if (!bubbleStylesToggle) return;
bubbleStylesToggle.setAttribute('aria-checked', on);
bubbleStylesToggle.classList.toggle('bg-blue', on);
bubbleStylesToggle.classList.toggle('bg-muted', !on);
const thumb = bubbleStylesToggle.querySelector('span');
if (thumb) {
thumb.classList.toggle('translate-x-5', on);
thumb.classList.toggle('translate-x-0', !on);
}
}
function markSwatch(id) {
if (!bubbleStylesContent) return;
bubbleStylesContent.querySelectorAll('.cai-pack-swatch').forEach(el => {
const elId = el.dataset.id ? parseInt(el.dataset.id) : null;
const on = elId === (id || null);
el.classList.toggle('ring-1', on);
el.classList.toggle('ring-white', on);
el.classList.toggle('ring-offset-1', on);
});
}
function selectBubblePack(id) {
applyBubbleStyle(id);
const s = getCurrentSettings();
s.bubbleStyle = id || null;
saveCurrentSettings(s);
markSwatch(id);
setBubbleToggleUI(!!id);
}
if (bubbleStylesToggle) {
bubbleStylesToggle.addEventListener('click', function(e) {
e.stopPropagation();
const isChecked = this.getAttribute('aria-checked') === 'true';
const newState = !isChecked;
setBubbleToggleUI(newState);
if (newState) {
setBubbleStylesOpen(true);
const s = getCurrentSettings();
selectBubblePack(s.bubbleStyle || CURATED_PACKS[0].id);
} else {
selectBubblePack(null);
setBubbleStylesOpen(false);
}
});
}
if (bubbleStylesContent) {
bubbleStylesContent.querySelectorAll('.cai-pack-swatch').forEach(item => {
item.addEventListener('click', function() {
const id = this.dataset.id ? parseInt(this.dataset.id) : null;
selectBubblePack(id);
});
});
}
const textSizeSlider = modal.querySelector('#text-size-slider');
const textSizeLabel = modal.querySelector('#text-size-value');
if (textSizeSlider) {
textSizeSlider.addEventListener('input', function() {
const val = parseInt(this.value);
textSizeLabel.textContent = val + '%';
applyTextSize(val);
const s = getCurrentSettings();
s.textSize = val;
saveCurrentSettings(s);
});
}
const bubbleRadiusSlider = modal.querySelector('#bubble-radius-slider');
const bubbleRadiusLabel = modal.querySelector('#bubble-radius-value');
if (bubbleRadiusSlider) {
bubbleRadiusSlider.addEventListener('input', function() {
const val = parseInt(this.value);
bubbleRadiusLabel.textContent = val + 'px';
applyBubbleRadius(val);
const s = getCurrentSettings();
s.bubbleRadius = val;
saveCurrentSettings(s);
});
}
const customCssInput = modal.querySelector('#custom-css-input');
if (customCssInput) {
let cssTimeout;
customCssInput.addEventListener('input', function() {
clearTimeout(cssTimeout);
cssTimeout = setTimeout(() => {
const val = this.value;
applyCustomCss(val);
const s = getCurrentSettings();
s.customCss = val;
saveCurrentSettings(s);
}, 500);
});
}
const particlesToggle = modal.querySelector('#toggle-particles');
const particlesControls = modal.querySelector('#cai-particles-controls');
if (particlesToggle) {
particlesToggle.addEventListener('click', function() {
const on = this.getAttribute('aria-checked') !== 'true';
this.setAttribute('aria-checked', on ? 'true' : 'false');
this.classList.toggle('bg-blue', on);
this.classList.toggle('bg-muted', !on);
const thumb = this.querySelector('span');
if (thumb) { thumb.classList.toggle('translate-x-5', on); thumb.classList.toggle('translate-x-0', !on); }
if (particlesControls) particlesControls.style.display = on ? 'flex' : 'none';
const s = getCurrentSettings();
s.particlesEnabled = on;
saveCurrentSettings(s);
applyParticles(s);
});
}
const particleEffect = modal.querySelector('#cai-particle-effect');
if (particleEffect) {
particleEffect.addEventListener('change', function() {
const s = getCurrentSettings();
s.particlesEffect = this.value;
saveCurrentSettings(s);
applyParticles(s);
});
}
function makeParticleSlider(slider, label, key) {
if (!slider) return;
let _timer;
slider.addEventListener('input', function() {
const val = parseInt(this.value);
if (label) label.textContent = val + '%';
if (_particleEngine) _particleEngine.setConfig({ [key]: val });
clearTimeout(_timer);
_timer = setTimeout(() => {
const s = getCurrentSettings();
s['particles' + key.charAt(0).toUpperCase() + key.slice(1)] = val;
saveCurrentSettings(s);
}, 200);
});
}
makeParticleSlider(modal.querySelector('#particle-density-slider'), modal.querySelector('#particle-density-value'), 'density');
makeParticleSlider(modal.querySelector('#particle-speed-slider'), modal.querySelector('#particle-speed-value'), 'speed');
makeParticleSlider(modal.querySelector('#particle-opacity-slider'), modal.querySelector('#particle-opacity-value'), 'opacity');
const addBtn = modal.querySelector('#cai-wallpaper-add-btn');
if (addBtn) {
addBtn.addEventListener('click', function(e) {
e.stopPropagation();
openWallpaperDialog();
});
}
const audioBtn = modal.querySelector('#cai-audio-add-btn');
if (audioBtn) {
audioBtn.addEventListener('click', function(e) {
e.stopPropagation();
openAudioWallpaperDialog();
});
}
const resetBtn = modal.querySelector('#cai-reset-btn');
if (resetBtn) {
resetBtn.addEventListener('click', function() {
showResetConfirmModal(closeModal);
});
}
updatePreview(currentTheme);
updatePreviewBackground(settings);
if (settings.bubbleStyle) updatePackPreview(settings.bubbleStyle);
}
function showResetConfirmModal(onReset) {
document.querySelector('.cai-reset-confirm-modal')?.remove();
document.querySelector('.cai-reset-confirm-overlay')?.remove();
const overlay = document.createElement('div');
overlay.className = 'cai-reset-confirm-overlay fixed inset-0 bg-black/50';
overlay.style.zIndex = '53';
overlay.style.transition = 'opacity 0.2s ease';
overlay.style.willChange = 'opacity';
document.body.appendChild(overlay);
const modal = document.createElement('div');
modal.className = 'cai-reset-confirm-modal fixed p-6 shadow-lg border-none left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] sm:rounded-spacing-l max-w-sm w-11/12 flex flex-col gap-4 bg-surface-elevation-1 text-foreground';
modal.style.zIndex = '54';
modal.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
modal.style.willChange = 'opacity, transform';
modal.innerHTML = `
<h2 class="text-base sm:text-lg font-semibold">Reset settings</h2>
<p class="text-sm text-muted-foreground">Are you sure you want to reset all settings to default? This cannot be undone.</p>
<div class="flex justify-end gap-2 mt-2">
<button class="cancel-btn px-4 py-2 text-sm rounded-md border border-input text-muted-foreground hover:text-foreground hover:border-foreground-300 transition-colors">Cancel</button>
<button class="confirm-btn px-4 py-2 text-sm rounded-md" disabled style="background:#dc2626;color:#fff;border:1px solid #b91c1c;opacity:0.5;cursor:not-allowed;">Confirm (5s)</button>
</div>
`;
document.body.appendChild(modal);
function animateClose(callback) {
clearInterval(ci);
modal.style.opacity = '0';
modal.style.transform = 'translate(-50%, -50%) scale(0.95)';
overlay.style.opacity = '0';
const onEnd = () => {
modal.removeEventListener('transitionend', onEnd);
modal.remove();
overlay.remove();
if (callback) callback();
};
modal.addEventListener('transitionend', onEnd);
setTimeout(() => {
if (document.body.contains(modal)) {
modal.removeEventListener('transitionend', onEnd);
modal.remove();
overlay.remove();
if (callback) callback();
}
}, 300);
}
modal.querySelector('.cancel-btn').addEventListener('click', () => animateClose());
overlay.addEventListener('click', () => animateClose());
const confirmBtn = modal.querySelector('.confirm-btn');
let countdown = 5;
confirmBtn.textContent = 'Confirm (5s)';
const ci = setInterval(() => {
countdown--;
if (countdown > 0) {
confirmBtn.textContent = `Confirm (${countdown}s)`;
} else {
clearInterval(ci);
confirmBtn.disabled = false;
confirmBtn.style.opacity = '1';
confirmBtn.style.cursor = 'pointer';
confirmBtn.textContent = 'Confirm';
}
}, 1000);
confirmBtn.addEventListener('click', function() {
if (this.disabled) return;
localStorage.removeItem(STORAGE_KEY);
animateClose(() => {
onReset();
setTimeout(() => location.reload(), 300);
});
});
}
// Debounced init to avoid multiple rapid calls
let initTimer = null;
function initChat() {
clearTimeout(initTimer);
initTimer = setTimeout(() => {
if (!getChatIdFromUrl()) return;
removeOriginalCustomize();
injectCustomButton();
loadSavedSettings();
}, 300);
}
injectAdaptiveStyles();
// Watch for #chat-messages being added/replaced (React swaps it)
let _chatMsgTimer = null;
const chatMsgObserver = new MutationObserver(() => {
if (document.querySelector('#chat-messages')) {
clearTimeout(_chatMsgTimer);
_chatMsgTimer = setTimeout(() => {
const settings = getCurrentSettings();
applyTextSize(settings.textSize !== undefined ? settings.textSize : 100);
applyBubbleRadius(settings.bubbleRadius !== undefined ? settings.bubbleRadius : 16);
applyFont({fontName: getFontSetting()});
}, 300);
}
});
chatMsgObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
// Initial load
setTimeout(initChat, 1000);
// SPA navigation handling
if (window.onurlchange === null) {
window.addEventListener('urlchange', () => initChat());
} else {
let lastUrl = window.location.href;
const observer = new MutationObserver(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
initChat();
}
});
observer.observe(document, { subtree: true, childList: true });
}
})();