Grok Code Filter Menu 1.21.24

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

Versione datata 05/09/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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 });
})();