Grok Filter Code Menu 1.21.25

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

  1. // ==UserScript==
  2. // @name Grok Filter Code Menu 1.21.25
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.21.25
  5. // @description Adds a filter menu to the code blocks in the Grok chat while maintaining the settings
  6. // @author tapeavion
  7. // @license MIT
  8. // @match https://grok.com/c/*
  9. // @match https://grok.com/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
  11. // @grant GM_addStyle
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // Добавление стилей
  20. const styles = `
  21. .filter-menu-btn {
  22. position: absolute;
  23. top: 4px;
  24. right: 485px;
  25. height: 31px !important;
  26. z-index: 1;
  27. padding: 4px 12px;
  28. background: #1d5752;
  29. color: #dcfff9;
  30. border: 2px solid aquamarine;
  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. }
  39. .filter-menu {
  40. position: fixed;
  41. z-index: 100000 !important;
  42. top: 100px;
  43. right: 4px;
  44. display: none;
  45. box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 4px;
  46. width: 255px;
  47. max-height: 750px;
  48. overflow-y: auto;
  49. background: rgb(45, 45, 45);
  50. border-radius: 8px;
  51. padding: 5px;
  52. border-width: 2px !important;
  53. border-style: solid !important;
  54. border-color: rgb(93, 255, 247) !important;
  55. border-image: initial !important;
  56. }
  57. .filter-item {
  58. display: flex;
  59. align-items: center;
  60. padding: 5px 0;
  61. color: #a0a0a0;
  62. font-size: 12px;
  63. }
  64. .filter-item input[type="checkbox"] {
  65. margin-right: 5px;
  66. width: 29px;
  67. height: 29px;
  68. }
  69. .filter-item label {
  70. flex: 1;
  71. cursor: pointer;
  72. }
  73. label {
  74. color: #80ebff;
  75. }
  76. label.color-picker-label {
  77. color: #40bb97;
  78. }
  79. .color-picker {
  80. margin: 5px 0 5px 20px;
  81. width: calc(100% - 20px);
  82. }
  83. .color-picker-label {
  84. display: block;
  85. color: #a0a0a0;
  86. font-size: 12px;
  87. margin: 2px 0 2px 20px;
  88. }
  89. button.inline-flex {
  90. background-color: #1d5752 !important;
  91. opacity: 0;
  92. animation: fadeIn 1s ease-in-out forwards;
  93. }
  94. button.inline-flex:hover {
  95. background-color: #1d5752 !important;
  96. opacity: 1;
  97. }
  98. @keyframes fadeIn {
  99. 0% { opacity: 0; }
  100. 100% { opacity: 1; }
  101. }
  102. .filter-slider {
  103. display: none;
  104. margin: 5px 0 5px 20px;
  105. width: calc(100% - 20px);
  106. background: #173034;
  107. -webkit-appearance: none;
  108. appearance: none;
  109. height: 15px;
  110. outline: none;
  111. right: 15px;
  112. position: relative;
  113. border-radius: 31px !important;
  114. }
  115. .filter-slider-label {
  116. display: none;
  117. color: #a0a0a0;
  118. font-size: 12px;
  119. margin: 2px 0 2px 20px;
  120. }
  121. .language-select {
  122. width: 100%;
  123. padding: 5px;
  124. margin-bottom: 5px;
  125. background: #3a3a3a;
  126. color: #a0a0a0;
  127. border: none;
  128. border-radius: 31px !important;
  129. font-size: 12px;
  130. }
  131. .filter-slider::-webkit-slider-thumb {
  132. -webkit-appearance: none;
  133. appearance: none;
  134. width: 20px;
  135. height: 20px;
  136. background: #35805f;
  137. border: 3px solid #164a53 !important;
  138. border-radius: 31px !important;
  139. cursor: pointer;
  140. box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
  141. }
  142. .filter-slider::-moz-range-thumb {
  143. width: 20px;
  144. height: 20px;
  145. background: #4CAF50;
  146. border-radius: 31px !important;
  147. cursor: pointer;
  148. border: none;
  149. }
  150. .filter-slider::-webkit-slider-runnable-track {
  151. background: linear-gradient(to right, #218a73 var(--value), #173034 var(--value));
  152. border-radius: 31px !important;
  153. border: 3px solid #55dfc5 !important;
  154. }
  155. .filter-slider::-moz-range-progress {
  156. background: #4CAF50;
  157. height: 6px;
  158. border-radius: 31px !important;
  159. }
  160. .reset-colorGrok6h63ew45-btn {
  161. background-color: #173034 !important;
  162. color: #218a73 !important;
  163. padding: 5px 10px;
  164. border: 1px solid #218a73 !important;
  165. border-radius: 4px;
  166. cursor: pointer;
  167. font-size: 16px;
  168. margin-top: 5px;
  169. display: block;
  170. width: 100%;
  171. text-align: center;
  172. }
  173. .reset-colorGrok6h63ew45-btn:hover {
  174. background-color: #719e8b !important;
  175. color: #051b16 !important;
  176. }
  177. `;
  178.  
  179. // Добавляем стили в документ
  180. const styleSheet = document.createElement('style');
  181. styleSheet.textContent = styles;
  182. document.head.appendChild(styleSheet);
  183.  
  184. // Определение языка пользователя
  185. const userLang = navigator.language || navigator.languages[0];
  186. const isRussian = userLang.startsWith('ru');
  187. const defaultLang = isRussian ? 'ru' : 'en';
  188.  
  189. // Локализация
  190. const translations = {
  191. ru: {
  192. filtersBtn: 'Фильтры',
  193. sliderLabel: 'Степень:',
  194. commentColorLabel: 'Цвет комментариев:',
  195. resetColorBtn: 'Сбросить цвет',
  196. filters: [
  197. { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  198. { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  199. { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  200. { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  201. { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  202. { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  203. { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  204. { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  205. { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  206. ],
  207. langSelect: 'Выберите язык:',
  208. langOptions: [
  209. { value: 'ru', label: 'Русский' },
  210. { value: 'en', label: 'English' }
  211. ]
  212. },
  213. en: {
  214. filtersBtn: 'Filters',
  215. sliderLabel: 'Level:',
  216. commentColorLabel: 'Comment color:',
  217. resetColorBtn: 'Reset color',
  218. filters: [
  219. { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  220. { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  221. { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  222. { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  223. { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  224. { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  225. { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  226. { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  227. { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  228. ],
  229. langSelect: 'Select language:',
  230. langOptions: [
  231. { value: 'ru', label: 'Русский' },
  232. { value: 'en', label: 'English' }
  233. ]
  234. }
  235. };
  236.  
  237. // Глобальный объект для хранения настроек и контейнеров
  238. const state = {
  239. settings: null,
  240. codeBlocks: new Map()
  241. };
  242.  
  243. // Загрузка настроек
  244. function loadSettings(callback) {
  245. const settings = {
  246. filterMenuLang: GM_getValue('filterMenuLang', defaultLang),
  247. codeFilterStates: GM_getValue('codeFilterStates', {}),
  248. codeFilterValues: GM_getValue('codeFilterValues', {}),
  249. commentColor: GM_getValue('commentColor', 'rgb(106, 153, 85)')
  250. };
  251. state.settings = settings;
  252. callback(settings);
  253. }
  254.  
  255. // Применение фильтров к конкретному блоку с retry
  256. function applyFilters(targetBlock, filterStates, filterValues, retries = 5) {
  257. const preElement = targetBlock.querySelector('pre');
  258. if (!preElement) {
  259. if (retries > 0) {
  260. setTimeout(() => applyFilters(targetBlock, filterStates, filterValues, retries - 1), 50);
  261. }
  262. return;
  263. }
  264. const filters = translations[state.settings.filterMenuLang].filters;
  265. const activeFilters = filters
  266. .filter(filter => filterStates[filter.value])
  267. .map(filter => {
  268. const unit = filter.unit || '';
  269. const value = filterValues[filter.value] || filter.default;
  270. return `${filter.value}(${value}${unit})`;
  271. });
  272. preElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  273. }
  274.  
  275. // Обновление фильтров для всех блоков
  276. function updateAllFilters() {
  277. state.codeBlocks.forEach(({codeContainer}) => {
  278. applyFilters(codeContainer, state.settings.codeFilterStates, state.settings.codeFilterValues);
  279. });
  280. GM_setValue('codeFilterStates', state.settings.codeFilterStates);
  281. GM_setValue('codeFilterValues', state.settings.codeFilterValues);
  282. }
  283.  
  284. // Применение цвета комментариев с retry
  285. function applyCommentColor(codeContainer, commentColor) {
  286. const commentElements = codeContainer.querySelectorAll(
  287. '.hljs-comment, span[style*="color: rgb(106, 153, 85)"], span[style*="color: #6a9955"], ' +
  288. 'span[style*="color: rgb(92, 99, 112)"]'
  289. );
  290.  
  291. if (commentElements.length > 0) {
  292. commentElements.forEach(element => {
  293. element.style.setProperty('color', commentColor, 'important');
  294. });
  295. } else {
  296. if (window.location.search.includes('debug')) {
  297. const lang = codeContainer.previousElementSibling?.querySelector('span.font-mono.text-xs')?.textContent || 'Неизвестный';
  298. const snippet = codeContainer.textContent.substring(0, 50) + '...';
  299. console.warn(`Не найдены элементы комментариев в контейнере (язык: ${lang}, фрагмент: "${snippet}"):`, codeContainer);
  300. }
  301. }
  302. }
  303.  
  304. // Обновление цвета комментариев для всех блоков
  305. function updateAllCommentColors() {
  306. requestAnimationFrame(() => {
  307. state.codeBlocks.forEach(({codeContainer}) => {
  308. applyCommentColor(codeContainer, state.settings.commentColor);
  309. });
  310. GM_setValue('commentColor', state.settings.commentColor);
  311. });
  312. }
  313.  
  314. // Функция debounce
  315. function debounce(func, wait) {
  316. let timeout;
  317. return function (...args) {
  318. clearTimeout(timeout);
  319. timeout = setTimeout(() => func.apply(this, args), wait);
  320. };
  321. }
  322.  
  323. // Создание меню фильтров
  324. function addFilterMenu(headerBlock, codeContainer) {
  325. if (headerBlock.querySelector('.filter-menu-btn')) return;
  326.  
  327. let currentLang = state.settings.filterMenuLang;
  328. let currentCommentColor = state.settings.commentColor;
  329. let savedFilterStates = { ...state.settings.codeFilterStates };
  330. let savedFilterValues = { ...state.settings.codeFilterValues };
  331.  
  332. const filterBtn = document.createElement('button');
  333. filterBtn.className = 'filter-menu-btn';
  334. filterBtn.textContent = translations[currentLang].filtersBtn;
  335.  
  336. const filterMenu = document.createElement('div');
  337. filterMenu.className = 'filter-menu';
  338.  
  339. const filters = translations[currentLang].filters;
  340. filters.forEach(filter => {
  341. if (!(filter.value in savedFilterStates)) {
  342. savedFilterStates[filter.value] = false;
  343. }
  344. if (!(filter.value in savedFilterValues)) {
  345. savedFilterValues[filter.value] = filter.default;
  346. }
  347. });
  348.  
  349. const langSelect = document.createElement('select');
  350. langSelect.className = 'language-select';
  351. const langLabel = document.createElement('label');
  352. langLabel.textContent = translations[currentLang].langSelect;
  353. langLabel.style.color = '#a0a0a0';
  354. langLabel.style.fontSize = '12px';
  355. langLabel.style.marginBottom = '2px';
  356. langLabel.style.display = 'block';
  357.  
  358. translations[currentLang].langOptions.forEach(option => {
  359. const opt = document.createElement('option');
  360. opt.value = option.value;
  361. opt.textContent = option.label;
  362. if (option.value === currentLang) opt.selected = true;
  363. langSelect.appendChild(opt);
  364. });
  365.  
  366. const colorPickerLabel = document.createElement('label');
  367. colorPickerLabel.className = 'color-picker-label';
  368. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  369.  
  370. const colorPicker = document.createElement('input');
  371. colorPicker.type = 'color';
  372. colorPicker.className = 'color-picker';
  373. colorPicker.value = currentCommentColor;
  374.  
  375. colorPicker.addEventListener('input', debounce(() => {
  376. currentCommentColor = colorPicker.value;
  377. state.settings.commentColor = currentCommentColor;
  378. requestAnimationFrame(() => {
  379. updateAllCommentColors();
  380. });
  381. }, 100));
  382.  
  383. function updateLanguage(lang) {
  384. currentLang = lang;
  385. state.settings.filterMenuLang = lang;
  386. GM_setValue('filterMenuLang', lang);
  387. filterBtn.textContent = translations[currentLang].filtersBtn;
  388. langLabel.textContent = translations[currentLang].langSelect;
  389. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  390. renderFilters();
  391. }
  392.  
  393. langSelect.addEventListener('change', () => updateLanguage(langSelect.value));
  394.  
  395. function renderFilters() {
  396. filterMenu.innerHTML = '';
  397. filterMenu.appendChild(langLabel);
  398. filterMenu.appendChild(langSelect);
  399. filterMenu.appendChild(colorPickerLabel);
  400. filterMenu.appendChild(colorPicker);
  401.  
  402. const resetColorBtn = document.createElement('button');
  403. resetColorBtn.className = 'reset-colorGrok6h63ew45-btn';
  404. resetColorBtn.textContent = translations[currentLang].resetColorBtn || 'Сбросить цвет';
  405. filterMenu.appendChild(resetColorBtn);
  406.  
  407. resetColorBtn.addEventListener('click', () => {
  408. const defaultColor = 'rgb(106, 153, 85)';
  409. currentCommentColor = defaultColor;
  410. state.settings.commentColor = defaultColor;
  411. colorPicker.value = '#6a9955';
  412. requestAnimationFrame(() => {
  413. updateAllCommentColors();
  414. });
  415. });
  416.  
  417. filters.forEach(filter => {
  418. const filterItem = document.createElement('div');
  419. filterItem.className = 'filter-item';
  420.  
  421. const checkbox = document.createElement('input');
  422. checkbox.type = 'checkbox';
  423. checkbox.checked = savedFilterStates[filter.value];
  424. checkbox.id = `filter-${filter.value}-${Date.now()}`;
  425.  
  426. const label = document.createElement('label');
  427. label.htmlFor = checkbox.id;
  428. label.textContent = filter.name;
  429.  
  430. const sliderLabel = document.createElement('label');
  431. sliderLabel.className = 'filter-slider-label';
  432. sliderLabel.textContent = translations[currentLang].sliderLabel;
  433.  
  434. const slider = document.createElement('input');
  435. slider.type = 'range';
  436. slider.className = 'filter-slider';
  437. slider.min = filter.min;
  438. slider.max = filter.max;
  439. slider.step = filter.step;
  440. slider.value = savedFilterValues[filter.value];
  441.  
  442. const updateSlider = () => {
  443. const value = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
  444. slider.style.background = `linear-gradient(to right, #218a73 ${value}%, #173034 ${value}%)`;
  445. };
  446.  
  447. slider.addEventListener('input', updateSlider);
  448. updateSlider();
  449.  
  450. if (checkbox.checked && filter.hasSlider) {
  451. slider.style.display = 'block';
  452. sliderLabel.style.display = 'block';
  453. }
  454.  
  455. checkbox.addEventListener('change', () => {
  456. savedFilterStates[filter.value] = checkbox.checked;
  457. state.settings.codeFilterStates = savedFilterStates;
  458. if (filter.hasSlider) {
  459. slider.style.display = checkbox.checked ? 'block' : 'none';
  460. sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
  461. }
  462. updateAllFilters();
  463. });
  464.  
  465. slider.addEventListener('input', () => {
  466. savedFilterValues[filter.value] = slider.value;
  467. state.settings.codeFilterValues = savedFilterValues;
  468. updateAllFilters();
  469. });
  470.  
  471. filterItem.appendChild(checkbox);
  472. filterItem.appendChild(label);
  473. filterMenu.appendChild(filterItem);
  474. filterMenu.appendChild(sliderLabel);
  475. filterMenu.appendChild(slider);
  476. });
  477. }
  478.  
  479. renderFilters();
  480.  
  481. filterBtn.addEventListener('click', () => {
  482. filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
  483. });
  484.  
  485. document.addEventListener('click', (e) => {
  486. if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
  487. filterMenu.style.display = 'none';
  488. }
  489. });
  490.  
  491. headerBlock.style.position = 'relative';
  492. headerBlock.appendChild(filterBtn);
  493. headerBlock.appendChild(filterMenu);
  494.  
  495. state.codeBlocks.set(headerBlock, { codeContainer, filterBtn, filterMenu });
  496.  
  497. applyFilters(codeContainer, savedFilterStates, savedFilterValues);
  498. applyCommentColor(codeContainer, currentCommentColor);
  499. }
  500.  
  501. // Обработка блоков кода
  502. function processCodeBlocks() {
  503. if (!state.settings) {
  504. loadSettings((settings) => {
  505. processCodeBlocksInternal(settings);
  506. });
  507. return;
  508. }
  509. processCodeBlocksInternal(state.settings);
  510. }
  511.  
  512. function findCodeContainer(bar) {
  513. let sibling = bar.nextElementSibling;
  514. while (sibling) {
  515. if (sibling.matches('div.shiki') || sibling.querySelector('pre, code') || sibling.matches('div[class*="code"]') || sibling.matches('div.sticky')) {
  516. if (sibling.matches('div.shiki')) {
  517. return sibling;
  518. }
  519. }
  520. sibling = sibling.nextElementSibling;
  521. }
  522. return null;
  523. }
  524.  
  525. function processCodeBlocksInternal(settings) {
  526. const headerSelectors = [
  527. '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',
  528. 'div.flex.flex-row.items-center.rounded-t-xl.bg-surface-l2.border > span.font-mono.text-xs',
  529. 'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
  530. 'div[class*="flex"][class*="bg-surface"] > span',
  531. 'div > span[class*="font-mono"]'
  532. ];
  533.  
  534. let headerBlocks = [];
  535. for (const selector of headerSelectors) {
  536. const headers = Array.from(document.querySelectorAll(selector))
  537. .filter(span => {
  538. const text = span.textContent.toLowerCase();
  539. return [
  540. 'javascript',
  541. 'typescript',
  542. 'text',
  543. 'css',
  544. 'html',
  545. 'python',
  546. 'java',
  547. 'cpp',
  548. 'json',
  549. 'bash',
  550. 'sql',
  551. 'xml',
  552. 'yaml',
  553. 'markdown'
  554. ].includes(text);
  555. })
  556. .map(span => span.closest('div'));
  557. headerBlocks.push(...headers);
  558. }
  559. headerBlocks = [...new Set(headerBlocks)];
  560.  
  561. state.codeBlocks.forEach((value, key) => {
  562. if (!document.body.contains(key)) {
  563. state.codeBlocks.delete(key);
  564. }
  565. });
  566.  
  567. headerBlocks.forEach(headerBlock => {
  568. if (state.codeBlocks.has(headerBlock)) return;
  569.  
  570. const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
  571. if (!langSpan) {
  572. console.log('Не найден span с языком:', headerBlock);
  573. return;
  574. }
  575.  
  576. const codeContainer = findCodeContainer(headerBlock);
  577.  
  578. if (codeContainer) {
  579. addFilterMenu(headerBlock, codeContainer);
  580. const codeObserver = new MutationObserver(() => {
  581. requestAnimationFrame(() => {
  582. applyCommentColor(codeContainer, state.settings.commentColor);
  583. });
  584. });
  585. codeObserver.observe(codeContainer, { childList: true, subtree: true, attributes: true });
  586. } else {
  587. console.log('Контейнер кода не найден для:', headerBlock);
  588. }
  589. });
  590. }
  591.  
  592. // Инициализация
  593. processCodeBlocks();
  594.  
  595. // Наблюдатель за изменениями DOM
  596. const observer = new MutationObserver((mutations) => {
  597. const relevantChanges = mutations.some(mutation => {
  598. return mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => {
  599. return node.nodeType === 1 && (node.getAttribute('data-testid') === 'code-block' || node.matches('div.message-bubble, .flex.flex-col.items-center, 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"]'));
  600. });
  601. });
  602. if (relevantChanges) {
  603. processCodeBlocks();
  604. setTimeout(processCodeBlocks, 1000);
  605. }
  606. });
  607. observer.observe(document.body, { childList: true, subtree: true });
  608. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址