// ==UserScript==
// @name Grok Code Filter Menu 1.21.18
// @namespace http://tampermonkey.net/
// @version 1.21.18
// @description Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
// @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
// @downloadURL
// @updateURL
// ==/UserScript==
(function() {
'use strict';
// Стили
const style = document.createElement('style');
style.textContent = `
.filter-menu-btn {
position: absolute;
top: 4px;
right: 460px;
height: 31px !important;
z-index: 1;
padding: 4px 8px;
background: #1d5752;
color: #b9bcc1;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s ease, color 0.2s ease;
}
.filter-menu-btn:hover {
background: #4a8983;
color: #ffffff;
}
.filter-menu {
position: absolute;
top: 40px;
right: 10px;
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: 550px;
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;
}
button.inline-flex {
background-color: #1d5752 !important;
opacity: 0;
animation: fadeIn 1s ease-in-out forwards;
}
button.inline-flex:hover {
background-color: #1d5752 !important;
opacity: 1;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
`;
document.head.appendChild(style);
// Определение языка пользователя
const userLang = navigator.language || navigator.languages[0];
const isRussian = userLang.startsWith('ru');
const defaultLang = isRussian ? 'ru' : 'en';
const savedLang = localStorage.getItem('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' }
]
}
};
// Глобальная переменная для текущего цвета комментариев
let currentCommentColor = localStorage.getItem('commentColor') || '#5c6370';
// Массив для хранения всех пар headerBlock и codeContainer
const codeBlockRegistry = [];
// Функция создания меню фильтров
function addFilterMenu(headerBlock, codeContainer) {
if (headerBlock.querySelector('.filter-menu-btn')) {
console.log('Фильтр уже существует для заголовка:', headerBlock);
return;
}
// Сохраняем пару headerBlock и codeContainer
codeBlockRegistry.push({ headerBlock, codeContainer });
console.log('Добавлен кодовый блок в реестр:', codeContainer.outerHTML);
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 = codeContainer;
// Загружаем сохраненные настройки
const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
// Инициализируем значения по умолчанию
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';
console.log('Применены фильтры к контейнеру:', targetBlock, activeFilters);
}
// Применяем цвет комментариев
// Альтернативный подход: применение цвета через глобальный стиль
function applyGlobalCommentColor() {
const existingStyle = document.getElementById('custom-comment-style');
if (existingStyle) existingStyle.remove();
const style = document.createElement('style');
style.id = 'custom-comment-style';
style.textContent = `
.hljs-comment, span[style*="color: rgb(92, 99, 112)"] {
color: ${currentCommentColor} !important;
}
`;
document.head.appendChild(style);
console.log('Применен глобальный стиль для комментариев:', currentCommentColor);
}
applyFilters();
applyGlobalCommentColor();
// Создаем выпадающий список для выбора языка
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;
localStorage.setItem('commentColor', currentCommentColor);
console.log('Изменен цвет комментариев:', currentCommentColor);
refreshAllCodeBlocks();
});
// Функция обновления интерфейса при смене языка
function updateLanguage(lang) {
currentLang = lang;
localStorage.setItem('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;
localStorage.setItem('codeFilterStates', JSON.stringify(savedFilterStates));
if (filter.hasSlider) {
slider.style.display = checkbox.checked ? 'block' : 'none';
sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
}
console.log('Изменен фильтр:', filter.value, checkbox.checked);
refreshAllCodeBlocks();
});
slider.addEventListener('input', () => {
savedFilterValues[filter.value] = slider.value;
localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues));
console.log('Изменено значение фильтра:', filter.value, slider.value);
refreshAllCodeBlocks();
});
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';
}
});
headerBlock.style.position = 'relative';
headerBlock.appendChild(filterBtn);
headerBlock.appendChild(filterMenu);
}
// Функция логирования структуры DOM для отладки
function logDomStructure(headerBlock) {
console.log('Заголовок блока кода:', headerBlock.outerHTML);
console.log('Следующий элемент (nextElementSibling):', headerBlock.nextElementSibling?.outerHTML || 'Не найден');
console.log('Родительский элемент:', headerBlock.parentElement.outerHTML);
console.log('Все <code> в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('code')).map(el => el.outerHTML));
console.log('Все .not-prose в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('.not-prose')).map(el => el.outerHTML));
}
// Функция для управления селекторами и контейнерами
function manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer) {
const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');
// Сохраняем успешные селекторы и контейнеры
if (headerBlocks.length > 0 && codeContainer) {
savedSelectors[headerSelectorUsed] = savedSelectors[headerSelectorUsed] || {};
savedSelectors[headerSelectorUsed].headerCount = headerBlocks.length;
savedSelectors[headerSelectorUsed].containerSelector = containerSelectorUsed;
savedSelectors[headerSelectorUsed].lastUsed = Date.now();
localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
console.log('Сохранены селекторы:', headerSelectorUsed, containerSelectorUsed);
}
// Очистка устаревших селекторов (старше 7 дней)
const oneWeek = 7 * 24 * 60 * 60 * 1000;
Object.keys(savedSelectors).forEach(selector => {
if (Date.now() - savedSelectors[selector].lastUsed > oneWeek) {
delete savedSelectors[selector];
}
});
localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
return savedSelectors;
}
// Функция для обновления всех кодовых блоков
function refreshAllCodeBlocks() {
const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
const filters = [
{ value: 'invert', default: 1 },
{ value: 'sepia', default: 1 },
{ value: 'grayscale', default: 1 },
{ value: 'blur', unit: 'px', default: 2 },
{ value: 'contrast', default: 2 },
{ value: 'brightness', default: 1.5 },
{ value: 'hue-rotate', unit: 'deg', default: 90 },
{ value: 'saturate', default: 1 },
{ value: 'opacity', default: 1 }
];
// Применяем к зарегистрированным блокам
console.log('Реестр кодовых блоков:', codeBlockRegistry.length);
codeBlockRegistry.forEach(({ codeContainer }) => {
const activeFilters = filters
.filter(filter => savedFilterStates[filter.value])
.map(filter => {
const unit = filter.unit || '';
const value = savedFilterValues[filter.value] || filter.default;
return `${filter.value}(${value}${unit})`;
});
codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
console.log('Применены фильтры к зарегистрированному контейнеру:', codeContainer, activeFilters);
const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
console.log('Найдено комментариев в зарегистрированном контейнере:', commentElements.length);
commentElements.forEach(element => {
element.style.color = currentCommentColor;
});
});
// Глобальное применение к новым или незарегистрированным блокам
const allCodeContainers = document.querySelectorAll('.not-prose div > code, .not-prose pre > code');
allCodeContainers.forEach(codeContainer => {
if (!codeBlockRegistry.some(reg => reg.codeContainer === codeContainer.parentElement)) {
const activeFilters = filters
.filter(filter => savedFilterStates[filter.value])
.map(filter => {
const unit = filter.unit || '';
const value = savedFilterValues[filter.value] || filter.default;
return `${filter.value}(${value}${unit})`;
});
codeContainer.parentElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
console.log('Применены фильтры к глобальному .not-prose контейнеру:', codeContainer.parentElement, activeFilters);
const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
console.log('Найдено комментариев в глобальном .not-prose контейнере:', commentElements.length);
commentElements.forEach(element => {
element.style.color = currentCommentColor;
});
}
});
}
// Функция поиска и обработки блоков кода
function processCodeBlocks() {
// Селекторы для заголовков блоков кода
const headerSelectors = [
'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
'div[class*="flex"][class*="bg-surface"] > span',
'div > span[class*="font-mono"]'
];
// Селекторы для контейнеров кода
const containerSelectors = [
'nextElementSibling',
'.not-prose div[style*="overflow-x: auto"]',
'.not-prose div > code',
'.not-prose pre > code',
'.not-prose div[style*="background: hsl"]'
];
// Загружаем сохраненные селекторы
const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');
let headerBlocks = [];
let headerSelectorUsed = null;
let containerSelectorUsed = null;
let codeContainer = null;
// Сначала пытаемся использовать сохраненные селекторы
for (const savedSelector of Object.keys(savedSelectors)) {
const headers = Array.from(document.querySelectorAll(savedSelector))
.filter(span => {
const text = span.textContent.toLowerCase();
return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
})
.map(span => span.closest('div'));
if (headers.length > 0) {
headerBlocks = [...new Set(headers)];
headerSelectorUsed = savedSelector;
const savedContainerSelector = savedSelectors[savedSelector].containerSelector;
// Проверяем сохраненный селектор контейнера
for (const headerBlock of headers) {
if (savedContainerSelector === 'nextElementSibling') {
codeContainer = headerBlock.nextElementSibling?.querySelector('code') ? headerBlock.nextElementSibling : null;
} else if (savedContainerSelector === '.not-prose div > code') {
codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
} else if (savedContainerSelector === '.not-prose pre > code') {
codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
} else {
codeContainer = headerBlock.parentElement.querySelector(savedContainerSelector);
}
if (codeContainer) {
containerSelectorUsed = savedContainerSelector;
console.log('Использованы сохраненные селекторы:', headerSelectorUsed, containerSelectorUsed);
break;
}
}
if (codeContainer) break;
}
}
// Если сохраненные селекторы не сработали, ищем новые
if (headerBlocks.length === 0) {
for (const selector of headerSelectors) {
const headers = Array.from(document.querySelectorAll(selector))
.filter(span => {
const text = span.textContent.toLowerCase();
return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
})
.map(span => span.closest('div'));
if (headers.length > 0) {
headerBlocks = [...new Set(headers)];
headerSelectorUsed = selector;
break;
}
}
}
console.log('Найдено заголовков блоков кода:', headerBlocks.length);
headerBlocks.forEach(headerBlock => {
const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
if (!langSpan) {
console.log('Заголовок без span с языком:', headerBlock);
return;
}
// Пытаемся найти контейнер кода
codeContainer = null;
if (headerBlock.nextElementSibling?.querySelector('code')) {
codeContainer = headerBlock.nextElementSibling;
containerSelectorUsed = 'nextElementSibling';
} else if (headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]')) {
codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]');
containerSelectorUsed = '.not-prose div[style*="overflow-x: auto"]';
} else if (headerBlock.parentElement.querySelector('.not-prose div > code')) {
codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
containerSelectorUsed = '.not-prose div > code';
} else if (headerBlock.parentElement.querySelector('.not-prose pre > code')) {
codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
containerSelectorUsed = '.not-prose pre > code';
} else if (headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]')) {
codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]');
containerSelectorUsed = '.not-prose div[style*="background: hsl"]';
}
if (codeContainer) {
console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML);
addFilterMenu(headerBlock, codeContainer);
// Сохраняем успешные селекторы
manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer);
} else {
console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML);
logDomStructure(headerBlock);
}
});
// Обновляем все кодовые блоки после обработки
refreshAllCodeBlocks();
}
// Инициализация
setTimeout(() => {
console.log('Инициализация processCodeBlocks');
processCodeBlocks();
}, 2000);
// Наблюдатель за изменениями DOM
const observer = new MutationObserver((mutations) => {
console.log('Обнаружены изменения DOM, вызывается processCodeBlocks');
processCodeBlocks();
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
});
// Инициализируем глобальное применение фильтров
refreshAllCodeBlocks();
})();