Module Dispatcher

Central UI for managing LoreCanvas modules. Provides a shared panel, per-chat module settings, and IndexedDB storage. External script for JanitorAI.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Module Dispatcher
// @namespace    Violentmonkey Scripts
// @version      4.5
// @match        https://janitorai.com/*
// @grant        none
// @author       Glazanochi
// @description  Central UI for managing LoreCanvas modules. Provides a shared panel, per-chat module settings, and IndexedDB storage. External script for JanitorAI.
// ==/UserScript==

/* jshint esversion: 11 */
/* jshint -W083 */

(function() {
    'use strict';

    // ============================================================
    // 0. ЯДРО IndexedDB (безопасное управление версиями)
    // ============================================================
    const DB_NAME = 'JanitorModulesDB';

    const BASE_STORES = {
        dispatcher: { keyPath: 'key' },
        chats_data: { keyPath: 'key' }
    };

    const registeredStores = { ...BASE_STORES };

    let dbInstance = null;
    let openPromise = null;

    function registerStore(storeName, keyPath) {
        if (registeredStores[storeName]) {
            console.warn(`Хранилище "${storeName}" уже зарегистрировано.`);
            return;
        }
        registeredStores[storeName] = { keyPath };
        console.log(`📌 Зарегистрировано хранилище "${storeName}"`);
    }

    async function openDB() {
        if (dbInstance) return dbInstance;
        if (openPromise) return openPromise;

        openPromise = (async () => {
            const databases = await indexedDB.databases();
            const currentDB = databases.find(db => db.name === DB_NAME);
            const currentVersion = currentDB ? currentDB.version : 0;

            if (currentVersion === 0) {
                return await createOrUpgrade(1);
            }

            dbInstance = await new Promise((resolve, reject) => {
                const request = indexedDB.open(DB_NAME, currentVersion);
                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject(request.error);
                request.onblocked = () => reject(new Error('Database blocked'));
            });

            const missingStores = Object.keys(registeredStores).filter(
                name => !dbInstance.objectStoreNames.contains(name)
            );

            if (missingStores.length > 0) {
                dbInstance.close();
                dbInstance = null;
                const newVersion = currentVersion + 1;
                return await createOrUpgrade(newVersion);
            }

            return dbInstance;
        })();

        return openPromise;
    }

    function createOrUpgrade(version) {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, version);

            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                for (const [name, config] of Object.entries(registeredStores)) {
                    if (!db.objectStoreNames.contains(name)) {
                        db.createObjectStore(name, { keyPath: config.keyPath });
                        console.log(`🆕 Создано хранилище "${name}"`);
                    }
                }
            };

            request.onsuccess = () => {
                dbInstance = request.result;
                console.log('✅ IndexedDB открыта (версия ' + version + ')');
                resolve(dbInstance);
            };

            request.onerror = () => reject(request.error);
            request.onblocked = () => reject(new Error('Database blocked'));
        });
    }

    async function getStore(storeName, mode = 'readonly') {
        const db = await openDB();
        const tx = db.transaction(storeName, mode);
        return tx.objectStore(storeName);
    }

    async function dbGet(storeName, key) {
        const store = await getStore(storeName);
        return new Promise((resolve, reject) => {
            const req = store.get(key);
            req.onsuccess = () => resolve(req.result ? req.result.value : undefined);
            req.onerror = () => reject(req.error);
        });
    }

    async function dbSet(storeName, key, value) {
        const store = await getStore(storeName, 'readwrite');
        return new Promise((resolve, reject) => {
            const req = store.put({ key, value });
            req.onsuccess = () => resolve();
            req.onerror = () => reject(req.error);
        });
    }

    async function dbDelete(storeName, key) {
        const store = await getStore(storeName, 'readwrite');
        return new Promise((resolve, reject) => {
            const req = store.delete(key);
            req.onsuccess = () => resolve();
            req.onerror = () => reject(req.error);
        });
    }

    async function dbGetAllKeys(storeName) {
        const store = await getStore(storeName);
        return new Promise((resolve, reject) => {
            const req = store.getAllKeys();
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }

    async function dbGetAll(storeName) {
        const store = await getStore(storeName);
        return new Promise((resolve, reject) => {
            const req = store.getAll();
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }

    async function getDispatcherData(key) { return await dbGet('dispatcher', key); }
    async function setDispatcherData(key, value) { await dbSet('dispatcher', key, value); }
    async function removeDispatcherData(key) { await dbDelete('dispatcher', key); }

    async function getChatData(chatId) { return await dbGet('chats_data', chatId); }
    async function setChatData(chatId, value) { await dbSet('chats_data', chatId, value); }
    async function removeChatData(chatId) { await dbDelete('chats_data', chatId); }

    async function updateChatField(chatId, field, value) {
        const record = await getChatData(chatId);
        if (!record) throw new Error('Chat not found');
        record[field] = value;
        await setChatData(chatId, record);
    }

    async function getModuleSettings(chatId, moduleId) {
        const record = await getChatData(chatId);
        if (!record || !record.moduleSettings) return {};
        return record.moduleSettings[moduleId] || {};
    }

    async function setModuleSettings(chatId, moduleId, settings) {
        const record = await getChatData(chatId);
        if (!record) throw new Error('Chat not found');
        if (!record.moduleSettings) record.moduleSettings = {};
        record.moduleSettings[moduleId] = settings;
        await setChatData(chatId, record);
    }

    window.__DB__ = {
        registerStore,
        openDB,
        getDispatcherData,
        setDispatcherData,
        removeDispatcherData,
        getChatData,
        setChatData,
        removeChatData,
        getModuleSettings,
        setModuleSettings,
        updateChatField,
        dbGet,
        dbSet,
        dbDelete,
        dbGetAllKeys,
        dbGetAll,
        getStore
    };

    // ============================================================
    // 1. ЛОКАЛИЗАЦИЯ
    // ============================================================
    const L10N = {
        currentLang: (navigator.language || 'en').startsWith('ru') ? 'ru' : 'en',

        strings: {
            'dispatcher_title': { ru: 'Диспетчер модулей', en: 'Module Dispatcher' },
            'dispatcher_help': {
                ru: '📦 Каждый модуль можно включить или выключить чекбоксом.\n▶ Нажмите на название модуля, чтобы раскрыть его настройки.\n📋 Кнопка «Чаты» показывает все сохранённые чаты и позволяет быстро переключаться между ними.\n🌎 Переключает язык интерфейса.\n🔄 Обновить страницу.\n▶️ Минимизировать панель Диспетчера.',
                en: '📦 Each module can be turned on/off with the checkbox.\n▶ Click on the module name to expand its settings.\n📋 The «Chats» button shows all saved chats and allows quick switching.\n🌎 Switches the interface language.\n🔄 Reload page.\n▶️ Minimize the Dispatcher panel.'
            },
            'no_modules': { ru: 'Нет активных модулей', en: 'No active modules' },
            'collapse_all': { ru: 'Свернуть всё', en: 'Collapse all' },
            'lang_switch': { ru: '🌎 EN', en: '🌍 RU' },
            'log_title': { ru: '📋 Лог ошибок', en: '📋 Error log' },
            'log_clear': { ru: 'Очистить', en: 'Clear' },
            'log_empty': { ru: 'Лог пуст', en: 'Log is empty' },

            'chats_title': { ru: '📋 Чаты', en: '📋 Chats' },
            'chats_tooltip': { ru: 'Управление чатами', en: 'Chat management' },
            'chats_help': {
                ru: '📋 Здесь показаны все чаты, в которых вы были с активным диспетчером.\n🗑️ Кнопка «Очистить все» удаляет все сохранения чатов.\n🗑️ Корзина у чата удаляет только этот чат.\n🔗 Нажмите на имя чата, чтобы перейти в него.\n⚠️ Удаление чата здесь не удаляет его с сайта - а лишь удаляет из БД настройки модулей, привязаные к этому чату.',
                en: '📋 Here are all chats you visited with the dispatcher active.\n🗑️ «Clear all» button deletes all chat saves.\n🗑️ The trash icon deletes only that chat.\n🔗 Click on chat name to navigate to it.\n⚠️ Deleting a chat here does not delete it from the site - it only deletes the module settings associated with this chat from the database.'
            },
            'lang_tooltip': { ru: 'Сменить язык', en: 'Switch language' },
            'collapse_tooltip': { ru: 'Свернуть всё', en: 'Collapse all' },
            'reload_tooltip': { ru: 'Обновить страницу', en: 'Reload page' },

            'chats_hint': { ru: 'Удалить настройки чатов в Диспетчере модулей. Это НЕ удаляет чат с сайта. Используйте, чтобы очистить память от несуществующих чатов, которые вы удалили на сайте.', en: 'Delete chat settings in Module Dispatcher. This does NOT delete the chat from the site. Use to clear memory of non-existent chats you have deleted on the site.' },
            'chats_empty': { ru: 'Нет сохранённых чатов', en: 'No saved chats' },
            'chats_delete_all': { ru: 'Очистить все', en: 'Clear all' },
            'chats_delete_confirm': { ru: 'Удалить все сохранения чатов?', en: 'Delete all chat saves?' },
            'chats_delete_single': { ru: 'Удалить', en: 'Delete' },
            'chats_created': { ru: 'Создан', en: 'Created' },
            'chats_last_used': { ru: 'Последний вход', en: 'Last used' },
            'chats_modules_active': { ru: 'Активные модули', en: 'Active modules' },
            'chats_unknown': { ru: 'Без названия', en: 'Untitled' },
            'chats_participants': { ru: 'Участники', en: 'Participants' },
            'chats_no_participants': { ru: '—', en: '—' },
            'dialog_cancel': { ru: 'Закрыть', en: 'Close' },
            'dialog_ok': { ru: 'OK', en: 'OK' },
            'confirm_delete_chat': { ru: 'Удалить чат «{name}»?', en: 'Delete chat «{name}»?' },
            'confirm_delete_all_chats': { ru: 'Удалить все сохранения чатов?', en: 'Delete all chat saves?' },
        },

        t(key, vars) {
            const lang = this.currentLang;
            let str = this.strings[key]?.[lang] || this.strings[key]?.en || key;
            if (vars) str = str.replace(/{([^}]+)}/g, (_, p) => vars[p] !== undefined ? vars[p] : '');
            return str;
        },

        async setLang(lang) {
            if (lang === 'ru' || lang === 'en') {
                this.currentLang = lang;
                await setDispatcherData('lang', lang);
                if (window.__MANAGER__ && window.__MANAGER__._updateUI) {
                    window.__MANAGER__._updateUI();
                }
                window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
            }
        }
    };

    window.__L10N__ = L10N;

    // ============================================================
    // 2. ДИСПЕТЧЕР
    // ============================================================
    (function setupDispatcher() {
        if (window.__MANAGER__) return;

        const modules = {};
        let moduleOrder = [];
        let logEntries = [];
        let panelElement = null;
        let containerElement = null;
        let logContent = null;
        let logIndicator = null;
        let logExpanded = false;

        let chatData = {};
        let currentChatId = null;
        let lastCheckedUrl = '';
        let metadataInterval = null;

        let panelCollapsed = false;
        let toggleButton = null;

        const hooks = {};

        // --- Вспомогательный диалог подтверждения ---
        function showConfirmDialog(title, message, zIndex = 10000010) {
            return new Promise(resolve => {
                const overlay = document.createElement('div');
                overlay.style.cssText = `
                    position: fixed;
                    top: 0; left: 0; width: 100%; height: 100%;
                    background: rgba(0,0,0,0.6);
                    backdrop-filter: blur(4px);
                    z-index: ${zIndex};
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    font-family: sans-serif;
                `;

                const box = document.createElement('div');
                box.style.cssText = `
                    background: #1e1e2e;
                    padding: 20px;
                    border-radius: 16px;
                    border: 2px solid #cba6f7;
                    max-width: 380px;
                    width: 90%;
                    color: #cdd6f4;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.9);
                    text-align: center;
                `;

                if (title) {
                    const h = document.createElement('h3');
                    h.style.cssText = 'margin:0 0 12px 0; color:#cba6f7; font-size:18px;';
                    h.textContent = title;
                    box.appendChild(h);
                }
                if (message) {
                    const p = document.createElement('div');
                    p.style.cssText = 'margin-bottom:16px; font-size:14px; line-height:1.4;';
                    p.textContent = message;
                    box.appendChild(p);
                }

                const btnRow = document.createElement('div');
                btnRow.style.cssText = 'display:flex; justify-content:center; gap:12px;';
                const okBtn = document.createElement('button');
                okBtn.textContent = L10N.t('dialog_ok');
                okBtn.style.cssText = 'background:#a6e3a1;color:#111;border:none;border-radius:6px;padding:6px 24px;font-weight:bold;cursor:pointer;';
                okBtn.addEventListener('click', () => { overlay.remove(); resolve(true); });
                const cancelBtn = document.createElement('button');
                cancelBtn.textContent = L10N.t('dialog_cancel');
                cancelBtn.style.cssText = 'background:#f38ba8;color:#111;border:none;border-radius:6px;padding:6px 24px;font-weight:bold;cursor:pointer;';
                cancelBtn.addEventListener('click', () => { overlay.remove(); resolve(false); });
                btnRow.appendChild(cancelBtn);
                btnRow.appendChild(okBtn);
                box.appendChild(btnRow);

                overlay.appendChild(box);
                document.body.appendChild(overlay);
                overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); resolve(false); } });
            });
        }

        // --- Работа с чатами ---
        function getChatIdFromURL() {
            const m = location.pathname.match(/\/chats\/(\d+)/);
            return m ? m[1] : null;
        }

        function getChatNameFromPage() {
            const title = document.title || '';
            const name = title.replace(/\s*[-|–]\s*JanitorAI\s*$/i, '').trim();
            if (!document.querySelector('textarea._chatTextarea_1e2lg_1')) return null;
            return name || null;
        }

function getParticipantsFromReact() {
    const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    if (!hook) return [];

    const renderer = Array.from(hook.renderers.values())[0];
    if (!renderer) return [];

    const textarea = document.querySelector('textarea._chatTextarea_1e2lg_1');
    if (!textarea) return [];

    let fiber = renderer.findFiberByHostInstance(textarea);
    while (fiber) {
        const props = fiber.memoizedProps;
        if (props && props.chatStore && props.chatStore.chatInfo) {
            const chatInfo = props.chatStore.chatInfo;
            const participants = new Set();

            const characterName = chatInfo.character?.chat_name;
            if (characterName) participants.add(characterName);

            const activePersona = props.chatStore.activePersona;
            let activePersonaName = null;

            if (activePersona) {
                const activeId = typeof activePersona === 'object' ? activePersona.id : activePersona;
                const found = chatInfo.personas?.find(p => p.id === activeId);
                if (found) activePersonaName = found.name;
            } else {
                const defaultPersona = chatInfo.personas?.find(p => p.is_default === true);
                if (defaultPersona) activePersonaName = defaultPersona.name;
            }

            if (activePersonaName) participants.add(activePersonaName);

            return Array.from(participants);
        }
        fiber = fiber.return;
    }

    return [];
}

        async function loadChatData() {
            try {
                const allRecords = await dbGetAll('chats_data');
                chatData = {};
                for (const record of allRecords) {
                    chatData[record.key] = record.value;
                }
            } catch (e) {
                console.error('Failed to load chat data:', e);
                chatData = {};
            }
        }

        async function ensureChatExists(chatId) {
            if (!chatId) return null;
            const stored = await getChatData(chatId);
            if (stored) {
                chatData[chatId] = stored;
                return stored;
            }
            const newChat = {
                name: '',
                participants: [],
                url: '',
                createdAt: Date.now(),
                lastUsed: Date.now(),
                modules: []
            };
            await setChatData(chatId, newChat);
            chatData[chatId] = newChat;
            return newChat;
        }

        async function fillChatMetadata(chatId) {
            const record = chatData[chatId];
            if (!record) return;

            let changed = false;
            const name = getChatNameFromPage();
            const participants = getParticipantsFromReact();

            if (name && (!record.name || record.name === L10N.t('chats_unknown'))) {
                record.name = name;
                changed = true;
            }
            if (participants.length > 0 && (!record.participants || record.participants.length === 0)) {
                record.participants = participants;
                changed = true;
            }
            record.url = location.href;
            record.lastUsed = Date.now();

            if (changed || !record.url) {
                await updateChatField(chatId, 'name', record.name);
                await updateChatField(chatId, 'participants', record.participants);
            }
            await updateChatField(chatId, 'url', record.url);
            await updateChatField(chatId, 'lastUsed', record.lastUsed);
        }

        function startMetadataPolling(chatId) {
            if (metadataInterval) clearInterval(metadataInterval);
            metadataInterval = setInterval(async () => {
                const currentId = getChatIdFromURL();
                if (currentId !== chatId) {
                    clearInterval(metadataInterval);
                    metadataInterval = null;
                    return;
                }
                const record = chatData[chatId];
                if (!record) return;
                if (record.name && record.name !== L10N.t('chats_unknown') && record.participants && record.participants.length > 0) {
                    clearInterval(metadataInterval);
                    metadataInterval = null;
                    return;
                }
                await fillChatMetadata(chatId);
            }, 500);
        }

        async function syncAllModules(chatId) {
            if (!chatId || !chatData[chatId]) {
                for (const id of moduleOrder) {
                    await setModuleEnabled(id, false, false, chatId);
                }
                return;
            }
            const enabledModules = chatData[chatId].modules || [];
            for (const id of moduleOrder) {
                const isEnabled = enabledModules.includes(id);
                await setModuleEnabled(id, isEnabled, false, chatId);
            }
        }

        async function setModuleEnabled(moduleId, enabled, save = true, chatId = currentChatId) {
            const mod = modules[moduleId];
            if (!mod || mod._pendingDeps) return;

            mod._enabled = enabled;

            const tile = mod.element;
            if (tile) {
                const checkbox = tile.querySelector('.module-checkbox');
                if (checkbox) checkbox.checked = enabled;
                tile.style.opacity = enabled ? '1' : '0.5';
                if (!enabled && mod.type === 'interface' && mod.expanded) {
                    toggleExpand(moduleId, true);
                }
            }

            if (enabled) {
                if (mod.onEnable) mod.onEnable(chatId);
            } else {
                if (mod.onDisable) mod.onDisable(chatId);
            }

            if (save && chatId && chatData[chatId]) {
                const record = chatData[chatId];
                if (enabled) {
                    if (!record.modules.includes(moduleId)) {
                        record.modules.push(moduleId);
                    }
                } else {
                    record.modules = record.modules.filter(id => id !== moduleId);
                }
                await updateChatField(chatId, 'modules', record.modules);
            }
        }

        function getCurrentChatId() {
            return currentChatId;
        }

        async function handleUrlChange() {
            const newChatId = getChatIdFromURL();
            if (newChatId !== currentChatId) {
                if (metadataInterval) {
                    clearInterval(metadataInterval);
                    metadataInterval = null;
                }
                await syncAllModules(currentChatId);
                currentChatId = newChatId;
                await runHook('chatChanged', currentChatId);
            }
            if (currentChatId) {
                await ensureChatExists(currentChatId);
                await fillChatMetadata(currentChatId);
                startMetadataPolling(currentChatId);
            }
            if (panelElement) {
                panelElement.style.display = currentChatId ? (panelCollapsed ? 'none' : 'flex') : 'none';
                if (toggleButton) {
                    toggleButton.style.display = currentChatId && panelCollapsed ? 'flex' : 'none';
                }
            }
            await syncAllModules(currentChatId);
        }

        function createPanel() {
            if (panelElement) return;

            const panel = document.createElement('div');
            panel.id = 'dispatcher-panel';
            panel.style.cssText = `
                position: fixed;
                top: 60px;
                right: 10px;
                width: 340px;
                max-height: calc(100vh - 70px);
                background: #181825;
                color: #cdd6f4;
                border: 2px solid #cba6f7;
                border-radius: 8px;
                padding: 0;
                z-index: 99999;
                font-family: sans-serif;
                font-size: 13px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.5);
                display: none;
                flex-direction: column;
                overflow: hidden;
                user-select: none;
            `;

            const header = document.createElement('div');
            header.className = 'dispatcher-header';
            header.style.cssText = `
                padding: 6px 10px;
                background: #1e1e2e;
                border-bottom: 1px solid #45475a;
                cursor: grab;
                flex-shrink: 0;
                font-weight: bold;
                font-size: 15px;
                color: #cba6f7;
                text-align: center;
                display: flex;
                align-items: center;
                justify-content: space-between;
            `;

            const helpBtn = document.createElement('button');
            helpBtn.textContent = '❔';
            helpBtn.title = L10N.t('dispatcher_help');
            helpBtn.style.cssText = `
                width: 32px; height: 32px; border-radius: 50%; border: 1px solid #45475a;
                background: #313244; color: #cdd6f4; font-size: 18px;
                cursor: pointer; display: flex; align-items: center; justify-content: center;
                transition: 0.15s; flex-shrink: 0;
            `;
            helpBtn.addEventListener('mouseenter', () => { helpBtn.style.background = '#45475a'; });
            helpBtn.addEventListener('mouseleave', () => { helpBtn.style.background = '#313244'; });
            helpBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                showDispatcherTooltip(helpBtn);
            });

            const headerTitle = document.createElement('span');
            headerTitle.className = 'dispatcher-header-title';
            headerTitle.textContent = L10N.t('dispatcher_title');
            headerTitle.style.cssText = 'flex:1; text-align:center;';

            const collapseBtn = document.createElement('button');
            collapseBtn.textContent = '▶️';
            collapseBtn.title = L10N.t('collapse_tooltip');
            collapseBtn.style.cssText = `
                width: 32px; height: 32px; border-radius: 50%; border: 1px solid #45475a;
                background: #313244; color: #cdd6f4; font-size: 18px;
                cursor: pointer; display: flex; align-items: center; justify-content: center;
                transition: 0.15s; flex-shrink: 0;
            `;
            collapseBtn.addEventListener('mouseenter', () => { collapseBtn.style.background = '#45475a'; });
            collapseBtn.addEventListener('mouseleave', () => { collapseBtn.style.background = '#313244'; });
            collapseBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                togglePanel();
            });

            header.appendChild(helpBtn);
            header.appendChild(headerTitle);
            header.appendChild(collapseBtn);
            panel.appendChild(header);

            const toolbar = document.createElement('div');
            toolbar.className = 'dispatcher-toolbar';
            toolbar.style.cssText = `
                display: flex;
                justify-content: flex-end;
                align-items: center;
                padding: 4px 10px;
                background: #181825;
                border-bottom: 1px solid #313244;
                flex-shrink: 0;
                gap: 6px;
            `;

            const reloadBtn = document.createElement('button');
            reloadBtn.id = 'dispatcher-reload-btn';
            reloadBtn.textContent = '🔄';
            reloadBtn.title = L10N.t('reload_tooltip');
            reloadBtn.style.cssText = `
                background: #313244;
                border: 1px solid #45475a;
                border-radius: 4px;
                color: #cdd6f4;
                cursor: pointer;
                font-size: 15px;
                padding: 4px 8px;
                transition: background 0.15s, border-color 0.15s;
            `;
            reloadBtn.addEventListener('mouseenter', () => { reloadBtn.style.background = '#45475a'; reloadBtn.style.borderColor = '#89b4fa'; });
            reloadBtn.addEventListener('mouseleave', () => { reloadBtn.style.background = '#313244'; reloadBtn.style.borderColor = '#45475a'; });
            reloadBtn.addEventListener('click', () => location.reload());

            const chatsBtn = document.createElement('button');
            chatsBtn.id = 'dispatcher-chats-btn';
            chatsBtn.textContent = L10N.t('chats_title');
            chatsBtn.title = L10N.t('chats_tooltip');
            chatsBtn.style.cssText = `
                background: #313244;
                border: 1px solid #45475a;
                border-radius: 4px;
                color: #cdd6f4;
                cursor: pointer;
                font-size: 13px;
                padding: 4px 8px;
                font-weight: 500;
                transition: background 0.15s, border-color 0.15s;
            `;
            chatsBtn.addEventListener('mouseenter', () => { chatsBtn.style.background = '#45475a'; chatsBtn.style.borderColor = '#89b4fa'; });
            chatsBtn.addEventListener('mouseleave', () => { chatsBtn.style.background = '#313244'; chatsBtn.style.borderColor = '#45475a'; });
            chatsBtn.addEventListener('click', () => openChatsModal());

            const langBtn = document.createElement('button');
            langBtn.id = 'dispatcher-lang-btn';
            langBtn.textContent = L10N.t('lang_switch');
            langBtn.title = L10N.t('lang_tooltip');
            langBtn.style.cssText = `
                background: #313244;
                border: 1px solid #45475a;
                border-radius: 4px;
                color: #cdd6f4;
                cursor: pointer;
                font-size: 13px;
                padding: 4px 8px;
                font-weight: 500;
                transition: background 0.15s, border-color 0.15s;
            `;
            langBtn.addEventListener('mouseenter', () => { langBtn.style.background = '#45475a'; langBtn.style.borderColor = '#89b4fa'; });
            langBtn.addEventListener('mouseleave', () => { langBtn.style.background = '#313244'; langBtn.style.borderColor = '#45475a'; });
            langBtn.addEventListener('click', () => {
                const newLang = L10N.currentLang === 'ru' ? 'en' : 'ru';
                L10N.setLang(newLang);
            });

            const collapseAllBtn = document.createElement('button');
            collapseAllBtn.id = 'dispatcher-collapse-all';
            collapseAllBtn.textContent = '▼';
            collapseAllBtn.title = L10N.t('collapse_tooltip');
            collapseAllBtn.style.cssText = `
                background: #313244;
                border: 1px solid #45475a;
                border-radius: 4px;
                color: #cdd6f4;
                cursor: pointer;
                font-size: 14px;
                padding: 4px 8px;
                transition: background 0.15s, border-color 0.15s;
            `;
            collapseAllBtn.addEventListener('mouseenter', () => { collapseAllBtn.style.background = '#45475a'; collapseAllBtn.style.borderColor = '#89b4fa'; });
            collapseAllBtn.addEventListener('mouseleave', () => { collapseAllBtn.style.background = '#313244'; collapseAllBtn.style.borderColor = '#45475a'; });
            collapseAllBtn.addEventListener('click', () => collapseAll());

            toolbar.appendChild(reloadBtn);
            toolbar.appendChild(chatsBtn);
            toolbar.appendChild(langBtn);
            toolbar.appendChild(collapseAllBtn);
            panel.appendChild(toolbar);

            const container = document.createElement('div');
            container.id = 'dispatcher-modules';
            container.style.cssText = `
                overflow-y: auto;
                flex: 1;
                padding: 4px 0;
                border-top: 1px solid #313244;
            `;
            panel.appendChild(container);

            const emptyMsg = document.createElement('div');
            emptyMsg.id = 'dispatcher-empty';
            emptyMsg.style.cssText = `padding: 20px; text-align: center; color: #a6adc8; font-style: italic;`;
            emptyMsg.textContent = L10N.t('no_modules');
            container.appendChild(emptyMsg);

            const logWrapper = document.createElement('div');
            logWrapper.id = 'dispatcher-log';
            logWrapper.style.cssText = `border-top: 1px solid #45475a; flex-shrink: 0;`;

            const logHeader = document.createElement('div');
            logHeader.className = 'dispatcher-log-header';
            logHeader.style.cssText = `
                padding: 6px 12px;
                cursor: pointer;
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: #181825;
                transition: background 0.15s;
                user-select: none;
            `;
            logHeader.addEventListener('mouseenter', () => logHeader.style.background = '#313244');
            logHeader.addEventListener('mouseleave', () => logHeader.style.background = '#181825');

            const logTitle = document.createElement('span');
            logTitle.style.cssText = `font-weight: bold; color: #a6adc8;`;
            logTitle.textContent = L10N.t('log_title');

            const logControls = document.createElement('span');
            logControls.style.cssText = `display: flex; align-items: center; gap: 6px;`;

            logIndicator = document.createElement('span');
            logIndicator.style.cssText = `
                background: #f38ba8;
                color: #111;
                border-radius: 50%;
                padding: 1px 6px;
                font-size: 11px;
                font-weight: bold;
                display: none;
            `;
            logIndicator.textContent = '0';

            const logExpandIcon = document.createElement('span');
            logExpandIcon.id = 'dispatcher-log-icon';
            logExpandIcon.textContent = '▶';
            logExpandIcon.style.cssText = `font-size: 12px; color: #a6adc8;`;

            logControls.appendChild(logIndicator);
            logControls.appendChild(logExpandIcon);
            logHeader.appendChild(logTitle);
            logHeader.appendChild(logControls);
            logWrapper.appendChild(logHeader);

            logContent = document.createElement('div');
            logContent.id = 'dispatcher-log-content';
            logContent.style.cssText = `
                padding: 4px 12px 8px 12px;
                max-height: 150px;
                overflow-y: auto;
                display: none;
                background: #11111b;
                border-top: 1px solid #313244;
                font-size: 12px;
                line-height: 1.4;
            `;
            const emptyLogMsg = document.createElement('div');
            emptyLogMsg.id = 'dispatcher-log-empty';
            emptyLogMsg.style.cssText = `color: #a6adc8; font-style: italic; padding: 4px 0;`;
            emptyLogMsg.textContent = L10N.t('log_empty');
            logContent.appendChild(emptyLogMsg);

            const clearBtn = document.createElement('button');
            clearBtn.id = 'dispatcher-log-clear';
            clearBtn.textContent = L10N.t('log_clear');
            clearBtn.style.cssText = `
                background: #f38ba8;
                color: #111;
                border: none;
                border-radius: 4px;
                padding: 2px 10px;
                font-weight: bold;
                cursor: pointer;
                font-size: 11px;
                margin-top: 4px;
                display: none;
            `;
            clearBtn.addEventListener('click', () => { logEntries = []; updateLogUI(); });
            logContent.appendChild(clearBtn);
            logWrapper.appendChild(logContent);
            panel.appendChild(logWrapper);

            document.body.appendChild(panel);
            panelElement = panel;
            containerElement = container;

            toggleButton = document.createElement('button');
            toggleButton.textContent = '◀️';
            toggleButton.style.cssText = `
                position: fixed;
                top: 60px;
                right: 10px;
                width: 40px; height: 40px;
                border-radius: 50%;
                background: #313244;
                border: 2px solid #cba6f7;
                color: #cdd6f4;
                font-size: 20px;
                cursor: pointer;
                z-index: 100000;
                display: none;
                align-items: center; justify-content: center;
                transition: 0.15s;
            `;
            toggleButton.addEventListener('mouseenter', () => { toggleButton.style.background = '#45475a'; });
            toggleButton.addEventListener('mouseleave', () => { toggleButton.style.background = '#313244'; });
            toggleButton.addEventListener('click', () => togglePanel());
            document.body.appendChild(toggleButton);

            rebuildList();

            let isDragging = false;
            let dragOffsetX = 0, dragOffsetY = 0;
            header.addEventListener('mousedown', function(e) {
                if (e.target.closest('button')) return;
                isDragging = true;
                const rect = panel.getBoundingClientRect();
                dragOffsetX = e.clientX - rect.left;
                dragOffsetY = e.clientY - rect.top;
                panel.style.cursor = 'grabbing';
                document.addEventListener('mousemove', onDrag);
                document.addEventListener('mouseup', onDragEnd);
                e.preventDefault();
            });

            function onDrag(e) {
                if (!isDragging) return;
                let left = e.clientX - dragOffsetX;
                let top = e.clientY - dragOffsetY;
                const maxX = window.innerWidth - panel.offsetWidth;
                const maxY = window.innerHeight - panel.offsetHeight;
                left = Math.max(0, Math.min(left, maxX));
                top = Math.max(0, Math.min(top, maxY));
                panel.style.left = left + 'px';
                panel.style.top = top + 'px';
                panel.style.right = 'auto';
            }

            function onDragEnd() {
                isDragging = false;
                panel.style.cursor = '';
                document.removeEventListener('mousemove', onDrag);
                document.removeEventListener('mouseup', onDragEnd);
            }

            logHeader.addEventListener('click', () => toggleLog());

            function togglePanel() {
                panelCollapsed = !panelCollapsed;
                if (panelCollapsed) {
                    panel.style.display = 'none';
                    toggleButton.style.display = 'flex';
                } else {
                    panel.style.display = currentChatId ? 'flex' : 'none';
                    toggleButton.style.display = 'none';
                }
            }

function showDispatcherTooltip(anchor) {
    // Удаляем предыдущий тултип
    const old = document.getElementById('dispatcher-tooltip');
    if (old) old.remove();

    const panelRect = panelElement.getBoundingClientRect();
    const tooltip = document.createElement('div');
    tooltip.id = 'dispatcher-tooltip';
    tooltip.textContent = L10N.t('dispatcher_help');
    tooltip.style.cssText = `
        position: fixed;
        top: ${panelRect.top + 40}px;
        left: ${panelRect.left + 10}px;
        width: ${panelRect.width - 20}px;
        max-height: 200px;
        overflow-y: auto;
        background: #1e1e2e;
        border: 1px solid #45475a;
        border-radius: 6px;
        padding: 10px;
        color: #cdd6f4;
        font-size: 13px;
        line-height: 1.5;
        z-index: 10000020;
        box-shadow: 0 4px 12px rgba(0,0,0,0.6);
        white-space: pre-line;
    `;
    document.body.appendChild(tooltip);

    // Закрытие по клику вне тултипа
    setTimeout(() => {
        document.addEventListener('click', function handler(e) {
            if (!tooltip.contains(e.target) && e.target !== anchor) {
                tooltip.remove();
                document.removeEventListener('click', handler);
            }
        });
    }, 0);
}
        }

        // --- Менеджер чатов (с кастомными подтверждениями и тултипом) ---
        async function openChatsModal() {
            if (currentChatId) await fillChatMetadata(currentChatId);

            const overlay = document.createElement('div');
            overlay.style.cssText = `
                position: fixed;
                top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0,0,0,0.6);
                backdrop-filter: blur(4px);
                z-index: 10000010;
                display: flex;
                justify-content: center;
                align-items: center;
                font-family: sans-serif;
            `;

            const modal = document.createElement('div');
            modal.style.cssText = `
                background: #1e1e2e;
                padding: 10px;
                border-radius: 16px;
                border: 2px solid #cba6f7;
                max-width: 600px;
                width: 95%;
                max-height: 95vh;
                color: #cdd6f4;
                box-shadow: 0 8px 32px rgba(0,0,0,0.9);
                display: flex;
                flex-direction: column;
                position: relative;
            `;

            // Заголовок с тултипом
            const titleRow = document.createElement('div');
            titleRow.style.cssText = 'display:flex; align-items:center; justify-content:center; margin-bottom:8px;';

            const title = document.createElement('h2');
            title.textContent = L10N.t('chats_title');
            title.style.cssText = `margin: 0; color: #cba6f7; font-size: 20px;`;

            const chatsHelpBtn = document.createElement('button');
            chatsHelpBtn.textContent = '❔';
            chatsHelpBtn.title = L10N.t('chats_help');
            chatsHelpBtn.style.cssText = `
                width: 28px; height: 28px; border-radius: 50%; border: 1px solid #45475a;
                background: #313244; color: #cdd6f4; font-size: 16px;
                cursor: pointer; display: flex; align-items: center; justify-content: center;
                margin-left: 8px; transition: 0.15s;
            `;
            chatsHelpBtn.addEventListener('mouseenter', () => { chatsHelpBtn.style.background = '#45475a'; });
            chatsHelpBtn.addEventListener('mouseleave', () => { chatsHelpBtn.style.background = '#313244'; });
            chatsHelpBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                // Показываем тултип внутри модального окна
                const old = document.getElementById('chats-help-tooltip');
                if (old) { old.remove(); return; }
                const tooltip = document.createElement('div');
                tooltip.id = 'chats-help-tooltip';
                tooltip.textContent = L10N.t('chats_help');
                tooltip.style.cssText = `
                    position: absolute;
                    top: 50px;
                    left: 10px;
                    right: 10px;
                    background: #1e1e2e;
                    border: 1px solid #45475a;
                    border-radius: 6px;
                    padding: 10px;
                    color: #cdd6f4;
                    font-size: 13px;
                    line-height: 1.5;
                    z-index: 10000011;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.6);
                    white-space: pre-line;
                `;
                modal.appendChild(tooltip);
                setTimeout(() => {
                    document.addEventListener('click', function handler(ev) {
                        if (!tooltip.contains(ev.target) && ev.target !== chatsHelpBtn) {
                            tooltip.remove();
                            document.removeEventListener('click', handler);
                        }
                    });
                }, 0);
            });

            titleRow.appendChild(title);
            titleRow.appendChild(chatsHelpBtn);
            modal.appendChild(titleRow);

            const listContainer = document.createElement('div');
            listContainer.style.cssText = `overflow-y: auto; flex: 1; margin-bottom: 12px; padding-right: 4px;`;

            const emptyMsg = document.createElement('div');
            emptyMsg.textContent = L10N.t('chats_empty');
            emptyMsg.style.cssText = `color: #a6adc8; text-align: center; padding: 20px; font-style: italic;`;

            const btnContainer = document.createElement('div');
            btnContainer.style.cssText = `display: flex; justify-content: center; gap: 8px; margin-top: 8px;`;

            const deleteAllBtn = document.createElement('button');
            deleteAllBtn.textContent = L10N.t('chats_delete_all');
            deleteAllBtn.style.cssText = `
                background: #f38ba8;
                color: #111;
                border: none;
                border-radius: 6px;
                padding: 6px 16px;
                font-weight: bold;
                cursor: pointer;
            `;
            deleteAllBtn.addEventListener('click', async () => {
                const confirmed = await showConfirmDialog(
                    L10N.t('chats_delete_confirm'),
                    L10N.t('confirm_delete_all_chats')
                );
                if (!confirmed) return;
                const keys = await dbGetAllKeys('chats_data');
                for (const key of keys) await removeChatData(key);
                chatData = {};
                renderChatsList(listContainer, emptyMsg);
            });

            const closeBtn = document.createElement('button');
            closeBtn.textContent = L10N.t('dialog_cancel');
            closeBtn.style.cssText = `
                background: #45475a;
                color: #cdd6f4;
                border: none;
                border-radius: 6px;
                padding: 6px 16px;
                font-weight: bold;
                cursor: pointer;
            `;
            closeBtn.addEventListener('click', () => overlay.remove());

            btnContainer.appendChild(deleteAllBtn);
            btnContainer.appendChild(closeBtn);

            function renderChatsList(container, emptyEl) {
                container.innerHTML = '';
                const ids = Object.keys(chatData);
                if (ids.length === 0) {
                    container.appendChild(emptyEl);
                    return;
                }
                ids.sort((a, b) => (chatData[b]?.lastUsed || 0) - (chatData[a]?.lastUsed || 0));

                for (const id of ids) {
                    const data = chatData[id];
                    const card = document.createElement('div');
                    card.style.cssText = `
                        background: #181825;
                        border: 1px solid #313244;
                        border-radius: 8px;
                        padding: 10px 14px;
                        margin-bottom: 8px;
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        gap: 12px;
                    `;

                    const info = document.createElement('div');
                    info.style.cssText = `flex: 1; min-width: 0;`;

                    const nameLink = document.createElement('a');
                    nameLink.href = data.url || '#';
                    nameLink.style.cssText = `
                        font-weight: bold;
                        font-size: 14px;
                        color: #89b4fa;
                        text-decoration: none;
                        display: inline-block;
                    `;
                    nameLink.textContent = data.name || L10N.t('chats_unknown');
                    nameLink.addEventListener('mouseenter', () => nameLink.style.textDecoration = 'underline');
                    nameLink.addEventListener('mouseleave', () => nameLink.style.textDecoration = 'none');

                    const nameLine = document.createElement('div');
                    nameLine.style.cssText = `margin-bottom: 2px;`;
                    nameLine.appendChild(nameLink);

                    const participants = data.participants?.length > 0 ? data.participants.join(', ') : L10N.t('chats_no_participants');
                    const participantsLine = document.createElement('div');
                    participantsLine.style.cssText = `font-size: 12px; color: #a6adc8;`;
                    participantsLine.textContent = `${L10N.t('chats_participants')}: ${participants}`;

                    const created = new Date(data.createdAt).toLocaleString();
                    const lastUsed = new Date(data.lastUsed).toLocaleString();
                    const metaLine = document.createElement('div');
                    metaLine.style.cssText = `font-size: 11px; color: #a6adc8; margin-top: 2px;`;
                    metaLine.textContent = `${L10N.t('chats_created')}: ${created} | ${L10N.t('chats_last_used')}: ${lastUsed}`;

                    const modulesList = document.createElement('div');
                    modulesList.style.cssText = `font-size: 12px; color: #89b4fa; margin-top: 4px;`;
                    const activeModules = data.modules || [];
                    if (activeModules.length > 0) {
                        const names = activeModules.map(mid => modules[mid]?.title || mid).join(', ');
                        modulesList.textContent = `${L10N.t('chats_modules_active')}: ${names}`;
                    } else {
                        modulesList.textContent = `${L10N.t('chats_modules_active')}: —`;
                    }

                    info.appendChild(nameLine);
                    info.appendChild(participantsLine);
                    info.appendChild(metaLine);
                    info.appendChild(modulesList);

                    // Кнопка удаления чата (корзина)
                    const delBtn = document.createElement('button');
                    delBtn.textContent = '🗑️';
                    delBtn.title = L10N.t('chats_delete_single');
                    delBtn.style.cssText = `
                        background: #f38ba8;
                        color: #111;
                        border: none;
                        border-radius: 6px;
                        padding: 12px 8px;
                        font-weight: bold;
                        cursor: pointer;
                        font-size: 18px;
                        flex-shrink: 0;
                        transition: background 0.15s;
                    `;
                    delBtn.addEventListener('mouseenter', () => { delBtn.style.background = '#e06c8a'; });
                    delBtn.addEventListener('mouseleave', () => { delBtn.style.background = '#f38ba8'; });
                    delBtn.addEventListener('click', async () => {
                        const confirmed = await showConfirmDialog(
                            L10N.t('chats_delete_single'),
                            L10N.t('confirm_delete_chat', { name: data.name || id })
                        );
                        if (!confirmed) return;
                        await removeChatData(id);
                        delete chatData[id];
                        renderChatsList(container, emptyEl);
                    });

                    card.appendChild(info);
                    card.appendChild(delBtn);
                    container.appendChild(card);
                }
            }

            renderChatsList(listContainer, emptyMsg);
            modal.appendChild(listContainer);
            modal.appendChild(btnContainer);
            overlay.appendChild(modal);
            document.body.appendChild(overlay);
            overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
        }

        function logError(moduleId, message) {
            const entry = { module: moduleId, message: message, timestamp: Date.now() };
            logEntries.push(entry);
            if (logEntries.length > 100) logEntries.shift();
            if (!logExpanded) toggleLog();
            updateLogUI();
        }

        function toggleLog() {
            logExpanded = !logExpanded;
            const content = document.getElementById('dispatcher-log-content');
            const icon = document.getElementById('dispatcher-log-icon');
            if (content) content.style.display = logExpanded ? 'block' : 'none';
            if (icon) icon.textContent = logExpanded ? '▼' : '▶';
            if (logExpanded && content) content.scrollTop = content.scrollHeight;
        }

        function updateLogUI() {
            const content = document.getElementById('dispatcher-log-content');
            const emptyMsg = document.getElementById('dispatcher-log-empty');
            const clearBtn = document.getElementById('dispatcher-log-clear');
            const indicator = logIndicator;
            if (!content) return;

            content.querySelectorAll('.log-entry').forEach(el => el.remove());

            if (logEntries.length === 0) {
                if (emptyMsg) emptyMsg.style.display = 'block';
                if (clearBtn) clearBtn.style.display = 'none';
                if (indicator) indicator.style.display = 'none';
                return;
            }

            if (emptyMsg) emptyMsg.style.display = 'none';
            if (clearBtn) clearBtn.style.display = 'block';
            if (indicator) {
                indicator.textContent = logEntries.length;
                indicator.style.display = 'inline-block';
            }

            for (const entry of logEntries) {
                const div = document.createElement('div');
                div.className = 'log-entry';
                div.style.cssText = `padding: 3px 0; border-bottom: 1px solid #313244; color: #f38ba8;`;
                const moduleName = modules[entry.module]?.title || entry.module;
                div.textContent = `[${moduleName}] ${entry.message}`;
                content.insertBefore(div, clearBtn);
            }

            if (logExpanded) content.scrollTop = content.scrollHeight;
        }

        // --- Управление модулями с зависимостями ---
        function checkDependencies(requires) {
            if (!requires || !Array.isArray(requires)) return true;
            return requires.every(globalName => window[globalName] !== undefined);
        }

        function registerModule(moduleId, config) {
            if (modules[moduleId]) {
                console.warn(`Модуль "${moduleId}" уже зарегистрирован.`);
                return;
            }
            if (!config.title) {
                console.error(`Модуль "${moduleId}" не имеет title.`);
                return;
            }
            if (!config.type || (config.type !== 'interface' && config.type !== 'button')) {
                console.error(`Модуль "${moduleId}" имеет неверный тип.`);
                return;
            }
            if (config.type === 'interface' && typeof config.content !== 'function') {
                console.error(`Модуль "${moduleId}" типа interface должен предоставлять функцию content.`);
                return;
            }
            if (config.type === 'button' && typeof config.onClick !== 'function') {
                console.error(`Модуль "${moduleId}" типа button должен предоставлять функцию onClick.`);
                return;
            }

            const depsOk = checkDependencies(config.requires);

            modules[moduleId] = {
                title: config.title,
                type: config.type,
                content: config.content || null,
                onClick: config.onClick || null,
                onActivate: config.onActivate || null,
                onDeactivate: config.onDeactivate || null,
                onEnable: config.onEnable || null,
                onDisable: config.onDisable || null,
                onClose: config.onClose || null,
                element: null,
                contentElement: null,
                expanded: false,
                _enabled: false,
                _pendingDeps: !depsOk,
                _requires: config.requires || []
            };

            moduleOrder.push(moduleId);
            rebuildList();

            if (!depsOk) {
                console.log(`⏳ Модуль "${moduleId}" ожидает зависимости: ${config.requires.join(', ')}`);
            } else {
                if (currentChatId && chatData[currentChatId]) {
                    const enabledModules = chatData[currentChatId].modules || [];
                    const isEnabled = enabledModules.includes(moduleId);
                    setModuleEnabled(moduleId, isEnabled, false, currentChatId);
                } else {
                    setModuleEnabled(moduleId, false, false, currentChatId);
                }
            }
        }

        function checkPendingModules() {
            for (const id of moduleOrder) {
                const mod = modules[id];
                if (mod && mod._pendingDeps && checkDependencies(mod._requires)) {
                    mod._pendingDeps = false;
                    console.log(`✅ Зависимости для модуля "${id}" удовлетворены`);
                    if (currentChatId && chatData[currentChatId]) {
                        const enabledModules = chatData[currentChatId].modules || [];
                        const isEnabled = enabledModules.includes(id);
                        setModuleEnabled(id, isEnabled, false, currentChatId);
                    } else {
                        setModuleEnabled(id, false, false, currentChatId);
                    }
                    rebuildList();
                }
            }
        }

        setInterval(checkPendingModules, 1000);

        function updateModule(moduleId, newContent) {
            if (!modules[moduleId]) return;
            const mod = modules[moduleId];
            if (mod.type !== 'interface') return;
            if (mod.contentElement) {
                mod.contentElement.innerHTML = newContent;
            }
            mod.content = () => newContent;
        }

        function closeModule(moduleId) {
            if (!modules[moduleId]) return;
            const mod = modules[moduleId];
            if (mod.onClose) mod.onClose();
            if (mod.element?.parentNode) mod.element.parentNode.removeChild(mod.element);
            delete modules[moduleId];
            const idx = moduleOrder.indexOf(moduleId);
            if (idx !== -1) moduleOrder.splice(idx, 1);
            rebuildList();
        }

        function collapseAll() {
            for (const id of moduleOrder) {
                const mod = modules[id];
                if (mod.type === 'interface' && mod.expanded) toggleExpand(id, true);
            }
        }

        function toggleExpand(moduleId, forceCollapse = false) {
            const mod = modules[moduleId];
            if (!mod || mod.type !== 'interface') return;
            if (!forceCollapse && mod._enabled === false) return;
            const newState = forceCollapse ? false : !mod.expanded;
            mod.expanded = newState;

            const tile = mod.element;
            if (tile) {
                const indicator = tile.querySelector('.dispatcher-indicator');
                if (indicator) indicator.textContent = newState ? '▼' : '▶';
                const contentDiv = mod.contentElement;
                if (contentDiv) contentDiv.style.display = newState ? 'block' : 'none';
            }
            if (newState && mod.onActivate) mod.onActivate();
            if (!newState && mod.onDeactivate) mod.onDeactivate();
        }

        function rebuildList() {
            if (!containerElement) return;
            while (containerElement.firstChild) containerElement.removeChild(containerElement.firstChild);

            if (moduleOrder.length === 0) {
                const empty = document.createElement('div');
                empty.id = 'dispatcher-empty';
                empty.style.cssText = `padding: 20px; text-align: center; color: #a6adc8; font-style: italic;`;
                empty.textContent = L10N.t('no_modules');
                containerElement.appendChild(empty);
                return;
            }

            for (const id of moduleOrder) {
                const mod = modules[id];
                const tile = document.createElement('div');
                tile.className = 'dispatcher-tile';
                tile.dataset.moduleId = id;
                tile.style.cssText = `
                    padding: 6px 10px; margin: 2px 4px; border-radius: 4px;
                    background: #1e1e2e; border: 1px solid #313244;
                    cursor: pointer; display: flex; align-items: center; gap: 6px;
                    transition: background 0.15s, opacity 0.2s;
                `;
                tile.style.opacity = mod._pendingDeps ? '0.4' : (mod._enabled ? '1' : '0.5');

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = 'module-checkbox';
                checkbox.style.cssText = `width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; accent-color: #cba6f7;`;
                checkbox.checked = mod._enabled;
                checkbox.disabled = mod._pendingDeps;
                checkbox.addEventListener('change', function(e) {
                    e.stopPropagation();
                    if (mod._pendingDeps) return;
                    setModuleEnabled(id, this.checked, true, currentChatId);
                });
                tile.appendChild(checkbox);

                const titleSpan = document.createElement('span');
                titleSpan.className = 'dispatcher-title';
                titleSpan.textContent = mod.title;
                titleSpan.style.cssText = `flex: 1; font-weight: 500;`;

                const indicator = document.createElement('span');
                indicator.className = 'dispatcher-indicator';
                indicator.style.cssText = `font-size: 12px; color: #a6adc8; margin-left: 8px;`;
                if (mod.type === 'interface') {
                    indicator.textContent = mod.expanded ? '▼' : '▶';
                } else {
                    indicator.textContent = '';
                }

                tile.appendChild(titleSpan);
                tile.appendChild(indicator);

                tile.addEventListener('click', function(e) {
                    if (e.target === checkbox) return;
                    if (mod._pendingDeps || !mod._enabled) return;
                    if (mod.type === 'interface') toggleExpand(id);
                    else if (mod.type === 'button' && mod.onClick) mod.onClick();
                });

                let contentContainer = null;
                if (mod.type === 'interface') {
                    contentContainer = document.createElement('div');
                    contentContainer.className = 'dispatcher-content';
                    contentContainer.style.cssText = `
                        padding: 8px 12px; background: #11111b;
                        border-top: 1px solid #313244;
                        display: ${mod.expanded ? 'block' : 'none'};
                    `;
                    try {
                        contentContainer.innerHTML = mod.content();
                    } catch (e) {
                        contentContainer.textContent = 'Error loading content';
                        console.error(`Ошибка загрузки содержимого модуля ${id}:`, e);
                    }
                    mod.contentElement = contentContainer;
                }

                mod.element = tile;
                mod.contentElement = contentContainer;

                containerElement.appendChild(tile);
                if (contentContainer) containerElement.appendChild(contentContainer);
            }
        }

        function updateUI() {
            const header = document.querySelector('.dispatcher-header');
            if (header) {
                const titleSpan = header.querySelector('.dispatcher-header-title');
                if (titleSpan) {
                    titleSpan.textContent = L10N.t('dispatcher_title');
                }
            }

            const chatsBtn = document.getElementById('dispatcher-chats-btn');
            if (chatsBtn) { chatsBtn.textContent = L10N.t('chats_title'); chatsBtn.title = L10N.t('chats_tooltip'); }
            const langBtn = document.getElementById('dispatcher-lang-btn');
            if (langBtn) { langBtn.textContent = L10N.t('lang_switch'); langBtn.title = L10N.t('lang_tooltip'); }
            const collapseBtn = document.getElementById('dispatcher-collapse-all');
            if (collapseBtn) collapseBtn.title = L10N.t('collapse_tooltip');
            const reloadBtn = document.getElementById('dispatcher-reload-btn');
            if (reloadBtn) reloadBtn.title = L10N.t('reload_tooltip');

            const collapsePanelBtn = document.querySelector('.dispatcher-header button:last-child');
            if (collapsePanelBtn) collapsePanelBtn.title = L10N.t('collapse_tooltip');

            const tooltip = document.getElementById('dispatcher-tooltip');
            if (tooltip) {
                tooltip.textContent = L10N.t('dispatcher_help');
            }

            const empty = document.getElementById('dispatcher-empty');
            if (empty) empty.textContent = L10N.t('no_modules');
            const logTitle = document.querySelector('#dispatcher-log .dispatcher-log-header span');
            if (logTitle) logTitle.textContent = L10N.t('log_title');
            const clearBtn = document.getElementById('dispatcher-log-clear');
            if (clearBtn) clearBtn.textContent = L10N.t('log_clear');
            const emptyLog = document.getElementById('dispatcher-log-empty');
            if (emptyLog) emptyLog.textContent = L10N.t('log_empty');
        }

        function showPanel() {
            if (panelElement) {
                panelElement.style.display = 'flex';
                if (toggleButton) toggleButton.style.display = 'none';
                panelCollapsed = false;
            } else {
                createPanel();
            }
        }

        // --- Система хуков ---
        function addHook(eventName, callback) {
            if (!hooks[eventName]) hooks[eventName] = [];
            hooks[eventName].push({ moduleId: null, callback });
        }

        async function runHook(eventName, ...args) {
            const hookList = hooks[eventName];
            if (!hookList || hookList.length === 0) return true;

            for (const hook of hookList) {
                try {
                    const result = await hook.callback(...args);
                    if (result === false) return false;
                } catch (e) {
                    console.error(`Ошибка в хуке ${eventName}:`, e);
                }
            }
            return true;
        }

        async function init() {
            await openDB();

            const savedLang = await getDispatcherData('lang');
            if (savedLang === 'ru' || savedLang === 'en') {
                L10N.currentLang = savedLang;
            }

            window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang: L10N.currentLang } }));

            await loadChatData();
            createPanel();
            await handleUrlChange();

            setTimeout(async () => { await handleUrlChange(); }, 2000);
            lastCheckedUrl = location.href;
            setInterval(async () => {
                if (location.href !== lastCheckedUrl) {
                    lastCheckedUrl = location.href;
                    await handleUrlChange();
                }
            }, 500);
            setTimeout(async () => { await syncAllModules(currentChatId); }, 500);
        }

        setTimeout(init, 50);

        window.__MANAGER__ = {
            register: registerModule,
            update: updateModule,
            close: closeModule,
setModuleTitle: function(moduleId, newTitle) {
    if (modules[moduleId]) {
        modules[moduleId].title = newTitle;
        const tile = document.querySelector(`.dispatcher-tile[data-module-id="${moduleId}"] .dispatcher-title`);
        if (tile) tile.textContent = newTitle;
    }
},
            show: showPanel,
            logError: logError,
            getCurrentChatId: getCurrentChatId,
            _updateUI: updateUI,
            getModules: () => moduleOrder.map(id => ({ id, ...modules[id] })),
            _refreshChat: () => handleUrlChange(),
            getModuleSettings: async (moduleId) => {
                if (!currentChatId) return {};
                return await getModuleSettings(currentChatId, moduleId);
            },
            setModuleSettings: async (moduleId, settings) => {
                if (!currentChatId) throw new Error('No active chat');
                await setModuleSettings(currentChatId, moduleId, settings);
            },
            addHook,
            runHook
        };

        console.log('📦 Диспетчер модулей загружен.');
    })();
})();