JanitorAI Character Card Scraper

Extract character card with "T" key (WHILE IN CHAT PAGE) and save as .txt, .png, or .json (proxy required)

Fra og med 23.06.2025. Se den nyeste version.

// ==UserScript==
// @name         JanitorAI Character Card Scraper
// @version      2.2
// @description  Extract character card with "T" key (WHILE IN CHAT PAGE) and save as .txt, .png, or .json (proxy required)
// @match        https://janitorai.com/*
// @icon         https://images.dwncdn.net/images/t_app-icon-l/p/46413ec0-e1d8-4eab-a0bc-67eadabb2604/3920235030/janitor-ai-logo
// @grant        none
// @namespace    https://sleazyfork.org/en/scripts/537206-janitorai-character-card-scraper
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(() => {
    'use strict';

    /* ============================
       ==      VARIABLES        ==
       ============================ */
    let hasInitialized = false
    let viewActive = false
    let shouldInterceptNext = false
    let networkInterceptActive = false
    let exportFormat = null
    let chatData = null
    let currentTab = sessionStorage.getItem('lastActiveTab') || 'export'
    let useChatNameForName = localStorage.getItem('useChatNameForName') === 'true' || false;
    let applyCharToken = localStorage.getItem('applyCharToken') !== 'false';
    let animationTimeouts = [];
    let guiElement = null;
    sessionStorage.removeItem('char_export_scroll');
    sessionStorage.removeItem('char_settings_scroll');
    const ANIMATION_DURATION = 150; // Animation duration for modal open/close in ms
    const TAB_ANIMATION_DURATION = 300; // Animation duration for tab switching in ms
    const TAB_BUTTON_DURATION = 250; // Animation duration for tab button effects
    const BUTTON_ANIMATION = 200; // Animation duration for format buttons
    const TOGGLE_ANIMATION = 350; // Animation duration for toggle switch
    const ACTIVE_TAB_COLOR = '#0080ff'; // Color for active tab indicator
    const INACTIVE_TAB_COLOR = 'transparent'; // Color for inactive tab indicator
    const BUTTON_COLOR = '#3a3a3a'; // Base color for buttons
    const BUTTON_HOVER_COLOR = '#4a4a4a'; // Hover color for buttons
    const BUTTON_ACTIVE_COLOR = '#0070dd'; // Active color for buttons when clicked
    const TOOLTIP_SLIDE_FROM_RIGHT = true; // true = slide towards right (default). Set false for slide-left variant.
    const TOOLTIP_SLIDE_OFFSET = 10; // px the tooltip travels during slide animation
    const characterMetaCache = {
        id: null,
        creatorUrl: '',
        characterVersion: '',
        characterCardUrl: '',
        name: '',
        creatorNotes: ''
    };

    /* ============================
       ==      UTILITIES        ==
       ============================ */
    function makeElement(tag, attrs = {}, styles = {}) {
        const el = document.createElement(tag);
        Object.entries(attrs).forEach(([key, value]) => el[key] = value);

        if (styles) {
            Object.entries(styles).forEach(([key, value]) => el.style[key] = value);
        }
        return el;
    }

    function saveFile(filename, blob) {
        const url = URL.createObjectURL(blob);
        const a = makeElement('a', {
            href: url,
            download: filename
        });
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
    }

    function escapeRegExp(s) {
        return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    function extractTagContent(sys, charName) {
        if (!charName || !sys) return '';
        const variants = [];
        const trimmed = charName.trim();
        variants.push(trimmed);
        const collapsed = trimmed.replace(/\s+/g, ' ');
        if (collapsed !== trimmed) variants.push(collapsed);
        if (trimmed.includes(' ')) variants.push(trimmed.replace(/\s+/g, '_'));

        for (const name of variants) {
            const escName = escapeRegExp(name);
            const regex = new RegExp(`<${escName}(?:\\s[^>]*)?\\s*>([\\s\\S]*?)<\\/${escName}\\s*>`, 'i');
            const m = sys.match(regex);
            if (m && m[1] != null) {
                return m[1].trim();
            }
            const openTagRx = new RegExp(`<${escName}(?:\\s[^>]*)?\\s*>`, 'i');
            const closeTagRx = new RegExp(`<\\/${escName}\\s*>`, 'i');
            const openMatch = openTagRx.exec(sys);
            const closeMatch = closeTagRx.exec(sys);
            if (openMatch && closeMatch && closeMatch.index > openMatch.index) {
                const start = openMatch.index + openMatch[0].length;
                const end = closeMatch.index;
                return sys.substring(start, end).trim();
            }
            try {
                const parser = new DOMParser();
                const doc = parser.parseFromString(`<root>${sys}</root>`, 'application/xml');
                const elems = doc.getElementsByTagName(name);
                if (elems.length) {
                    return elems[0].textContent.trim();
                }
            } catch (_) {}
        }
        return '';
    }

    /* ============================
       ==          UI           ==
       ============================ */
    function createUI() {
        if (guiElement && document.body.contains(guiElement)) {
            return;
        }

        animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
        animationTimeouts = [];

        viewActive = true;

        const gui = makeElement('div', {
            id: 'char-export-gui'
        }, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%) scale(0.95)',
            background: '#222',
            color: 'white',
            padding: '15px 20px 7px',
            borderRadius: '8px',
            boxShadow: '0 0 20px rgba(0,0,0,0.5)',
            zIndex: '10000',
            textAlign: 'center',
            width: '320px',
            overflow: 'hidden',
            display: 'flex',
            flexDirection: 'column',
            boxSizing: 'border-box',
            opacity: '0',
            transition: `opacity ${ANIMATION_DURATION}ms ease-out, transform ${ANIMATION_DURATION}ms ease-out`
        });

        guiElement = gui;
        const unselectStyle = document.createElement('style');
        unselectStyle.textContent = `#char-export-gui, #char-export-gui * {
  user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;
}
#char-export-gui {
  backdrop-filter: blur(6px);
  background: rgba(34,34,34,0.92);
  border: none;
  border-radius: 8px;
}
#char-export-overlay {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.5); z-index: 9998;
}
#char-export-gui #char-export-tooltip {
  font-size: 12px; background: #222; color: #fff;
  padding: 6px 10px; border-radius: 4px; pointer-events: none;
}
#char-export-gui button {
  background: #333; color: #fff; border: none;
  border-radius: 4px; padding: 8px 12px;
  transition: background 200ms ease, transform 200ms ease;
}
#char-export-gui button:hover:not(:disabled) {
  background: #444; transform: translateY(-1px);
}
#char-export-gui button:disabled {
  opacity: 0.6; cursor: not-allowed;
}
#char-export-gui button:focus {
  outline: none;
}`;
        document.head.appendChild(unselectStyle);

        const tabContainer = makeElement('div', {}, {
            display: 'flex',
            justifyContent: 'center',
            marginBottom: '15px',
            borderBottom: '1px solid #444',
            paddingBottom: '8px',
            width: '100%'
        });

        const tabsWrapper = makeElement('div', {}, {
            display: 'flex',
            justifyContent: 'center',
            width: '100%',
            maxWidth: '300px',
            margin: '0 auto'
        });

        const createTabButton = (text, isActive) => {
            const button = makeElement('button', {
                textContent: text
            }, {
                background: 'transparent',
                border: 'none',
                color: '#fff',
                padding: '8px 20px',
                cursor: 'pointer',
                margin: '0 5px',
                fontWeight: 'bold',
                flex: '1',
                textAlign: 'center',
                position: 'relative',
                overflow: 'hidden',
                transition: `opacity ${TAB_BUTTON_DURATION}ms ease, transform ${TAB_BUTTON_DURATION}ms ease, color ${TAB_BUTTON_DURATION}ms ease`
            });

            const indicator = makeElement('div', {}, {
                position: 'absolute',
                bottom: '0',
                left: '0',
                width: '100%',
                height: '2px',
                background: isActive ? ACTIVE_TAB_COLOR : INACTIVE_TAB_COLOR,
                transition: `transform ${TAB_BUTTON_DURATION}ms ease, background-color ${TAB_BUTTON_DURATION}ms ease`
            });

            if (!isActive) {
                button.style.opacity = '0.7';
                indicator.style.transform = 'scaleX(0.5)';
            }

            button.appendChild(indicator);
            return {
                button,
                indicator
            };
        };

        const {
            button: exportTab,
            indicator: exportIndicator
        } = createTabButton('Export', true);
        const {
            button: settingsTab,
            indicator: settingsIndicator
        } = createTabButton('Settings', false);

        exportTab.onmouseover = () => {
            if (currentTab !== 'export') {
                exportTab.style.opacity = '1';
                exportTab.style.transform = 'translateY(-2px)';
                exportIndicator.style.transform = 'scaleX(0.8)';
            }
        };
        exportTab.onmouseout = () => {
            if (currentTab !== 'export') {
                exportTab.style.opacity = '0.7';
                exportTab.style.transform = '';
                exportIndicator.style.transform = 'scaleX(0.5)';
            }
        };

        settingsTab.onmouseover = () => {
            if (currentTab !== 'settings') {
                settingsTab.style.opacity = '1';
                settingsTab.style.transform = 'translateY(-2px)';
                settingsIndicator.style.transform = 'scaleX(0.8)';
            }
        };
        settingsTab.onmouseout = () => {
            if (currentTab !== 'settings') {
                settingsTab.style.opacity = '0.7';
                settingsTab.style.transform = '';
                settingsIndicator.style.transform = 'scaleX(0.5)';
            }
        };

        tabsWrapper.appendChild(exportTab);
        tabsWrapper.appendChild(settingsTab);
        tabContainer.appendChild(tabsWrapper);
        gui.appendChild(tabContainer);
        /* ========= Dynamic Tooltip ========= */
        (() => {
            let tEl;
            const show = (target) => {
                const msg = target.getAttribute('data-tooltip');
                if (!msg) return;
                if (!tEl) {
                    tEl = document.createElement('div');
                    tEl.id = 'char-export-tooltip';
                    tEl.id = 'char-export-tooltip';
                    tEl.style.cssText = 'position:fixed;padding:6px 10px;font-size:12px;background:#222;color:#fff;border-radius:4px;white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,0.4);opacity:0;transition:opacity 200ms ease,transform 200ms ease;z-index:10002;';
                    const offset = TOOLTIP_SLIDE_FROM_RIGHT ? -TOOLTIP_SLIDE_OFFSET : TOOLTIP_SLIDE_OFFSET;
                    tEl.style.transform = `translateX(${offset}px)`;
                    document.body.appendChild(tEl);
                }
                tEl.textContent = msg;
                const guiRect = gui.getBoundingClientRect();
                const tgtRect = target.getBoundingClientRect();
                tEl.style.top = (tgtRect.top + tgtRect.height / 2 - tEl.offsetHeight / 2) + 'px';
                tEl.style.left = (guiRect.right + 10) + 'px';
                requestAnimationFrame(() => {
                    tEl.style.opacity = '1';
                    tEl.style.transform = 'translateX(0)';
                });
            };
            const hide = () => {
                if (!tEl) return;
                tEl.style.opacity = '0';
                const offsetHide = TOOLTIP_SLIDE_FROM_RIGHT ? -TOOLTIP_SLIDE_OFFSET : TOOLTIP_SLIDE_OFFSET;
                tEl.style.transform = `translateX(${offsetHide}px)`;
            };
            gui.addEventListener('mouseover', e => {
                const tgt = e.target.closest('[data-tooltip]');
                if (tgt) show(tgt);
            });
            gui.addEventListener('mouseout', e => {
                const tgt = e.target.closest('[data-tooltip]');
                if (tgt) hide();
            });
            window.addEventListener('keydown', ev => {
                hide();
                if ((ev.key === 't' || ev.key === 'T') && tEl) {
                    tEl.style.opacity = '0';
                    const offsetHide = TOOLTIP_SLIDE_FROM_RIGHT ? -TOOLTIP_SLIDE_OFFSET : TOOLTIP_SLIDE_OFFSET;
                    tEl.style.transform = `translateX(${offsetHide}px)`;
                }
            });
        })();

        const exportContent = makeElement('div', {
            id: 'export-tab'
        }, {
            maxHeight: '60vh',
            overflowY: 'auto',
            padding: '0 5px 10px 0',
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center'
        });

        const title = makeElement('h2', {
            textContent: 'Export Character Card'
        }, {
            margin: '0 0 12px 0',
            fontSize: '18px',
            paddingTop: '5px'
        });
        exportContent.appendChild(title);

        const buttonContainer = makeElement('div', {}, {
            display: 'flex',
            gap: '10px',
            justifyContent: 'center',
            marginBottom: '3px',
            marginTop: '8px'
        });

        ['TXT', 'PNG', 'JSON'].forEach(format => {
            const type = format.toLowerCase();

            const button = makeElement('button', {
                textContent: format
            }, {
                background: BUTTON_COLOR,
                border: 'none',
                color: 'white',
                padding: '10px 20px',
                borderRadius: '6px',
                cursor: 'pointer',
                fontWeight: 'bold',
                position: 'relative',
                overflow: 'hidden',
                flex: '1',
                transition: `all ${BUTTON_ANIMATION}ms ease`,
                boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
                transform: 'translateY(0)'
            });

            const shine = makeElement('div', {}, {
                position: 'absolute',
                top: '0',
                left: '0',
                width: '100%',
                height: '100%',
                background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 60%)',
                transform: 'translateX(-100%)',
                transition: `transform ${BUTTON_ANIMATION * 1.5}ms ease-out`,
                pointerEvents: 'none'
            });
            button.appendChild(shine);

            button.onmouseover = () => {
                button.style.background = BUTTON_HOVER_COLOR;
                button.style.transform = 'translateY(-2px)';
                button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
                shine.style.transform = 'translateX(100%)';
            };

            button.onmouseout = () => {
                button.style.background = BUTTON_COLOR;
                button.style.transform = 'translateY(0)';
                button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
                shine.style.transform = 'translateX(-100%)';
            };

            button.onmousedown = () => {
                button.style.transform = 'translateY(1px)';
                button.style.boxShadow = '0 1px 2px rgba(0,0,0,0.2)';
                button.style.background = BUTTON_ACTIVE_COLOR;
            };

            button.onmouseup = () => {
                button.style.transform = 'translateY(-2px)';
                button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
                button.style.background = BUTTON_HOVER_COLOR;
            };

            button.onclick = (e) => {
                const rect = button.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;

                const ripple = makeElement('div', {}, {
                    position: 'absolute',
                    borderRadius: '50%',
                    backgroundColor: 'rgba(255,255,255,0.4)',
                    width: '5px',
                    height: '5px',
                    transform: 'scale(1)',
                    opacity: '1',
                    animation: 'ripple 600ms linear',
                    pointerEvents: 'none',
                    top: `${y}px`,
                    left: `${x}px`,
                    marginLeft: '-2.5px',
                    marginTop: '-2.5px'
                });

                button.appendChild(ripple);

                exportFormat = type;
                closeV();
                extraction();

                setTimeout(() => ripple.remove(), 600);
            };

            buttonContainer.appendChild(button);
        });


        if (!document.getElementById('char-export-style')) {
            const style = document.createElement('style');
            style.id = 'char-export-style';
            style.textContent = `
          @keyframes ripple {
            to {
              transform: scale(30);
              opacity: 0; pointer-events: none;
            }
          }
        `;
            document.head.appendChild(style);
        }
        if (!document.getElementById('char-export-tooltip-style')) {
            const tooltipStyle = document.createElement('style');
            tooltipStyle.id = 'char-export-tooltip-style';
            tooltipStyle.textContent = `
[data-tooltip] {
  position: relative;
}
[data-tooltip]::before {
  content: '';
  position: absolute;
  left: calc(100% + 4px);
  top: 50%;
  transform: translateY(-50%) scaleY(0.5);
  border: solid transparent;
  border-width: 6px 6px 6px 0;
  border-left-color: #333;
  opacity: 0; pointer-events: none;
  transition: opacity 150ms ease;
  z-index: 10001;
}
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  left: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%) scale(0.8);
  background: #333;
  color: #fff;
  padding: 6px 10px;
  border-radius: 4px;
  white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity 150ms ease, transform 150ms ease;
  z-index: 10001;
}
[data-tooltip]:hover::before,
[data-tooltip]:hover::after {
  opacity: 1;
  transform: translateY(-50%) scale(1);
}

.toggle-wrapper {
  border-radius: 8px;
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.toggle-wrapper.active {
  background: transparent;
  box-shadow: none;
}
            `;
            document.head.appendChild(tooltipStyle);
            if (!document.getElementById('char-export-scrollbar-style')) {
                const scrollStyle = document.createElement('style');
                scrollStyle.id = 'char-export-scrollbar-style';
                scrollStyle.textContent = `
#content-wrapper { overflow: visible; }
#settings-tab { overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; }
#settings-tab::-webkit-scrollbar { width: 0; height: 0; }







            `;
                document.head.appendChild(scrollStyle);
                if (!document.getElementById('char-export-tooltip-override-style')) {
                    const overrideStyle = document.createElement('style');
                    overrideStyle.id = 'char-export-tooltip-override-style';
                    overrideStyle.textContent = `
[data-tooltip]::before { transform: translateY(-50%) translateX(-6px); transition: transform 200ms ease, opacity 200ms ease; }
[data-tooltip]::after { transform: translateY(-50%) translateX(-10px); transition: transform 200ms ease, opacity 200ms ease; }
[data-tooltip]:hover::before { transform: translateY(-50%) translateX(0); opacity:1; }
[data-tooltip]:hover::after { transform: translateY(-50%) translateX(0); opacity:1; }

[data-tooltip]::before,[data-tooltip]::after{display:none !important;}
@keyframes toggle-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } }
.toggle-wrapper.pulse { animation: toggle-pulse 300ms ease; }

.switch .slider-before { transition: transform 200ms ease, box-shadow 200ms ease; }
.toggle-wrapper.active .slider-before { box-shadow: 0 0 2px rgba(0,0,0,0.2), 0 0 8px rgba(0,128,255,0.5); }
.toggle-wrapper.active .slider { background-color: #0080ff; }
            `;
                    document.head.appendChild(overrideStyle);
                }
            }
        }

        exportContent.appendChild(buttonContainer);

        const contentWrapper = makeElement('div', {
            id: 'content-wrapper'
        }, {
            height: '103px',
            width: '100%',
            overflow: 'visible',
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            position: 'relative'
        });
        gui.appendChild(contentWrapper);

        const tabContentStyles = {
            height: '100%',
            width: '100%',
            overflowY: 'auto',
            overflowX: 'hidden',
            padding: '0',


            position: 'absolute',
            top: '0',
            left: '0',
            opacity: '1',
            transform: 'scale(1)',
            transition: `opacity ${TAB_ANIMATION_DURATION}ms ease, transform ${TAB_ANIMATION_DURATION}ms ease`,
        };

        Object.assign(exportContent.style, tabContentStyles);
        exportContent.style.overflowY = 'hidden';
        exportContent.style.overflowX = 'hidden';



        const settingsContent = makeElement('div', {
            id: 'settings-tab',
            style: 'display: none;'
        }, tabContentStyles);

        contentWrapper.appendChild(exportContent);
        contentWrapper.appendChild(settingsContent);
        const savedExportScroll = parseInt(sessionStorage.getItem('char_export_scroll') || '0', 10);
        exportContent.scrollTop = savedExportScroll;
        const savedSettingsScroll = parseInt(sessionStorage.getItem('char_settings_scroll') || '0', 10);
        settingsContent.scrollTop = savedSettingsScroll;
        settingsContent.addEventListener('scroll', () => sessionStorage.setItem('char_settings_scroll', settingsContent.scrollTop));


        requestAnimationFrame(() => {
            exportContent.scrollTop = savedExportScroll;
            settingsContent.scrollTop = savedSettingsScroll;
        });

        const settingsTitle = makeElement('h2', {
            textContent: 'Export Settings'
        }, {
            margin: '0 0 15px 0',
            fontSize: '18px',
            paddingTop: '5px'
        });
        settingsContent.appendChild(settingsTitle);

        const toggleContainer = makeElement('div', {}, {
            display: 'flex',
            alignItems: 'center',
            marginBottom: '4px',
            marginTop: '5px',
            padding: '10px 10px 9px',
            background: '#2a2a2a',
            borderRadius: '8px',
            gap: '10px'
        });

        const toggleWrapper = makeElement('div', {
            className: 'toggle-wrapper'
        }, {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            width: '100%',
            cursor: 'pointer'
        });

        const toggleLabel = makeElement('span', {
            textContent: 'Use character\'s chat name',

        }, {
            fontSize: '13px',
            color: '#fff',
            order: '2',
            textAlign: 'left',
            flex: '1',
            paddingLeft: '10px',
            wordBreak: 'break-word',
            lineHeight: '1.4',




        });

        const toggle = makeElement('label', {
            className: 'switch'
        }, {
            position: 'relative',
            display: 'inline-block',
            width: '40px',
            height: '24px',
            order: '1',
            margin: '0',
            flexShrink: '0',
            borderRadius: '24px',
            boxShadow: '0 1px 3px rgba(0,0,0,0.2) inset',
            transition: `all ${TOGGLE_ANIMATION}ms ease`
        });

        const slider = makeElement('span', {
            className: 'slider round'
        }, {
            position: 'absolute',
            cursor: 'pointer',
            top: '0',
            left: '0',
            right: '0',
            bottom: '0',
            backgroundColor: useChatNameForName ? ACTIVE_TAB_COLOR : '#ccc',
            transition: `background-color ${TOGGLE_ANIMATION}ms cubic-bezier(0.34, 1.56, 0.64, 1)`,
            borderRadius: '24px',
            overflow: 'hidden'
        });

        const sliderShine = makeElement('div', {}, {
            position: 'absolute',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            background: 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%)',
            opacity: '0.5',
            transition: `opacity ${TOGGLE_ANIMATION}ms ease`
        });
        slider.appendChild(sliderShine);

        const sliderBefore = makeElement('span', {
            className: 'slider-before'
        }, {
            position: 'absolute',
            content: '""',
            height: '16px',
            width: '16px',
            left: '4px',
            bottom: '4px',
            backgroundColor: 'white',
            transition: `transform ${TOGGLE_ANIMATION}ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow ${TOGGLE_ANIMATION}ms ease`,
            borderRadius: '50%',
            transform: useChatNameForName ? 'translateX(16px)' : 'translateX(0)',
            boxShadow: useChatNameForName ?
                '0 0 2px rgba(0,0,0,0.2), 0 0 5px rgba(0,128,255,0.3)' : '0 0 2px rgba(0,0,0,0.2)'
        });

        const input = makeElement('input', {
            type: 'checkbox',
            checked: useChatNameForName
        }, {
            opacity: '0',
            width: '0',
            height: '0',
            position: 'absolute'
        });

        input.addEventListener('change', (e) => {
            useChatNameForName = e.target.checked;
            localStorage.setItem('useChatNameForName', useChatNameForName);

            slider.style.backgroundColor = useChatNameForName ? ACTIVE_TAB_COLOR : '#ccc';
            sliderBefore.style.transform = useChatNameForName ? 'translateX(16px)' : 'translateX(0)';

            sliderBefore.style.boxShadow = useChatNameForName ?
                '0 0 2px rgba(0,0,0,0.2), 0 0 5px rgba(0,128,255,0.3)' :
                '0 0 2px rgba(0,0,0,0.2)';
            toggleWrapper.classList.toggle('active', useChatNameForName);
            toggleWrapper.classList.add('pulse');
            setTimeout(() => toggleWrapper.classList.remove('pulse'), 300);

            if (useChatNameForName) {
                const pulse = makeElement('div', {}, {
                    position: 'absolute',
                    top: '0',
                    left: '0',
                    right: '0',
                    bottom: '0',
                    backgroundColor: ACTIVE_TAB_COLOR,
                    borderRadius: '24px',
                    opacity: '0.5',
                    transform: 'scale(1.2)',
                    pointerEvents: 'none',
                    zIndex: '-1'
                });

                toggle.appendChild(pulse);

                setTimeout(() => {
                    pulse.style.opacity = '0';
                    pulse.style.transform = 'scale(1.5)';
                    pulse.style.transition = 'all 400ms ease-out';
                }, 10);

                setTimeout(() => pulse.remove(), 400);
            }
        });

        slider.style.backgroundColor = useChatNameForName ? '#007bff' : '#ccc';
        slider.appendChild(sliderBefore);
        toggle.appendChild(input);
        toggle.appendChild(slider);

        toggleWrapper.addEventListener('click', (e) => {
            e.preventDefault();
            input.checked = !input.checked;
            const event = new Event('change');
            input.dispatchEvent(event);
            document.body.focus();
        });

        toggleWrapper.appendChild(toggleLabel);
        toggleLabel.setAttribute('data-tooltip', 'Uses chat name for the character name instead of label name.');
        toggleWrapper.appendChild(toggle);
        toggleContainer.appendChild(toggleWrapper);
        settingsContent.appendChild(toggleContainer);
        const toggleContainerChar = makeElement('div', {}, {
            display: 'flex',
            alignItems: 'center',
            marginBottom: '4px',
            marginTop: '5px',
            padding: '10px 10px 9px',
            background: '#2a2a2a',
            borderRadius: '8px',
            gap: '10px'
        });
        const toggleWrapperChar = makeElement('div', {
            className: 'toggle-wrapper'
        }, {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            width: '100%',
            cursor: 'pointer'
        });
        const toggleLabelChar = makeElement('span', {
            textContent: 'Apply {{char}} tokenization',

        }, {
            fontSize: '13px',
            color: '#fff',
            order: '2',
            textAlign: 'left',
            flex: '1',
            paddingLeft: '10px',
            wordBreak: 'break-word',
            lineHeight: '1.4',




        });
        const toggleChar = makeElement('label', {
            className: 'switch'
        }, {
            position: 'relative',
            display: 'inline-block',
            width: '40px',
            height: '24px',
            order: '1',
            margin: '0',
            flexShrink: '0',
            borderRadius: '24px',
            boxShadow: '0 1px 3px rgba(0,0,0,0.2) inset',
            transition: `all ${TOGGLE_ANIMATION}ms ease`
        });
        const sliderChar = makeElement('span', {
            className: 'slider round'
        }, {
            position: 'absolute',
            cursor: 'pointer',
            top: '0',
            left: '0',
            right: '0',
            bottom: '0',
            backgroundColor: applyCharToken ? ACTIVE_TAB_COLOR : '#ccc',
            transition: `background-color ${TOGGLE_ANIMATION}ms cubic-bezier(0.34, 1.56, 0.64, 1)`,
            borderRadius: '24px',
            overflow: 'hidden'
        });
        const sliderShineChar = makeElement('div', {}, {
            position: 'absolute',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            background: 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%)',
            opacity: '0.5',
            transition: `opacity ${TOGGLE_ANIMATION}ms ease`
        });
        sliderChar.appendChild(sliderShineChar);
        const sliderBeforeChar = makeElement('span', {
            className: 'slider-before'
        }, {
            position: 'absolute',
            content: '""',
            height: '16px',
            width: '16px',
            left: '4px',
            bottom: '4px',
            backgroundColor: 'white',
            transition: `transform ${TOGGLE_ANIMATION}ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow ${TOGGLE_ANIMATION}ms ease`,
            borderRadius: '50%',
            transform: applyCharToken ? 'translateX(16px)' : 'translateX(0)',
            boxShadow: applyCharToken ? '0 0 2px rgba(0,0,0,0.2), 0 0 5px rgba(0,128,255,0.3)' : '0 0 2px rgba(0,0,0,0.2)'
        });
        const inputChar = makeElement('input', {
            type: 'checkbox',
            checked: applyCharToken
        }, {
            opacity: '0',
            width: '0',
            height: '0',
            position: 'absolute'
        });
        inputChar.addEventListener('change', (e) => {
            applyCharToken = e.target.checked;
            localStorage.setItem('applyCharToken', applyCharToken);
            toggleWrapperChar.classList.toggle('active', applyCharToken);
            toggleWrapperChar.classList.add('pulse');
            setTimeout(() => toggleWrapperChar.classList.remove('pulse'), 300);
            sliderChar.style.backgroundColor = applyCharToken ? ACTIVE_TAB_COLOR : '#ccc';
            sliderBeforeChar.style.transform = applyCharToken ? 'translateX(16px)' : 'translateX(0)';
            sliderBeforeChar.style.boxShadow = applyCharToken ? '0 0 2px rgba(0,0,0,0.2), 0 0 5px rgba(0,128,255,0.3)' : '0 0 2px rgba(0,0,0,0.2)';
            if (applyCharToken) {
                const pulseChar = makeElement('div', {}, {
                    position: 'absolute',
                    top: '0',
                    left: '0',
                    right: '0',
                    bottom: '0',
                    backgroundColor: ACTIVE_TAB_COLOR,
                    borderRadius: '24px',
                    opacity: '0.5',
                    transform: 'scale(1.2)',
                    pointerEvents: 'none',
                    zIndex: '-1'
                });
                toggleChar.appendChild(pulseChar);
                setTimeout(() => {
                    pulseChar.style.opacity = '0';
                    pulseChar.style.transform = 'scale(1.5)';
                    pulseChar.style.transition = 'all 400ms ease-out';
                }, 10);
                setTimeout(() => pulseChar.remove(), 400);
            }
        });
        sliderChar.appendChild(sliderBeforeChar);
        toggleChar.appendChild(inputChar);
        toggleChar.appendChild(sliderChar);
        toggleWrapperChar.addEventListener('click', (e) => {
            e.preventDefault();
            inputChar.checked = !inputChar.checked;
            const event = new Event('change');
            inputChar.dispatchEvent(event);
            document.body.focus();
        });
        toggleWrapperChar.appendChild(toggleLabelChar);
        toggleLabelChar.setAttribute('data-tooltip', 'Toggle replacement of character names with {{char}} placeholder.');
        toggleWrapperChar.appendChild(toggleChar);
        toggleContainerChar.appendChild(toggleWrapperChar);
        settingsContent.appendChild(toggleContainerChar);

        const tabs = {
            export: {
                content: exportContent,
                tab: exportTab,
                active: true
            },
            settings: {
                content: settingsContent,
                tab: settingsTab,
                active: false
            }
        };

        function switchTab(tabKey) {
            animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
            animationTimeouts = [];

            Object.entries(tabs).forEach(([key, {
                content,
                tab
            }]) => {
                const isActive = key === tabKey;

                tab.style.opacity = isActive ? '1' : '0.7';
                tab.style.transform = isActive ? 'translateY(-2px)' : '';

                const indicator = tab.lastChild;
                if (indicator) {
                    if (isActive) {
                        indicator.style.background = ACTIVE_TAB_COLOR;
                        indicator.style.transform = 'scaleX(1)';
                    } else {
                        indicator.style.background = INACTIVE_TAB_COLOR;
                        indicator.style.transform = 'scaleX(0.5)';
                    }
                }

                content.style.display = 'block';
                content.style.pointerEvents = isActive ? 'auto' : 'none';

                if (isActive) {
                    content.style.opacity = '0';
                    content.style.transform = 'scale(0.95)';

                    void content.offsetWidth;

                    requestAnimationFrame(() => {
                        content.style.opacity = '1';
                        content.style.transform = 'scale(1)';
                    });

                } else {
                    requestAnimationFrame(() => {
                        content.style.opacity = '0';
                        content.style.transform = 'scale(0.95)';
                    });

                    const hideTimeout = setTimeout(() => {
                        if (!tabs[key].active) {
                            content.style.display = 'none';
                        }
                    }, TAB_ANIMATION_DURATION);
                    animationTimeouts.push(hideTimeout);
                }

                tabs[key].active = isActive;
            });

            currentTab = tabKey;
            try {
                sessionStorage.setItem('lastActiveTab', tabKey);
            } catch (e) {
                console.warn('Failed to save tab state to sessionStorage', e);
            }
        }

        const handleTabClick = (e) => {
            const tt = document.getElementById('char-export-tooltip');
            if (tt) {
                tt.style.opacity = '0';
                const offsetHide = TOOLTIP_SLIDE_FROM_RIGHT ? -TOOLTIP_SLIDE_OFFSET : TOOLTIP_SLIDE_OFFSET;
                tt.style.transform = `translateX(${offsetHide}px)`;
            }
            const tabKey = e.target === exportTab ? 'export' : 'settings';
            if (!tabs[tabKey].active) {
                switchTab(tabKey);
            }
        };

        exportTab.onclick = handleTabClick;
        settingsTab.onclick = handleTabClick;

        Object.entries(tabs).forEach(([key, {
            content
        }]) => {
            const isActive = key === currentTab;

            content.style.display = isActive ? 'block' : 'none';
            content.style.opacity = isActive ? '1' : '0';
            content.style.transform = isActive ? 'scale(1)' : 'scale(0.95)';
        });

        switchTab(currentTab);
        document.body.appendChild(gui);

        void gui.offsetWidth;

        requestAnimationFrame(() => {
            gui.style.opacity = '1';
            gui.style.transform = 'translate(-50%, -50%) scale(1)';
        });

        document.addEventListener('click', handleDialogOutsideClick);
    }

    function toggleUIState() {
        animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
        animationTimeouts = [];

        if (guiElement && document.body.contains(guiElement)) {
            if (viewActive) {
                guiElement.style.display = 'flex';

                requestAnimationFrame(() => {
                    guiElement.style.opacity = '1';
                    guiElement.style.transform = 'translate(-50%, -50%) scale(1)';
                });
            } else {
                requestAnimationFrame(() => {
                    guiElement.style.opacity = '0';
                    guiElement.style.transform = 'translate(-50%, -50%) scale(0.95)';
                });

                const removeTimeout = setTimeout(() => {
                    if (!viewActive && guiElement && document.body.contains(guiElement)) {
                        document.body.removeChild(guiElement);
                        document.removeEventListener('click', handleDialogOutsideClick);
                        guiElement = null;
                    }
                }, ANIMATION_DURATION);
                animationTimeouts.push(removeTimeout);
            }
        } else if (viewActive) {
            createUI();
        }
    }

    function openV() {
        viewActive = true;
        toggleUIState();
    }

    function closeV() {
        viewActive = false;
        toggleUIState();
    }

    function handleDialogOutsideClick(e) {
        const gui = document.getElementById('char-export-gui');
        if (gui && !gui.contains(e.target)) {
            closeV();
        }
    }

    /* ============================
       ==      INTERCEPTORS     ==
       ============================ */
    function interceptNetwork() {
        if (networkInterceptActive) return;
        networkInterceptActive = true;

        const origXHR = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this.addEventListener('load', () => {
                if (url.includes('generateAlpha')) modifyResponse(this.responseText);
                if (url.includes('/hampter/chats/')) modifyChatResponse(this.responseText);
            });
            return origXHR.apply(this, arguments);
        };

        const origFetch = window.fetch;
        window.fetch = function(input, init) {
            const url = typeof input === 'string' ? input : input?.url;

            if (url && (url.includes('skibidi.com') || url.includes('proxy'))) {
                if (shouldInterceptNext && exportFormat) {
                    setTimeout(() => modifyResponse('{}'), 300);
                    return Promise.resolve(new Response('{}'));
                }
                return Promise.resolve(new Response(JSON.stringify({
                    error: 'Service unavailable'
                })));
            }

            try {
                return origFetch.apply(this, arguments).then(res => {
                    if (res.url?.includes('generateAlpha')) res.clone().text().then(modifyResponse);
                    if (res.url?.includes('/hampter/chats/')) res.clone().text().then(modifyChatResponse);
                    return res;
                });
            } catch (e) {
                return Promise.resolve(new Response('{}'));
            }
        };
    }

    function modifyResponse(text) {
        if (!shouldInterceptNext) return;
        shouldInterceptNext = false;
        try {
            const json = JSON.parse(text);
            const sys = json.messages.find(m => m.role === 'system')?.content || '';
            let initMsg = '';
            if (chatData?.chatMessages?.length) {
                const msgs = chatData.chatMessages;
                initMsg = msgs[msgs.length - 1].message;
            }
            const header = document.querySelector('p.chakra-text.css-1nj33dt');
            const headerName = header?.textContent.match(/Chat with\s+(.*)$/)?.[1]?.trim();
            const fullName = (chatData?.character?.chat_name || chatData?.character?.name || '').trim();
            const nameFirst = (chatData?.character?.name || '').trim().split(/\s+/)[0];
            const charName = fullName || nameFirst || headerName || 'char';
            let charBlock = extractTagContent(sys, fullName);
            if (!charBlock) {
                charBlock = extractTagContent(sys, nameFirst || charName);
            }
            const scen = extractTagContent(sys, 'scenario');
            const rawExs = extractTagContent(sys, 'example_dialogs');
            const exs = rawExs.replace(/^\s*Example conversations between[^:]*:\s*/, '');
            const userName = document.documentElement.innerHTML.match(/\\"name\\":\\"([^\\"]+)\\"/)?.[1] || '';
            switch (exportFormat) {
                case 'txt': {
                    saveAsTxt(charBlock, scen, initMsg, exs, charName, userName);
                    break;
                }
                case 'png': {
                    saveAsPng(charName, charBlock, scen, initMsg, exs, userName);
                    break;
                }
                case 'json': {
                    saveAsJson(charName, charBlock, scen, initMsg, exs, userName);
                    break;
                }
            }
            exportFormat = null;
        } catch (err) {
            console.error('Error processing response:', err);
        }
    }

    function modifyChatResponse(text) {
        try {
            if (!text || typeof text !== 'string' || !text.trim()) return;

            const data = JSON.parse(text);
            if (data && data.character) {
                chatData = data;
            }
        } catch (err) {
            // ignore parsing errors
        }
    }

    /* ============================
       ==      CORE LOGIC       ==
       ============================ */
    async function getCharacterMeta() {
        const charId = chatData?.character?.id;
        if (!charId) return {
            creatorUrl: '',
            characterVersion: '',
            characterCardUrl: '',
            name: '',
            creatorNotes: ''
        };
        if (characterMetaCache.id === charId && characterMetaCache.useChatNameForName === useChatNameForName) {
            return {
                creatorUrl: characterMetaCache.creatorUrl,
                characterVersion: characterMetaCache.characterVersion,
                characterCardUrl: characterMetaCache.characterCardUrl,
                name: characterMetaCache.name,
                creatorNotes: characterMetaCache.creatorNotes
            };
        }
        let creatorUrl = '',
            characterCardUrl = `https://janitorai.com/characters/${charId}`,
            characterVersion = characterCardUrl,
            name = chatData?.character?.name?.trim() || '',
            creatorNotes = chatData?.character?.description?.trim() || '';
        try {
            const response = await fetch(characterCardUrl);
            const html = await response.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const link = doc.querySelector('a.chakra-link.css-15sl5jl');
            if (link) {
                const href = link.getAttribute('href');
                if (href) creatorUrl = `https://janitorai.com${href}`;
            }
        } catch (err) {
            console.error('Error fetching creator URL:', err);
        }
        if (chatData?.character?.chat_name && !useChatNameForName) {
            characterVersion += `\nChat Name: ${chatData.character.chat_name.trim()}`;
        }
        characterMetaCache.id = charId;
        characterMetaCache.creatorUrl = creatorUrl;
        characterMetaCache.characterVersion = characterVersion;
        characterMetaCache.characterCardUrl = characterCardUrl;
        characterMetaCache.name = name;
        characterMetaCache.creatorNotes = creatorNotes;
        characterMetaCache.useChatNameForName = useChatNameForName;
        return {
            creatorUrl,
            characterVersion,
            characterCardUrl,
            name,
            creatorNotes
        };
    }

    async function buildTemplate(charBlock, scen, initMsg, exs) {
        const sections = [];
        const {
            creatorUrl,
            characterCardUrl
        } = await getCharacterMeta();
        const realName = chatData.character.name.trim();
        sections.push(`==== Name ====\n${realName}`);
        const chatName = (chatData.character.chat_name || realName).trim();
        sections.push(`==== Chat Name ====\n${chatName}`);
        if (charBlock) sections.push(`==== Description ====\n${charBlock.trim()}`);
        if (scen) sections.push(`==== Scenario ====\n${scen.trim()}`);
        if (initMsg) sections.push(`==== Initial Message ====\n${initMsg.trim()}`);
        if (exs) sections.push(`==== Example Dialogs ====\n${exs.trim()}`);
        sections.push(`==== Character Card ====\n${characterCardUrl}`);
        sections.push(`==== Creator ====\n${creatorUrl}`);
        return sections.join('\n\n');
    }

    function tokenizeNames(text, charName, userName) {
        const parts = text.split('\n\n');
        const [cRx, uRx] = [charName, userName].map(n => n ? escapeRegExp(n) : '');
        for (let i = 0, l = parts.length; i < l; ++i) {
            if (!/^==== (Name|Chat Name|Initial Message|Character Card|Creator) ====/.test(parts[i])) {
                parts[i] = parts[i]
                    .replace(new RegExp(`(?<!\\w)${cRx}(?!\\w)`, 'g'), '{{char}}')
                    .replace(new RegExp(`(?<!\\w)${uRx}(?!\\w)`, 'g'), '{{user}}');
            }
        }
        return parts.join('\n\n');
    }

    function tokenizeField(text, charName, userName) {
        if (!text) return text;
        const esc = n => escapeRegExp(n);
        const rules = [];
        if (applyCharToken && charName) {
            rules.push([new RegExp(`\\b${esc(charName)}('s)?\\b`, 'gi'), (_, sfx) => `{{char}}${sfx||''}`]);
        }
        if (userName) {
            rules.push([new RegExp(`\\b${esc(userName)}('s)?\\b`, 'gi'), (_, sfx) => `{{user}}${sfx||''}`]);
        }
        return rules.reduce((t, [rx, repl]) => t.replace(rx, repl), text);
    }

    function extraction() {
        if (!exportFormat) return;
        if (!document.querySelector('span.css-yhlqn1') && !document.querySelector('span.css-154nobl')) return;
        if (!chatData?.character) {
            if (confirm('Chat data not loaded yet. Reload the page now?')) {
                window.location.reload();
            }
            return;
        }
        if (!chatData.character.allow_proxy) {
            alert('Proxy disabled — extraction aborted.');
            return;
        }
        shouldInterceptNext = true;
        interceptNetwork();
        callApi();
    }

    function callApi() {
        try {
            const textarea = document.querySelector('textarea');
            if (!textarea) return;

            Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')
                .set.call(textarea, 'extract-char');
            textarea.dispatchEvent(new Event('input', {
                bubbles: true
            }));

            ['keydown', 'keyup'].forEach(type =>
                textarea.dispatchEvent(new KeyboardEvent(type, {
                    key: 'Enter',
                    code: 'Enter',
                    bubbles: true
                }))
            );
        } catch (err) {
            // ignore errors
        }
    }

    /* ============================
       ==    CHARA CARD V2      ==
       ============================ */
    async function buildCharaCardV2(charName, charBlock, scen, initMsg, exs, userName) {
        const {
            creatorUrl,
            characterVersion,
            name,
            creatorNotes
        } = await getCharacterMeta();
        const tokenizedDesc = tokenizeField(charBlock, charName, userName);
        const tokenizedScen = tokenizeField(scen, charName, userName);
        const tokenizedExs = tokenizeField(exs, charName, userName);

        let displayName = name;
        let versionText = characterVersion;

        if (useChatNameForName && chatData?.character?.chat_name) {
            displayName = chatData.character.chat_name.trim();
            versionText = `${characterVersion}\nName: ${name}`;
        }

        return {
            spec: "chara_card_v2",
            spec_version: "2.0",
            data: {
                name: displayName,
                description: tokenizedDesc.trim(),
                personality: "",
                scenario: tokenizedScen.trim(),
                first_mes: initMsg.trim(),
                mes_example: tokenizedExs.trim(),
                creator_notes: creatorNotes,
                system_prompt: "",
                post_history_instructions: "",
                alternate_greetings: [],
                character_book: null,
                tags: [],
                creator: creatorUrl,
                character_version: versionText,
                extensions: {}
            }
        };
    }

    /* ============================
       ==       EXPORTERS       ==
       ============================ */
    async function saveAsTxt(charBlock, scen, initMsg, exs, charName, userName) {
        const template = await buildTemplate(charBlock, scen, initMsg, exs);
        const tokenized = tokenizeField(template, charName, userName);
        const rawName = chatData.character.name || chatData.character.chat_name || 'card';
        const fileName = rawName.trim() || 'card';
        saveFile(
            `${fileName}.txt`,
            new Blob([tokenized], {
                type: 'text/plain'
            })
        );
    }

    async function saveAsJson(charName, charBlock, scen, initMsg, exs, userName) {
        const jsonData = await buildCharaCardV2(charName, charBlock, scen, initMsg, exs, userName);

        const rawName = chatData.character.name || chatData.character.chat_name || 'card';
        const fileName = rawName.trim() || 'card';
        saveFile(
            `${fileName}.json`,
            new Blob([JSON.stringify(jsonData, null, 2)], {
                type: 'application/json'
            })
        );
    }

    async function saveAsPng(charName, charBlock, scen, initMsg, exs, userName) {
        try {
            const avatarImg = document.querySelector('img[src*="/bot-avatars/"]');
            if (!avatarImg) {
                alert('Character avatar not found.');
                return;
            }

            const cardData = await buildCharaCardV2(charName, charBlock, scen, initMsg, exs, userName);

            const avatarResponse = await fetch(avatarImg.src);
            const avatarBlob = await avatarResponse.blob();

            const img = new Image();
            img.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = img.width;
                canvas.height = img.height;

                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);

                canvas.toBlob(async (blob) => {
                    try {
                        const arrayBuffer = await blob.arrayBuffer();
                        const pngData = new Uint8Array(arrayBuffer);

                        const jsonString = JSON.stringify(cardData);

                        const base64Data = btoa(unescape(encodeURIComponent(jsonString)));

                        const keyword = "chara";
                        const keywordBytes = new TextEncoder().encode(keyword);
                        const nullByte = new Uint8Array([0]);
                        const textBytes = new TextEncoder().encode(base64Data);

                        const chunkType = new Uint8Array([116, 69, 88, 116]); // "tEXt" in ASCII
                        const dataLength = keywordBytes.length + nullByte.length + textBytes.length;

                        const lengthBytes = new Uint8Array(4);
                        lengthBytes[0] = (dataLength >>> 24) & 0xFF;
                        lengthBytes[1] = (dataLength >>> 16) & 0xFF;
                        lengthBytes[2] = (dataLength >>> 8) & 0xFF;
                        lengthBytes[3] = dataLength & 0xFF;

                        const crcData = new Uint8Array(chunkType.length + keywordBytes.length + nullByte.length + textBytes.length);
                        crcData.set(chunkType, 0);
                        crcData.set(keywordBytes, chunkType.length);
                        crcData.set(nullByte, chunkType.length + keywordBytes.length);
                        crcData.set(textBytes, chunkType.length + keywordBytes.length + nullByte.length);

                        const crc = computeCrc32(crcData, 0, crcData.length);
                        const crcBytes = new Uint8Array(4);
                        crcBytes[0] = (crc >>> 24) & 0xFF;
                        crcBytes[1] = (crc >>> 16) & 0xFF;
                        crcBytes[2] = (crc >>> 8) & 0xFF;
                        crcBytes[3] = crc & 0xFF;

                        let pos = 8; // Skip PNG signature
                        while (pos < pngData.length - 12) {
                            const length = pngData[pos] << 24 | pngData[pos + 1] << 16 |
                                pngData[pos + 2] << 8 | pngData[pos + 3];

                            const type = String.fromCharCode(
                                pngData[pos + 4], pngData[pos + 5],
                                pngData[pos + 6], pngData[pos + 7]
                            );

                            if (type === 'IEND') break;
                            pos += 12 + length; // 4 (length) + 4 (type) + length + 4 (CRC)
                        }

                        const finalSize = pngData.length + lengthBytes.length + chunkType.length + dataLength + crcBytes.length;
                        const finalPNG = new Uint8Array(finalSize);

                        finalPNG.set(pngData.subarray(0, pos));
                        let writePos = pos;

                        finalPNG.set(lengthBytes, writePos);
                        writePos += lengthBytes.length;
                        finalPNG.set(chunkType, writePos);
                        writePos += chunkType.length;
                        finalPNG.set(keywordBytes, writePos);
                        writePos += keywordBytes.length;
                        finalPNG.set(nullByte, writePos);
                        writePos += nullByte.length;
                        finalPNG.set(textBytes, writePos);
                        writePos += textBytes.length;
                        finalPNG.set(crcBytes, writePos);
                        writePos += crcBytes.length;

                        finalPNG.set(pngData.subarray(pos), writePos);

                        const rawName = chatData.character.name || chatData.character.chat_name || 'card';
                        const fileName = rawName.trim() || 'card';
                        saveFile(
                            `${fileName}.png`,
                            new Blob([finalPNG], {
                                type: 'image/png'
                            })
                        );

                        console.log("Character card created successfully!");
                    } catch (err) {
                        console.error('Error creating PNG:', err);
                        alert('Failed to create PNG: ' + err.message);
                    }
                }, 'image/png');
            };

            img.src = URL.createObjectURL(avatarBlob);
        } catch (err) {
            console.error('Error creating PNG:', err);
            alert('Failed to create PNG: ' + err.message);
        }
    }

    function computeCrc32(data, start, length) {
        let crc = 0xFFFFFFFF;

        for (let i = 0; i < length; i++) {
            const byte = data[start + i];
            crc = (crc >>> 8) ^ crc32Table[(crc ^ byte) & 0xFF];
        }

        return ~crc >>> 0; // Invert and cast to unsigned 32-bit
    }

    const crc32Table = (() => {
        const table = new Uint32Array(256);

        for (let i = 0; i < 256; i++) {
            let crc = i;
            for (let j = 0; j < 8; j++) {
                crc = (crc & 1) ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1;
            }
            table[i] = crc;
        }

        return table;
    })();

    /* ============================
       ==       ROUTING         ==
       ============================ */
    function inChats() {
        const isInChat = /^\/chats\/\d+/.test(window.location.pathname);
        return isInChat;
    }

    function initialize() {
        if (hasInitialized || !inChats()) return;
        hasInitialized = true;
        shouldInterceptNext = false;
        networkInterceptActive = false;
        exportFormat = null;
        chatData = null;

        document.removeEventListener('keydown', handleKeyDown);
        document.addEventListener('keydown', handleKeyDown);

        interceptNetwork();
    }

    function handleKeyDown(e) {
        if (!inChats()) return;
        if (e.key.toLowerCase() !== 't' || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
        if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.isContentEditable) return;

        if (!document.querySelector('span.css-yhlqn1') && !document.querySelector('span.css-154nobl')) return;
        if (!chatData?.character) {
            if (confirm('Chat data not loaded yet. Reload the page now?')) {
                window.location.reload();
            }
            return;
        }
        if (!chatData.character.allow_proxy) {
            alert('Proxy disabled — extraction aborted.');
            return;
        }

        viewActive = !viewActive;
        toggleUIState();
    }

    function cleanup() {
        hasInitialized = false;
        const gui = document.getElementById('char-export-gui');
        if (gui) gui.remove();
        document.removeEventListener('keydown', handleKeyDown);
        viewActive = false;
        animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
        animationTimeouts = [];
    }

    function handleRoute() {
        if (inChats()) {
            initialize();
        } else {
            cleanup();
        }
    }

    /* ============================
       ==      ENTRYPOINT       ==
       ============================ */
    window.addEventListener('load', () => {
        handleRoute();
    }, {
        once: true
    });
    window.addEventListener('popstate', () => {
        handleRoute();
    });

    ['pushState', 'replaceState'].forEach(fn => {
        const orig = history[fn];
        history[fn] = function(...args) {
            const res = orig.apply(this, args);
            setTimeout(handleRoute, 50);
            return res;
        };
    });
    handleRoute();
})();