AI Chat Template Assistant

Universal template system with upward menu and cross-platform support

// ==UserScript==
// @name         AI Chat Template Assistant
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Universal template system with upward menu and cross-platform support
// @author       Dieha
// @license      MIT
// @match        https://aistudio.google.com/app*
// @match        https://chat.qwen.ai/*
// @match        https://chat.qwen.com/*
// @match        https://chat.deepseek.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ================ CONFIGURATION ================
    const GLOBAL_CONFIG = {
        buttonText: "Шаблоны",
        maxMenuDepth: 5,
        animationDuration: 200, // ms
        retryInterval: 1500,
        debugMode: true // Включите для отладки, особенно на AI Studio
    };

    const TEMPLATE_DATA = [
        {
            categoryName: "Основные Задачи",
            items: [
                { label: "Код файлов", text: "Напишите полный код изменяемых файл[ов/а]\n" },
                {
                    label: "Объяснения", // Уровень 0 (относительно категории)
                    subItems: [
                        { label: "Объясни (ELI5)", text: "Объясни [ТЕМА] так, как будто мне 5 лет." }, // Уровень 1
                        { label: "Подробно с примерами", text: "Объясни [ТЕМА] подробно, приведи примеры." },
                        {
                            label: "Сложные Аналогии", // Уровень 1
                            subItems: [ // Уровень 2
                                { label: "Аналогия для Технаря", text: "Объясни [ТЕМА] через аналогию из IT." },
                                { label: "Аналогия для Гуманитария", text: "Объясни [ТЕМА] через аналогию из литературы."}
                            ]
                        }
                    ]
                },
            ]
        },
        {
            categoryName: "Работа с кодом",
            items: [
                {
                    label: "Рефакторинг",
                    text: "Проведи рефакторинг следующего кода:\n```\n[ВСТАВЬТЕ КОД]\n```"
                },
                {
                    label: "Поиск ошибок",
                    text: "Найди и исправь ошибки в коде:\n```\n[ВСТАВЬТЕ КОД]\n```"
                }
            ]
        }
    ];

    const PLATFORM_SETTINGS = {
        'aistudio.google.com': {
            // buttonContainer: ".prompt-input-wrapper-container", // Оставляем этот, если prepend работает
            buttonContainer: ".prompt-input-wrapper-container > .text-wrapper", // Попробуем так, чтобы кнопка была слева от текстового поля
            // buttonContainer: ".prompt-input-wrapper-container > div:nth-child(2)", // Если хотим перед первой кнопкой "add-chunk-menu"
            textAreaSelector: 'textarea[aria-label="Type something or pick one from prompt gallery"]', // ОБНОВЛЕНО
            insertMethod: "react", // Оставляем react, он должен хорошо работать с Angular (на котором построен AI Studio)
            buttonStyle: {
                background: "#2D2E30",
                border: "1px solid #454545",
                hoverBackground: "#3A3C3F",
                textColor: "#E3E3E3"
            }
        },
        'chat.deepseek.com': {
            buttonContainer: "div.ec4f5d61",
            textAreaSelector: "textarea#chat-input",
            insertMethod: "advanced", // ВОЗВРАЩАЕМ advanced
            buttonStyle: {
                background: "transparent",
                border: "1px solid #5E5E5E",
                hoverBackground: "#2D2F33",
                textColor: "#F8FAFF"
            }
        },
        'chat.qwen.ai': { // Также для chat.qwen.com, т.к. detectPlatform ищет по .includes()
            buttonContainer: ".chat-message-input-container-inner > div.flex.items-center.min-h-\\[56px\\]",
            textAreaSelector: "textarea#chat-input",
            insertMethod: "standard",
            buttonStyle: {
                background: "#2B2B2B",
                border: "1px solid #4A4A4A",
                hoverBackground: "#3A3A3A",
                textColor: "#E0E0E0"
            }
        }
    };
    // Добавим chat.qwen.com, если его конфигурация идентична chat.qwen.ai
    if (!PLATFORM_SETTINGS['chat.qwen.com'] && PLATFORM_SETTINGS['chat.qwen.ai']) {
        PLATFORM_SETTINGS['chat.qwen.com'] = { ...PLATFORM_SETTINGS['chat.qwen.ai'] };
    }


    // ================ GLOBAL STATE ================
    let mainMenu = null;
    let templateButton = null;
    const activeSubmenus = []; // Массив для хранения активных подменю
    let currentPlatform = null;

    // ================ STYLES ================
    GM_addStyle(`
        .template-system-container {
            position: relative;
            display: inline-block;
            margin-right: 12px;
            vertical-align: middle;
            z-index: 99999;
        }

        .template-main-button {
            display: flex;
            align-items: center;
            height: 36px;
            padding: 0 16px;
            border-radius: 8px;
            font-size: 14px;
            font-family: system-ui, sans-serif;
            cursor: pointer;
            transition: all 0.2s ease;
            box-sizing: border-box;
            user-select: none;
        }

        .template-main-button:hover {
            filter: brightness(1.1);
        }

        .template-main-button::after {
            content: "▼";
            font-size: 0.7em;
            margin-left: 8px;
            opacity: 0.7;
            transition: transform 0.2s ease;
        }

        .template-main-button.active::after {
            transform: rotate(180deg);
        }

        .template-menu-wrapper {
            position: fixed; /* Позиционирование left/bottom/top будет через JS */
            min-width: 280px;
            max-height: 70vh;
            overflow-y: auto;
            /* Изначальный сдвиг для анимации "всплытия" */
            transform: translateY(10px);
            opacity: 0;
            visibility: hidden;
            /* Плавный переход для opacity и transform, visibility меняется резко с задержкой */
            transition: opacity ${GLOBAL_CONFIG.animationDuration}ms ease, transform ${GLOBAL_CONFIG.animationDuration}ms ease, visibility 0s linear ${GLOBAL_CONFIG.animationDuration}ms;
            z-index: 100000;
            pointer-events: none;
        }

        .template-menu-wrapper.visible {
            opacity: 1;
            visibility: visible;
            transform: translateY(0); /* Возврат на место */
            pointer-events: all;
            transition-delay: 0s; /* Убрать задержку для visibility при показе */
        }

        .template-menu-content {
            background: #2D2E30;
            border-radius: 12px; /* Можно оставить 12px со всех сторон или 12px 12px 0 0 если всегда сверху */
            box-shadow: 0 0px 32px rgba(0,0,0,0.3); /* Тень изменена, т.к. меню может быть и снизу */
            padding: 12px 0;
            /* margin-bottom: 10px;  Убрано, т.к. позиционирование точное */
        }

        .menu-category-header {
            padding: 10px 20px 8px;
            font-size: 12px;
            font-weight: 600;
            color: #909090;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            border-bottom: 1px solid #404040;
            margin: 0 12px 6px;
        }

        .menu-item {
            position: relative;
            padding: 12px 20px;
            color: #E3E3E3;
            cursor: pointer;
            font-size: 14px;
            white-space: nowrap;
            transition: background 0.15s ease;
            margin: 0 8px;
            border-radius: 6px;
        }

        .menu-item:hover {
            background: #3A3C3F;
        }

        .menu-item.has-submenu::after {
            content: "▶";
            position: absolute;
            right: 16px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 12px;
            color: #A0A0A0;
        }

        .submenu-container {
            position: fixed; /* ИЗМЕНЕНО: было absolute, теперь fixed для корректного positionSubmenu */
            background: #353638;
            min-width: 260px;
            border-radius: 8px;
            box-shadow: 4px 4px 24px rgba(0,0,0,0.3);
            padding: 8px 0;
            display: none; /* Будет 'block' при показе */
            z-index: 100001;
        }

        ::-webkit-scrollbar {
            width: 8px;
        }
        ::-webkit-scrollbar-track {
            background: #252526;
            border-radius: 4px;
        }
        ::-webkit-scrollbar-thumb {
            background: #454545;
            border-radius: 4px;
        }
    `);

    // ================ CORE FUNCTIONS ================
    function logDebug(...args) {
        if (GLOBAL_CONFIG.debugMode) {
            console.log("[ACTA]", ...args);
        }
    }

    function logError(...args) {
        if (GLOBAL_CONFIG.debugMode) {
            console.error("[ACTA]", ...args);
        }
    }

    function initializeSystem() {
        currentPlatform = detectPlatform();

        if (!currentPlatform) {
            logDebug("Platform not detected. Retrying...");
            retryInitialization();
            return;
        }

        if (!validatePlatformConfig(currentPlatform)) {
            logDebug("Platform config invalid or elements not found. Retrying...", currentPlatform);
            retryInitialization();
            return;
        }

        if (isAlreadyInitialized()) {
            logDebug("System already initialized.");
            return;
        }

        try {
            createUIElements();
            setupEventHandlers();
            logDebug("Initialization complete for platform:", window.location.hostname);
        } catch (error) {
            logError("Error during initialization:", error);
        }
    }

    function detectPlatform() {
        const hostname = window.location.hostname;
        for (const domain of Object.keys(PLATFORM_SETTINGS)) {
            if (hostname.includes(domain)) return PLATFORM_SETTINGS[domain];
        }
        return null;
    }

    function validatePlatformConfig(config) {
        if (!config || !config.buttonContainer || !config.textAreaSelector) {
            logError("Platform config is missing essential selectors.");
            return false;
        }
        const buttonContainerEl = document.querySelector(config.buttonContainer);
        const textAreaEl = document.querySelector(config.textAreaSelector);

        if (!buttonContainerEl) {
            logDebug(`Button container ("${config.buttonContainer}") not found.`);
        }
        if (!textAreaEl) {
            logDebug(`Text area ("${config.textAreaSelector}") not found.`);
        }
        return !!buttonContainerEl && !!textAreaEl;
    }

    function createUIElements() {
        const buttonHost = document.querySelector(currentPlatform.buttonContainer);
        if (!buttonHost) {
            logError("Button host container not found, cannot create UI elements.");
            return; // Добавлена проверка
        }

        const container = document.createElement("div");
        container.className = "template-system-container";

        templateButton = document.createElement("div");
        templateButton.className = "template-main-button";
        templateButton.textContent = GLOBAL_CONFIG.buttonText;

        Object.assign(templateButton.style, {
            background: currentPlatform.buttonStyle.background,
            border: currentPlatform.buttonStyle.border,
            color: currentPlatform.buttonStyle.textColor
        });
        templateButton.addEventListener('mouseenter', () => {
            if (currentPlatform.buttonStyle.hoverBackground) {
                templateButton.style.setProperty('background', currentPlatform.buttonStyle.hoverBackground, 'important');
            }
        });
        templateButton.addEventListener('mouseleave', () => {
            templateButton.style.background = currentPlatform.buttonStyle.background;
        });


        container.appendChild(templateButton);
        buttonHost.prepend(container);

        mainMenu = document.createElement("div");
        mainMenu.className = "template-menu-wrapper";

        const menuContent = document.createElement("div");
        menuContent.className = "template-menu-content";

        TEMPLATE_DATA.forEach(category => {
            const categoryHeader = document.createElement("div");
            categoryHeader.className = "menu-category-header";
            categoryHeader.textContent = category.categoryName;
            menuContent.appendChild(categoryHeader);

            category.items.forEach(item => {
                menuContent.appendChild(createMenuItem(item, 0)); // Начальная глубина 0
            });
        });

        mainMenu.appendChild(menuContent);
        document.body.appendChild(mainMenu);
    }

    function createMenuItem(itemData, depth) { // depth - глубина текущего элемента
        const itemElement = document.createElement("div");
        itemElement.className = "menu-item";
        itemElement.textContent = itemData.label;

        if (itemData.subItems && depth < GLOBAL_CONFIG.maxMenuDepth) {
            itemElement.classList.add("has-submenu");
            const submenu = createSubMenu(itemData.subItems, depth + 1); // Подменю будет на depth + 1
            setupSubmenuBehavior(itemElement, submenu, depth + 1); // Передаем глубину создаваемого подменю
        } else if (itemData.text) {
            itemElement.dataset.template = itemData.text;
        }
        return itemElement;
    }

    function createSubMenu(items, depth) {
        const submenu = document.createElement("div");
        submenu.className = "submenu-container";
        submenu.dataset.depth = depth; // Сохраняем глубину для управления

        items.forEach(item => {
            submenu.appendChild(createMenuItem(item, depth)); // Элементы подменю на той же глубине, что и само подменю
        });
        return submenu;
    }

    function removeSubmenu(submenuElement) {
        if (!submenuElement) return;
        const index = activeSubmenus.indexOf(submenuElement);
        if (index > -1) {
            activeSubmenus.splice(index, 1);
        }
        submenuElement.remove();
    }

    function closeSubmenusFromDepth(targetDepth) {
        const toRemove = [];
        for (let i = activeSubmenus.length - 1; i >= 0; i--) {
            const sub = activeSubmenus[i];
            if (parseInt(sub.dataset.depth, 10) >= targetDepth) {
                toRemove.push(sub); // Собираем для удаления
            }
        }
        toRemove.forEach(sub => removeSubmenu(sub));
    }

    function setupSubmenuBehavior(parentItem, submenu, submenuDepth) {
        parentItem.addEventListener("mouseenter", () => {
            closeSubmenusFromDepth(submenuDepth); // Закрыть подменю этого и более глубоких уровней от других веток

            document.body.appendChild(submenu); // Важно добавить в DOM до getBoundingClientRect/offsetWidth
            positionSubmenu(parentItem, submenu);
            activeSubmenus.push(submenu);
        });

        // Чтобы подменю не закрывалось при переходе с родителя на него
        let PItimer, SUtimer;
        parentItem.addEventListener("mouseleave", (e) => {
            PItimer = setTimeout(() => {
                if (!submenu.matches(':hover')) { // Если курсор не над подменю
                    removeSubmenu(submenu);
                }
            }, 100); // Небольшая задержка
        });
        submenu.addEventListener("mouseenter", () => clearTimeout(PItimer)); // Отменить закрытие, если вошли в подменю

        submenu.addEventListener("mouseleave", (e) => {
            SUtimer = setTimeout(() => {
                if (!parentItem.matches(':hover')) { // Если курсор не над родительским элементом
                    removeSubmenu(submenu);
                }
            }, 100);
        });
        parentItem.addEventListener("mouseenter", () => clearTimeout(SUtimer)); // Отменить закрытие, если вернулись на родителя
    }


    function positionSubmenu(parentElement, submenu) {
        submenu.style.display = "block"; // Показать для измерения размеров
        const parentRect = parentElement.getBoundingClientRect();
        const submenuRect = submenu.getBoundingClientRect(); // Получить размеры после display: block
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        let left = parentRect.right + 5;
        let top = parentRect.top;

        // Проверка правой границы
        if (left + submenuRect.width > viewportWidth - 10) {
            left = parentRect.left - submenuRect.width - 5;
        }
        // Если и слева не помещается (очень широкое подменю или родитель у края)
        if (left < 10) {
            left = 10;
        }

        // Проверка нижней границы
        if (top + submenuRect.height > viewportHeight - 10) {
            top = viewportHeight - submenuRect.height - 10;
        }
        // Если и сверху не помещается (очень высокое подменю или родитель у края)
        if (top < 10) {
            top = 10;
        }

        submenu.style.left = `${left}px`;
        submenu.style.top = `${top}px`;
    }

    function setupEventHandlers() {
        if (!templateButton || !mainMenu) {
            logError("Cannot setup event handlers: button or menu not created.");
            return;
        }
        templateButton.addEventListener("click", (e) => {
            e.stopPropagation();
            toggleMainMenu();
        });

        document.addEventListener("click", (e) => {
            // Проверяем, был ли клик вне кнопки И вне главного меню И вне любого активного подменю
            if (mainMenu.classList.contains("visible") &&
                !templateButton.contains(e.target) &&
                !mainMenu.contains(e.target) &&
                !activeSubmenus.some(submenu => submenu.contains(e.target))) {
                closeAllMenus();
            }
        });

        // Обработчик выбора шаблона (привязан к mainMenu, чтобы не слушать весь документ без нужды)
        mainMenu.addEventListener("click", (e) => {
            const targetItem = e.target.closest(".menu-item:not(.has-submenu)"); // Только элементы без подменю
            if (targetItem && targetItem.dataset.template) {
                insertTemplateText(targetItem.dataset.template);
                closeAllMenus();
            }
        });
        // То же для подменю (они в body, поэтому слушаем body, но проверяем класс)
        document.body.addEventListener("click", (e) => {
            const targetItem = e.target.closest(".submenu-container .menu-item:not(.has-submenu)");
            if (targetItem && targetItem.dataset.template) {
                insertTemplateText(targetItem.dataset.template);
                closeAllMenus();
            }
        });


        window.addEventListener("resize", () => { if(mainMenu.classList.contains("visible")) updateMenuPosition(); });
        window.addEventListener("scroll", () => { if(mainMenu.classList.contains("visible")) updateMenuPosition(); }, true);
    }

    function toggleMainMenu() {
        const становитсяВидимым = !mainMenu.classList.contains("visible");
        mainMenu.classList.toggle("visible");
        templateButton.classList.toggle("active");

        if (становитсяВидимым) {
            updateMenuPosition(); // Позиционируем при открытии
        } else {
            closeSubmenusFromDepth(0); // Закрыть все подменю при закрытии главного
        }
    }

    function updateMenuPosition() {
        if (!mainMenu || !mainMenu.classList.contains("visible") || !templateButton) return;

        const buttonRect = templateButton.getBoundingClientRect();
        mainMenu.style.visibility = 'hidden'; // Скрыть на время измерений, чтобы избежать мигания
        mainMenu.style.display = 'block';    // Убедиться, что display не none
        const menuHeight = mainMenu.offsetHeight;
        const menuWidth = mainMenu.offsetWidth;
        mainMenu.style.display = '';       // Вернуть как было (если был не block)
        mainMenu.style.visibility = '';    // Вернуть видимость

        const viewportHeight = window.innerHeight;
        const viewportWidth = window.innerWidth;
        const margin = 10; // Отступ от кнопки

        // Вертикальное позиционирование
        // Приоритет НАД кнопкой, если кнопка в нижней половине И есть место,
        // ИЛИ если места сверху больше, чем снизу (и снизу не хватает)
        let menuTop, menuBottom = "auto";

        if ( (buttonRect.top > viewportHeight / 2 && buttonRect.top >= menuHeight + margin) ||
            (buttonRect.top >= menuHeight + margin && (viewportHeight - buttonRect.bottom) < menuHeight + margin && (viewportHeight - buttonRect.bottom) < buttonRect.top) ) {
            // Позиционируем НАД кнопкой
            menuTop = "auto";
            menuBottom = `${viewportHeight - buttonRect.top + margin}px`;
            mainMenu.style.borderRadius = "12px 12px 0 0"; // Если меню сверху
        } else {
            // Позиционируем ПОД кнопкой
            menuTop = `${buttonRect.bottom + margin}px`;
            menuBottom = "auto";
            mainMenu.style.borderRadius = "0 0 12px 12px"; // Если меню снизу
        }
        // Коррекция, если меню выходит за пределы экрана по высоте
        if (menuTop !== "auto") {
            const topNum = parseFloat(menuTop);
            if (topNum < margin) menuTop = `${margin}px`;
            if (topNum + menuHeight > viewportHeight - margin) {
                menuTop = `${Math.max(margin, viewportHeight - menuHeight - margin)}px`;
            }
        } else if (menuBottom !== "auto") {
            const bottomNum = parseFloat(menuBottom);
            // Если mainMenu.style.bottom установлено, то top вычисляется браузером.
            // Нам нужно проверить, не уходит ли верхний край меню за пределы viewport.
            // (viewportHeight - bottomNum - menuHeight) - это будет координата top.
            if ((viewportHeight - bottomNum - menuHeight) < margin) {
                menuBottom = `${Math.max(margin, viewportHeight - menuHeight - margin)}px`;
                // Это не совсем то, это изменит bottom, что сдвинет top.
                // Лучше установить maxHeight, если не помещается
            }
        }

        mainMenu.style.top = menuTop;
        mainMenu.style.bottom = menuBottom;


        // Горизонтальное позиционирование
        if (buttonRect.left + menuWidth > viewportWidth - 20) {
            mainMenu.style.left = "auto";
            mainMenu.style.right = `${Math.max(10, viewportWidth - (buttonRect.right))}px`; // Выровнять по правому краю кнопки или по краю экрана
        } else {
            mainMenu.style.left = `${buttonRect.left}px`;
            mainMenu.style.right = "auto";
        }
    }

    function insertTemplateText(text) {
        const textArea = document.querySelector(currentPlatform.textAreaSelector);
        if (!textArea) {
            logError("Textarea not found for inserting text.");
            return;
        }

        // ИЗМЕНЕНИЕ ЗДЕСЬ: Убираем обработку плейсхолдеров с prompt
        let finalTtext = text;
        // Конец изменения

        try {
            // Запоминаем текущее значение и позицию курсора
            const start = textArea.selectionStart;
            const end = textArea.selectionEnd;
            const originalValue = textArea.value;

            // Формируем новый текст
            const newValue = originalValue.substring(0, start) + finalTtext + originalValue.substring(end);

            if (currentPlatform.insertMethod === "react") {
                insertForReact(textArea, newValue, finalTtext.length, start);
            } else if (currentPlatform.insertMethod === "advanced") {
                insertWithEvents(textArea, newValue, finalTtext.length, start);
            } else {
                standardInsert(textArea, newValue, finalTtext.length, start);
            }
        } catch (error) {
            logError("Insert error, trying fallback:", error);
            fallbackInsert(textArea, finalTtext); // В fallback передаем только сам шаблон
        }
        textArea.focus();
    }

    // Обновленные функции вставки для установки курсора после вставленного текста
    function setCursorPosition(textArea, position) {
        textArea.selectionStart = textArea.selectionEnd = position;
    }

    function insertForReact(textArea, text, insertedTextLength, originalStart) {
        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
        nativeInputValueSetter.call(textArea, text);
        const ev = new Event("input", { bubbles: true });
        textArea.dispatchEvent(ev);
        setCursorPosition(textArea, originalStart + insertedTextLength);
    }

    function insertWithEvents(textArea, text, insertedTextLength, originalStart) {
        // Попробуем сначала "родной" способ, как в React, на случай если он сработает
        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
        if (nativeInputValueSetter) {
            nativeInputValueSetter.call(textArea, text);
        } else {
            textArea.value = text; // Если сеттер не нашелся, используем прямое присваивание
        }

        // Отправляем разнообразные события
        // Порядок может иметь значение
        textArea.dispatchEvent(new Event('focus', { bubbles: true, cancelable: true }));
        textArea.dispatchEvent(new Event('keydown', { bubbles: true, cancelable: true, key: 'a', char: 'a', keyCode: 65 })); // имитация нажатия клавиши
        textArea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true, inputType: 'insertText' }));
        textArea.dispatchEvent(new Event('keyup', { bubbles: true, cancelable: true, key: 'a', char: 'a', keyCode: 65 }));
        textArea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
        // textArea.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true })); // Может быть не нужно сразу блюрить

        setCursorPosition(textArea, originalStart + insertedTextLength);
        // Иногда нужно еще раз сфокусироваться после всех манипуляций
        textArea.focus();
    }

    function standardInsert(textArea, text, insertedTextLength, originalStart) {
        textArea.value = text;
        textArea.dispatchEvent(new Event("input", { bubbles: true }));
        setCursorPosition(textArea, originalStart + insertedTextLength);
    }

    function fallbackInsert(textArea, textToInsert) {
        textArea.focus();
        // document.execCommand более не рекомендуется, но может быть последним средством
        // Простая вставка в текущую позицию курсора
        const start = textArea.selectionStart;
        const end = textArea.selectionEnd;
        textArea.value = textArea.value.substring(0, start) + textToInsert + textArea.value.substring(end);
        setCursorPosition(textArea, start + textToInsert.length);
    }


    function closeAllMenus() {
        if (mainMenu) mainMenu.classList.remove("visible");
        if (templateButton) templateButton.classList.remove("active");
        closeSubmenusFromDepth(0); // Закрыть все подменю (глубина 0 и больше)
    }

    function retryInitialization() {
        logDebug(`Retrying initialization in ${GLOBAL_CONFIG.retryInterval}ms...`);
        setTimeout(initializeSystem, GLOBAL_CONFIG.retryInterval);
    }

    function isAlreadyInitialized() {
        return !!document.querySelector(".template-system-container");
    }

    // ================ INITIALIZATION ================
    // Запускаем инициализацию после полной загрузки страницы или с небольшой задержкой,
    // чтобы дать шанс динамическим элементам появиться.
    if (document.readyState === "complete" || document.readyState === "interactive") {
        setTimeout(initializeSystem, 500); // Небольшая задержка перед первой попыткой
    } else {
        window.addEventListener("DOMContentLoaded", () => setTimeout(initializeSystem, 500));
    }

    // MutationObserver для отслеживания изменений в DOM, если начальная инициализация не удалась
    const observer = new MutationObserver((mutations, obs) => {
        if (!isAlreadyInitialized()) {
            logDebug("DOM changed, attempting re-initialization.");
            initializeSystem(); // Попытка инициализации при изменениях DOM
        }
        // Если уже инициализировано, можно остановить наблюдение, если это целесообразно
        // else { obs.disconnect(); logDebug("System initialized, observer disconnected."); }
    });

    // Начинаем наблюдение за body, если элементы могут появляться динамически
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();