Grok Code Filter Menu

Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек

Устаревшая версия за 28.03.2025. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Grok Code Filter Menu
// @namespace    http://tampermonkey.net/
// @version      1.21.10
// @description  Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
// @author       tapeavion
// @license      MIT
// @match        https://grok.com/chat/*
// @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';

    // Стили
    GM_addStyle(`
        .filter-menu-btn {
            position: absolute;
            top: 4px;
            right: 160px;
            z-index: 9999;
            padding: 4px 8px;
            background: #2d2d2d;
            color: #a0a0a0;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s ease, color 0.2s ease;
        }
        .filter-menu-btn:hover {
            background: #4a4a4a;
            color: #ffffff;
        }
        .filter-menu {
            position: absolute;
            top: 32px;
            right: 160px;
            background: #2d2d2d;
            border: 1px solid #444;
            border-radius: 8px;
            padding: 5px;
            z-index: 9999;
            display: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            width: 200px;
            max-height: 400px;
            overflow-y: auto;
        }
        .filter-item {
            display: flex;
            align-items: center;
            padding: 5px 0;
            color: #a0a0a0;
            font-size: 12px;
        }
        .filter-item input[type="checkbox"] {
            margin-right: 5px;
        }
        .filter-item label {
            flex: 1;
            cursor: pointer;
        }
        .filter-slider {
            display: none;
            margin: 5px 0 5px 20px;
            width: calc(100% - 20px);
        }
        .filter-slider-label {
            display: none;
            color: #a0a0a0;
            font-size: 12px;
            margin: 2px 0 2px 20px;
        }
        .language-select {
            width: 100%;
            padding: 5px;
            margin-bottom: 5px;
            background: #3a3a3a;
            color: #a0a0a0;
            border: none;
            border-radius: 4px;
            font-size: 12px;
        }
        .color-picker {
            margin: 5px 0 5px 20px;
            width: calc(100% - 20px);
        }
        .color-picker-label {
            display: block;
            color: #a0a0a0;
            font-size: 12px;
            margin: 2px 0 2px 20px;
        }
    `);

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

    // Локализация
    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' }
            ]
        }
    };

    // Функция создания меню фильтров
    function addFilterMenu(codeBlock) {
        if (codeBlock.querySelector('.filter-menu-btn')) return;

        let currentLang = savedLang;
        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 targetBlock = codeBlock.querySelector('div[style*="background: rgb(18, 19, 20)"]') || codeBlock;

        // Загружаем сохраненные настройки
        const savedFilterStates = GM_getValue('codeFilterStates', {});
        const savedFilterValues = GM_getValue('codeFilterValues', {});
        const savedCommentColor = GM_getValue('commentColor', '#5c6370'); // Оригинальный цвет в HEX

        // Инициализируем значения по умолчанию
        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;
            }
        });

        // Применяем сохраненные фильтры
        function applyFilters() {
            const activeFilters = filters
                .filter(filter => savedFilterStates[filter.value])
                .map(filter => {
                    const unit = filter.unit || '';
                    const value = savedFilterValues[filter.value];
                    return `${filter.value}(${value}${unit})`;
                });
            targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
        }

        // Применяем цвет комментариев
        function applyCommentColor() {
            const commentElements = codeBlock.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
            commentElements.forEach(element => {
                element.style.color = savedCommentColor;
            });
        }
        applyFilters();
        applyCommentColor();

        // Создаем выпадающий список для выбора языка
        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 = savedCommentColor;

        colorPicker.addEventListener('input', () => {
            const newColor = colorPicker.value;
            GM_setValue('commentColor', newColor);
            const commentElements = document.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
            commentElements.forEach(element => {
                element.style.color = newColor;
            });
        });

        // Функция обновления интерфейса при смене языка
        function updateLanguage(lang) {
            currentLang = lang;
            GM_setValue('filterMenuLang', currentLang);
            filterBtn.textContent = translations[currentLang].filtersBtn;
            langLabel.textContent = translations[currentLang].langSelect;
            colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
            filterMenu.innerHTML = ''; // Очищаем меню
            filterMenu.appendChild(langLabel);
            filterMenu.appendChild(langSelect);
            filterMenu.appendChild(colorPickerLabel);
            filterMenu.appendChild(colorPicker);
            renderFilters();
        }

        // Обработчик смены языка
        langSelect.addEventListener('change', () => {
            updateLanguage(langSelect.value);
        });

        // Рендеринг фильтров
        function renderFilters() {
            const filters = translations[currentLang].filters;
            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}`;

                const label = document.createElement('label');
                label.htmlFor = `filter-${filter.value}`;
                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;
                    GM_setValue('codeFilterStates', savedFilterStates);
                    if (filter.hasSlider) {
                        slider.style.display = checkbox.checked ? 'block' : 'none';
                        sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
                    }
                    applyFilters();
                });

                slider.addEventListener('input', () => {
                    savedFilterValues[filter.value] = slider.value;
                    GM_setValue('codeFilterValues', savedFilterValues);
                    applyFilters();
                });

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

        // Инициализация
        filterMenu.appendChild(langLabel);
        filterMenu.appendChild(langSelect);
        filterMenu.appendChild(colorPickerLabel);
        filterMenu.appendChild(colorPicker);
        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';
            }
        });

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

    // Функция поиска и обработки блоков кода
    function processCodeBlocks() {
        const codeBlocks = document.querySelectorAll('div[class*="relative"][class*="mt-3"][class*="mb-3"][class*="-mx-4"]');
        console.log('Найдено блоков кода:', codeBlocks.length);
        codeBlocks.forEach(block => {
            if (block.querySelector('div[style*="background: rgb(18, 19, 20)"]')) {
                addFilterMenu(block);
                const savedFilterStates = GM_getValue('codeFilterStates', {});
                const savedFilterValues = GM_getValue('codeFilterValues', {});
                const savedCommentColor = GM_getValue('commentColor', '#5c6370');
                const targetBlock = block.querySelector('div[style*="background: rgb(18, 19, 20)"]') || block;
                const filters = [
                    { value: 'invert' },
                    { value: 'sepia' },
                    { value: 'grayscale' },
                    { value: 'blur', unit: 'px' },
                    { value: 'contrast' },
                    { value: 'brightness' },
                    { value: 'hue-rotate', unit: 'deg' },
                    { value: 'saturate' },
                    { value: 'opacity' }
                ];
                const activeFilters = filters
                    .filter(filter => savedFilterStates[filter.value])
                    .map(filter => {
                        const unit = filter.unit || '';
                        const value = savedFilterValues[filter.value] || (filter.value === 'blur' ? 2 : filter.value === 'brightness' ? 1.5 : filter.value === 'contrast' ? 2 : filter.value === 'hue-rotate' ? 90 : 1);
                        return `${filter.value}(${value}${unit})`;
                    });
                targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
                const commentElements = block.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
                commentElements.forEach(element => {
                    element.style.color = savedCommentColor;
                });
            }
        });
    }

    setTimeout(processCodeBlocks, 1000);
    processCodeBlocks();

    const observer = new MutationObserver(() => {
        processCodeBlocks();
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();