Grok Code Filter Menu 1.21.24

Adds a filter menu to the code blocks in the Grok chat while maintaining the settings

Versión del día 5/9/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Grok Code Filter Menu 1.21.24
// @namespace    http://tampermonkey.net/
// @version      1.21.24
// @description  Adds a filter menu to the code blocks in the Grok chat while maintaining the settings
// @author       tapeavion
// @license      MIT
// @match        https://grok.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==





(function() {
    'use strict';

    // Определение языка пользователя
    const userLang = navigator.language || navigator.languages[0];
    const isRussian = userLang.startsWith('ru');
    const defaultLang = isRussian ? 'ru' : 'en';

    // Локализация
    const translations = {
        ru: {
            filtersBtn: 'Фильтры',
            sliderLabel: 'Степень:',
            commentColorLabel: 'Цвет комментариев:',
            filters: [
                { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
                { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
                { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
                { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
                { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
                { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
            ],
            langSelect: 'Выберите язык:',
            langOptions: [
                { value: 'ru', label: 'Русский' },
                { value: 'en', label: 'English' }
            ]
        },
        en: {
            filtersBtn: 'Filters',
            sliderLabel: 'Level:',
            commentColorLabel: 'Comment color:',
            filters: [
                { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
                { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
                { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
                { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
                { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
                { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
            ],
            langSelect: 'Select language:',
            langOptions: [
                { value: 'ru', label: 'Русский' },
                { value: 'en', label: 'English' }
            ]
        }
    };

    // Глобальный объект для хранения настроек и контейнеров
    const state = {
        settings: null,
        codeBlocks: new Map() // Храним соответствие headerBlock -> {codeContainer, filterBtn, filterMenu}
    };

    // Загрузка настроек
    function loadSettings(callback) {
        chrome.storage.local.get(
            ['filterMenuLang', 'codeFilterStates', 'codeFilterValues', 'commentColor'],
            (result) => {
                const settings = {
                    filterMenuLang: result.filterMenuLang || defaultLang,
                    codeFilterStates: result.codeFilterStates || {},
                    codeFilterValues: result.codeFilterValues || {},
                    commentColor: result.commentColor || 'rgb(92, 99, 112)'
                };
                state.settings = settings;
                callback(settings);
            }
        );
    }

    // Применение фильтров к конкретному блоку с retry
    function applyFilters(targetBlock, filterStates, filterValues, retries = 5) {
        const preElement = targetBlock.querySelector('pre');
        if (!preElement) {
            if (retries > 0) {
                setTimeout(() => applyFilters(targetBlock, filterStates, filterValues, retries - 1), 50);
            }
            return;
        }
        const filters = translations[state.settings.filterMenuLang].filters;
        const activeFilters = filters
            .filter(filter => filterStates[filter.value])
            .map(filter => {
                const unit = filter.unit || '';
                const value = filterValues[filter.value] || filter.default;
                return `${filter.value}(${value}${unit})`;
            });
        preElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
    }

    // Обновление фильтров для всех блоков
    function updateAllFilters() {
        state.codeBlocks.forEach(({codeContainer}) => {
            applyFilters(codeContainer, state.settings.codeFilterStates, state.settings.codeFilterValues);
        });
        chrome.storage.local.set({
            codeFilterStates: state.settings.codeFilterStates,
            codeFilterValues: state.settings.codeFilterValues
        });
    }

    // Применение цвета комментариев с retry
    function applyCommentColor(codeContainer, commentColor, retries = 5) {
        const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment');
        if (commentElements.length > 0) {
            commentElements.forEach(element => {
                element.style.color = commentColor;
            });
            return;
        }
        if (retries > 0) {
            setTimeout(() => applyCommentColor(codeContainer, commentColor, retries - 1), 50);
        }
    }

    // Обновление цвета комментариев для всех блоков
    function updateAllCommentColors() {
        state.codeBlocks.forEach(({codeContainer}) => {
            applyCommentColor(codeContainer, state.settings.commentColor);
        });
        chrome.storage.local.set({ commentColor: state.settings.commentColor });
    }

    // Создание меню фильтров
    function addFilterMenu(headerBlock, codeContainer) {
        if (headerBlock.querySelector('.filter-menu-btn')) return;

        let currentLang = state.settings.filterMenuLang;
        let currentCommentColor = state.settings.commentColor;
        let savedFilterStates = { ...state.settings.codeFilterStates };
        let savedFilterValues = { ...state.settings.codeFilterValues };

        const filterBtn = document.createElement('button');
        filterBtn.className = 'filter-menu-btn';
        filterBtn.textContent = translations[currentLang].filtersBtn;

        const filterMenu = document.createElement('div');
        filterMenu.className = 'filter-menu';

        // Инициализация фильтров
        const filters = translations[currentLang].filters;
        filters.forEach(filter => {
            if (!(filter.value in savedFilterStates)) {
                savedFilterStates[filter.value] = false;
            }
            if (!(filter.value in savedFilterValues)) {
                savedFilterValues[filter.value] = filter.default;
            }
        });

        // Создание выпадающего списка для языка
        const langSelect = document.createElement('select');
        langSelect.className = 'language-select';
        const langLabel = document.createElement('label');
        langLabel.textContent = translations[currentLang].langSelect;
        langLabel.style.color = '#a0a0a0';
        langLabel.style.fontSize = '12px';
        langLabel.style.marginBottom = '2px';
        langLabel.style.display = 'block';

        translations[currentLang].langOptions.forEach(option => {
            const opt = document.createElement('option');
            opt.value = option.value;
            opt.textContent = option.label;
            if (option.value === currentLang) opt.selected = true;
            langSelect.appendChild(opt);
        });

        // Создание выбора цвета
        const colorPickerLabel = document.createElement('label');
        colorPickerLabel.className = 'color-picker-label';
        colorPickerLabel.textContent = translations[currentLang].commentColorLabel;

        const colorPicker = document.createElement('input');
        colorPicker.type = 'color';
        colorPicker.className = 'color-picker';
        colorPicker.value = currentCommentColor;

        // Обработчик изменения цвета
        colorPicker.addEventListener('input', () => {
            currentCommentColor = colorPicker.value;
            state.settings.commentColor = currentCommentColor;
            updateAllCommentColors();
        });

        // Обновление интерфейса при смене языка
        function updateLanguage(lang) {
            currentLang = lang;
            state.settings.filterMenuLang = lang;
            chrome.storage.local.set({ filterMenuLang: lang });
            filterBtn.textContent = translations[currentLang].filtersBtn;
            langLabel.textContent = translations[currentLang].langSelect;
            colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
            renderFilters();
        }

        langSelect.addEventListener('change', () => updateLanguage(langSelect.value));

        // Рендеринг фильтров
        function renderFilters() {
            filterMenu.innerHTML = '';
            filterMenu.appendChild(langLabel);
            filterMenu.appendChild(langSelect);
            filterMenu.appendChild(colorPickerLabel);
            filterMenu.appendChild(colorPicker);

            filters.forEach(filter => {
                const filterItem = document.createElement('div');
                filterItem.className = 'filter-item';

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = savedFilterStates[filter.value];
                checkbox.id = `filter-${filter.value}-${Date.now()}`; // Уникальный ID

                const label = document.createElement('label');
                label.htmlFor = checkbox.id;
                label.textContent = filter.name;

                const sliderLabel = document.createElement('label');
                sliderLabel.className = 'filter-slider-label';
                sliderLabel.textContent = translations[currentLang].sliderLabel;

                const slider = document.createElement('input');
                slider.type = 'range';
                slider.className = 'filter-slider';
                slider.min = filter.min;
                slider.max = filter.max;
                slider.step = filter.step;
                slider.value = savedFilterValues[filter.value];

                if (checkbox.checked && filter.hasSlider) {
                    slider.style.display = 'block';
                    sliderLabel.style.display = 'block';
                }

                checkbox.addEventListener('change', () => {
                    savedFilterStates[filter.value] = checkbox.checked;
                    state.settings.codeFilterStates = savedFilterStates;
                    if (filter.hasSlider) {
                        slider.style.display = checkbox.checked ? 'block' : 'none';
                        sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
                    }
                    updateAllFilters();
                });

                slider.addEventListener('input', () => {
                    savedFilterValues[filter.value] = slider.value;
                    state.settings.codeFilterValues = savedFilterValues;
                    updateAllFilters();
                });

                filterItem.appendChild(checkbox);
                filterItem.appendChild(label);
                filterMenu.appendChild(filterItem);
                filterMenu.appendChild(sliderLabel);
                filterMenu.appendChild(slider);
            });
        }

        renderFilters();

        filterBtn.addEventListener('click', () => {
            filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
        });

        document.addEventListener('click', (e) => {
            if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
                filterMenu.style.display = 'none';
            }
        });

        headerBlock.style.position = 'relative';
        headerBlock.appendChild(filterBtn);
        headerBlock.appendChild(filterMenu);

        // Сохраняем блок в state
        state.codeBlocks.set(headerBlock, { codeContainer, filterBtn, filterMenu });

        applyFilters(codeContainer, savedFilterStates, savedFilterValues);
        applyCommentColor(codeContainer, currentCommentColor);
    }

    // Обработка блоков кода
    function processCodeBlocks() {
        if (!state.settings) {
            loadSettings((settings) => {
                processCodeBlocksInternal(settings);
            });
            return;
        }
        processCodeBlocksInternal(state.settings);
    }

    function findCodeContainer(bar) {
        let sibling = bar.nextElementSibling;
        while (sibling) {
            if (sibling.matches('div.shiki') || sibling.querySelector('pre, code') || sibling.matches('div[class*="code"]') || sibling.matches('div.sticky')) {
                if (sibling.matches('div.shiki')) {
                    return sibling;
                }
            }
            sibling = sibling.nextElementSibling;
        }
        return null;
    }

    function processCodeBlocksInternal(settings) {
        const headerSelectors = [
            'div.flex.flex-row.px-4.py-2.h-10.items-center.rounded-t-xl.bg-surface-l2.border.border-border-l1 > span.font-mono.text-xs',
            'div.flex.flex-row.items-center.rounded-t-xl.bg-surface-l2.border > span.font-mono.text-xs',
            'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
            'div[class*="flex"][class*="bg-surface"] > span',
            'div > span[class*="font-mono"]'
        ];

        let headerBlocks = [];
        for (const selector of headerSelectors) {
            const headers = Array.from(document.querySelectorAll(selector))
                .filter(span => {
                    const text = span.textContent.toLowerCase();
                    return ['javascript', 'text', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
                })
                .map(span => span.closest('div'));
            headerBlocks.push(...headers);
        }
        headerBlocks = [...new Set(headerBlocks)];

        // Очистка удалённых блоков
        state.codeBlocks.forEach((value, key) => {
            if (!document.body.contains(key)) {
                state.codeBlocks.delete(key);
            }
        });

        headerBlocks.forEach(headerBlock => {
            if (state.codeBlocks.has(headerBlock)) return;

            const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
            if (!langSpan) {
                console.log('Не найден span с языком:', headerBlock);
                return;
            }

            const codeContainer = findCodeContainer(headerBlock);

            if (codeContainer) {
                addFilterMenu(headerBlock, codeContainer);
            } else {
                console.log('Контейнер кода не найден для:', headerBlock);
            }
        });
    }

    // Инициализация
    processCodeBlocks();

    // Наблюдатель за изменениями DOM
    const observer = new MutationObserver((mutations) => {
        const relevantChanges = mutations.some(mutation => {
            return mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => {
                return node.nodeType === 1 && (node.getAttribute('data-testid') === 'code-block' || node.matches('div.message-bubble, div.response-content-markdown, div.shiki, div[class*="code"], div[class*="flex"][class*="rounded-t"]') || node.querySelector('[data-testid="code-block"], div.message-bubble, div.response-content-markdown, div.shiki, div[class*="code"], div[class*="flex"][class*="rounded-t"]'));
            });
        });
        if (relevantChanges) {
            processCodeBlocks();
            setTimeout(processCodeBlocks, 100); // Повтор для асинхронных добавлений
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();