Grok Code Filter Menu 1.21.15

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

当前为 2025-04-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Grok Code Filter Menu 1.21.15
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.21.15
  5. // @description Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
  6. // @author tapeavion
  7. // @license MIT
  8. // @match https://grok.com/chat*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Стили
  19. const style = document.createElement('style');
  20. style.textContent = `
  21. .filter-menu-btn {
  22. position: absolute;
  23. top: 4px;
  24. right: 430px;
  25. height: 31px !important;
  26. z-index: 10001;
  27. padding: 4px 8px;
  28. background: #1d5752;
  29. color: #b9bcc1;
  30. border: none;
  31. border-radius: 8px;
  32. cursor: pointer;
  33. font-size: 12px;
  34. transition: background 0.2s ease, color 0.2s ease;
  35. }
  36. .filter-menu-btn:hover {
  37. background: #4a8983;
  38. color: #ffffff;
  39. }
  40. .filter-menu {
  41. position: absolute;
  42. top: 40px;
  43. right: 10px;
  44. background: #2d2d2d;
  45. border: 1px solid #444;
  46. border-radius: 8px;
  47. padding: 5px;
  48. z-index: 9999;
  49. display: none;
  50. box-shadow: 0 2px 4px rgba(0,0,0,0.3);
  51. width: 200px;
  52. max-height: 550px;
  53. overflow-y: auto;
  54. }
  55. .filter-item {
  56. display: flex;
  57. align-items: center;
  58. padding: 5px 0;
  59. color: #a0a0a0;
  60. font-size: 12px;
  61. }
  62. .filter-item input[type="checkbox"] {
  63. margin-right: 5px;
  64. }
  65. .filter-item label {
  66. flex: 1;
  67. cursor: pointer;
  68. }
  69. .filter-slider {
  70. display: none;
  71. margin: 5px 0 5px 20px;
  72. width: calc(100% - 20px);
  73. }
  74. .filter-slider-label {
  75. display: none;
  76. color: #a0a0a0;
  77. font-size: 12px;
  78. margin: 2px 0 2px 20px;
  79. }
  80. .language-select {
  81. width: 100%;
  82. padding: 5px;
  83. margin-bottom: 5px;
  84. background: #3a3a3a;
  85. color: #a0a0a0;
  86. border: none;
  87. border-radius: 4px;
  88. font-size: 12px;
  89. }
  90. .color-picker {
  91. margin: 5px 0 5px 20px;
  92. width: calc(100% - 20px);
  93. }
  94. .color-picker-label {
  95. display: block;
  96. color: #a0a0a0;
  97. font-size: 12px;
  98. margin: 2px 0 2px 20px;
  99. }
  100. /* Новый стиль для кнопки */
  101. div[class*="flex"][class*="rounded-t"] > button[class*="inline-flex"][class*="h-8"][class*="rounded-lg"][class*="text-xs"] {
  102. background: #20665f !important;
  103. color: aliceblue !important;
  104. }
  105. `;
  106. document.head.appendChild(style);
  107.  
  108. // Определение языка пользователя
  109. const userLang = navigator.language || navigator.languages[0];
  110. const isRussian = userLang.startsWith('ru');
  111. const defaultLang = isRussian ? 'ru' : 'en';
  112. const savedLang = localStorage.getItem('filterMenuLang') || defaultLang;
  113.  
  114. // Локализация
  115. const translations = {
  116. ru: {
  117. filtersBtn: 'Фильтры',
  118. sliderLabel: 'Степень:',
  119. commentColorLabel: 'Цвет комментариев:',
  120. filters: [
  121. { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  122. { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  123. { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  124. { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  125. { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  126. { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  127. { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  128. { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  129. { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  130. ],
  131. langSelect: 'Выберите язык:',
  132. langOptions: [
  133. { value: 'ru', label: 'Русский' },
  134. { value: 'en', label: 'English' }
  135. ]
  136. },
  137. en: {
  138. filtersBtn: 'Filters',
  139. sliderLabel: 'Level:',
  140. commentColorLabel: 'Comment color:',
  141. filters: [
  142. { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  143. { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  144. { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  145. { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  146. { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  147. { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  148. { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  149. { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  150. { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  151. ],
  152. langSelect: 'Select language:',
  153. langOptions: [
  154. { value: 'ru', label: 'Русский' },
  155. { value: 'en', label: 'English' }
  156. ]
  157. }
  158. };
  159.  
  160. // Глобальная переменная для текущего цвета комментариев
  161. let currentCommentColor = localStorage.getItem('commentColor') || '#5c6370';
  162.  
  163. // Функция создания меню фильтров
  164. function addFilterMenu(headerBlock, codeContainer) {
  165. if (headerBlock.querySelector('.filter-menu-btn')) return;
  166.  
  167. let currentLang = savedLang;
  168. const filterBtn = document.createElement('button');
  169. filterBtn.className = 'filter-menu-btn';
  170. filterBtn.textContent = translations[currentLang].filtersBtn;
  171.  
  172. const filterMenu = document.createElement('div');
  173. filterMenu.className = 'filter-menu';
  174.  
  175. // Целевой блок — контейнер кода
  176. const targetBlock = codeContainer;
  177.  
  178. // Загружаем сохраненные настройки
  179. const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
  180. const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
  181.  
  182. // Инициализируем значения по умолчанию
  183. const filters = translations[currentLang].filters;
  184. filters.forEach(filter => {
  185. if (!(filter.value in savedFilterStates)) {
  186. savedFilterStates[filter.value] = false;
  187. }
  188. if (!(filter.value in savedFilterValues)) {
  189. savedFilterValues[filter.value] = filter.default;
  190. }
  191. });
  192.  
  193. // Применяем сохраненные фильтры
  194. function applyFilters() {
  195. const activeFilters = filters
  196. .filter(filter => savedFilterStates[filter.value])
  197. .map(filter => {
  198. const unit = filter.unit || '';
  199. const value = savedFilterValues[filter.value];
  200. return `${filter.value}(${value}${unit})`;
  201. });
  202. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  203. }
  204.  
  205. // Применяем цвет комментариев
  206. function applyCommentColor() {
  207. const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment');
  208. commentElements.forEach(element => {
  209. element.style.color = currentCommentColor;
  210. });
  211. }
  212. applyFilters();
  213. applyCommentColor();
  214.  
  215. // Создаем выпадающий список для выбора языка
  216. const langSelect = document.createElement('select');
  217. langSelect.className = 'language-select';
  218. const langLabel = document.createElement('label');
  219. langLabel.textContent = translations[currentLang].langSelect;
  220. langLabel.style.color = '#a0a0a0';
  221. langLabel.style.fontSize = '12px';
  222. langLabel.style.marginBottom = '2px';
  223. langLabel.style.display = 'block';
  224.  
  225. translations[currentLang].langOptions.forEach(option => {
  226. const opt = document.createElement('option');
  227. opt.value = option.value;
  228. opt.textContent = option.label;
  229. if (option.value === currentLang) {
  230. opt.selected = true;
  231. }
  232. langSelect.appendChild(opt);
  233. });
  234.  
  235. // Создаем элемент для выбора цвета комментариев
  236. const colorPickerLabel = document.createElement('label');
  237. colorPickerLabel.className = 'color-picker-label';
  238. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  239.  
  240. const colorPicker = document.createElement('input');
  241. colorPicker.type = 'color';
  242. colorPicker.className = 'color-picker';
  243. colorPicker.value = currentCommentColor;
  244.  
  245. colorPicker.addEventListener('input', () => {
  246. currentCommentColor = colorPicker.value;
  247. localStorage.setItem('commentColor', currentCommentColor);
  248. document.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment').forEach(element => {
  249. element.style.color = currentCommentColor;
  250. });
  251. });
  252.  
  253. // Функция обновления интерфейса при смене языка
  254. function updateLanguage(lang) {
  255. currentLang = lang;
  256. localStorage.setItem('filterMenuLang', currentLang);
  257. filterBtn.textContent = translations[currentLang].filtersBtn;
  258. langLabel.textContent = translations[currentLang].langSelect;
  259. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  260. filterMenu.innerHTML = '';
  261. filterMenu.appendChild(langLabel);
  262. filterMenu.appendChild(langSelect);
  263. filterMenu.appendChild(colorPickerLabel);
  264. filterMenu.appendChild(colorPicker);
  265. renderFilters();
  266. }
  267.  
  268. // Обработчик смены языка
  269. langSelect.addEventListener('change', () => {
  270. updateLanguage(langSelect.value);
  271. });
  272.  
  273. // Рендеринг фильтров
  274. function renderFilters() {
  275. const filters = translations[currentLang].filters;
  276. filters.forEach(filter => {
  277. const filterItem = document.createElement('div');
  278. filterItem.className = 'filter-item';
  279.  
  280. const checkbox = document.createElement('input');
  281. checkbox.type = 'checkbox';
  282. checkbox.checked = savedFilterStates[filter.value];
  283. checkbox.id = `filter-${filter.value}`;
  284.  
  285. const label = document.createElement('label');
  286. label.htmlFor = `filter-${filter.value}`;
  287. label.textContent = filter.name;
  288.  
  289. const sliderLabel = document.createElement('label');
  290. sliderLabel.className = 'filter-slider-label';
  291. sliderLabel.textContent = translations[currentLang].sliderLabel;
  292.  
  293. const slider = document.createElement('input');
  294. slider.type = 'range';
  295. slider.className = 'filter-slider';
  296. slider.min = filter.min;
  297. slider.max = filter.max;
  298. slider.step = filter.step;
  299. slider.value = savedFilterValues[filter.value];
  300.  
  301. if (checkbox.checked && filter.hasSlider) {
  302. slider.style.display = 'block';
  303. sliderLabel.style.display = 'block';
  304. }
  305.  
  306. checkbox.addEventListener('change', () => {
  307. savedFilterStates[filter.value] = checkbox.checked;
  308. localStorage.setItem('codeFilterStates', JSON.stringify(savedFilterStates));
  309. if (filter.hasSlider) {
  310. slider.style.display = checkbox.checked ? 'block' : 'none';
  311. sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
  312. }
  313. applyFilters();
  314. });
  315.  
  316. slider.addEventListener('input', () => {
  317. savedFilterValues[filter.value] = slider.value;
  318. localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues));
  319. applyFilters();
  320. });
  321.  
  322. filterItem.appendChild(checkbox);
  323. filterItem.appendChild(label);
  324. filterMenu.appendChild(filterItem);
  325. filterMenu.appendChild(sliderLabel);
  326. filterMenu.appendChild(slider);
  327. });
  328. }
  329.  
  330. // Инициализация
  331. filterMenu.appendChild(langLabel);
  332. filterMenu.appendChild(langSelect);
  333. filterMenu.appendChild(colorPickerLabel);
  334. filterMenu.appendChild(colorPicker);
  335. renderFilters();
  336.  
  337. // Обработчики для кнопки
  338. filterBtn.addEventListener('click', () => {
  339. filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
  340. });
  341.  
  342. document.addEventListener('click', (e) => {
  343. if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
  344. filterMenu.style.display = 'none';
  345. }
  346. });
  347.  
  348. headerBlock.style.position = 'relative';
  349. headerBlock.appendChild(filterBtn);
  350. headerBlock.appendChild(filterMenu);
  351. }
  352.  
  353. // Функция логирования структуры DOM для отладки
  354. function logDomStructure(headerBlock) {
  355. console.log('Заголовок блока кода:', headerBlock.outerHTML);
  356. console.log('Следующий элемент (nextElementSibling):', headerBlock.nextElementSibling?.outerHTML || 'Не найден');
  357. console.log('Родительский элемент:', headerBlock.parentElement.outerHTML);
  358. console.log('Все <code> в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('code')).map(el => el.outerHTML));
  359. console.log('Все <div> с overflow-x: auto в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('div[style*="overflow-x: auto"]')).map(el => el.outerHTML));
  360. }
  361.  
  362. // Функция поиска и обработки блоков кода
  363. function processCodeBlocks() {
  364. // Селекторы для заголовков блоков кода
  365. const headerSelectors = [
  366. 'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs', // Основной: span с языком
  367. 'div[class*="flex"][class*="bg-surface"] > span', // Резервный: flex и bg-surface
  368. 'div > span[class*="font-mono"]' // Общий: любой span с font-mono
  369. ];
  370.  
  371. let headerBlocks = [];
  372. for (const selector of headerSelectors) {
  373. const headers = Array.from(document.querySelectorAll(selector))
  374. .filter(span => {
  375. const text = span.textContent.toLowerCase();
  376. return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
  377. })
  378. .map(span => span.closest('div'));
  379. headerBlocks.push(...headers);
  380. if (headerBlocks.length > 0) break; // Прерываем, если нашли заголовки
  381. }
  382. headerBlocks = [...new Set(headerBlocks)]; // Удаляем дубликаты
  383. console.log('Найдено заголовков блоков кода:', headerBlocks.length);
  384.  
  385. headerBlocks.forEach(headerBlock => {
  386. // Проверяем наличие span с языком
  387. const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
  388. if (!langSpan) {
  389. console.log('Заголовок без span с языком:', headerBlock);
  390. return;
  391. }
  392.  
  393. // Пытаемся найти контейнер кода
  394. let codeContainer = null;
  395.  
  396. // Вариант 1: Следующий элемент
  397. if (headerBlock.nextElementSibling?.querySelector('code')) {
  398. codeContainer = headerBlock.nextElementSibling;
  399. }
  400.  
  401. // Вариант 2: div с overflow-x: auto в родителе
  402. if (!codeContainer) {
  403. codeContainer = headerBlock.parentElement.querySelector('div[style*="overflow-x: auto"]');
  404. }
  405.  
  406. // Вариант 3: div с code в родителе
  407. if (!codeContainer) {
  408. codeContainer = headerBlock.parentElement.querySelector('div > code')?.parentElement;
  409. }
  410.  
  411. // Вариант 4: pre с code в родителе
  412. if (!codeContainer) {
  413. codeContainer = headerBlock.parentElement.querySelector('pre > code')?.parentElement;
  414. }
  415.  
  416. // Вариант 5: Любой div с background hsl в родителе
  417. if (!codeContainer) {
  418. codeContainer = headerBlock.parentElement.querySelector('div[style*="background: hsl"]');
  419. }
  420.  
  421. if (codeContainer) {
  422. console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML);
  423. addFilterMenu(headerBlock, codeContainer);
  424.  
  425. // Применяем сохраненные фильтры и цвет комментариев
  426. const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
  427. const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
  428. const filters = [
  429. { value: 'invert' },
  430. { value: 'sepia' },
  431. { value: 'grayscale' },
  432. { value: 'blur', unit: 'px' },
  433. { value: 'contrast' },
  434. { value: 'brightness' },
  435. { value: 'hue-rotate', unit: 'deg' },
  436. { value: 'saturate' },
  437. { value: 'opacity' }
  438. ];
  439. const activeFilters = filters
  440. .filter(filter => savedFilterStates[filter.value])
  441. .map(filter => {
  442. const unit = filter.unit || '';
  443. const value = savedFilterValues[filter.value] || (filter.value === 'blur' ? 2 : filter.value === 'brightness' ? 1.5 : filter.value === 'contrast' ? 2 : filter.value === 'hue-rotate' ? 90 : 1);
  444. return `${filter.value}(${value}${unit})`;
  445. });
  446. codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  447.  
  448. const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment');
  449. commentElements.forEach(element => {
  450. element.style.color = currentCommentColor;
  451. });
  452. } else {
  453. console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML);
  454. logDomStructure(headerBlock); // Логируем структуру для анализа
  455. }
  456. });
  457. }
  458.  
  459. // Инициализация
  460. setTimeout(processCodeBlocks, 2000); // Увеличил задержку для асинхронной загрузки
  461. processCodeBlocks();
  462.  
  463. // Наблюдатель за изменениями DOM
  464. const observer = new MutationObserver(() => {
  465. processCodeBlocks();
  466. });
  467. observer.observe(document.body, { childList: true, subtree: true, attributes: true });
  468. })();