Advertisement
kiranwayne

Venice Enhanced v0.5.3

Apr 30th, 2025 (edited)
211
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 35.01 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         Venice Enhanced
  3. // @namespace    http://tampermonkey.net/
  4. // @version      0.5.3
  5. // @description  Customize width, justification, add collapsable/movable Table of Contents (Questions only) with top offset on venice.ai. Handles message deletion & edits, and SPA navigation. Aligns scroll to top with offset. Show/hide via menu. Handles Shadow DOM. Optimized observer. Better ToC text extraction.
  6. // @author       kiranwayne
  7. // @match        https://venice.ai/*
  8. // @grant        GM_getValue
  9. // @grant        GM_setValue
  10. // @grant        GM_registerMenuCommand
  11. // @grant        GM_unregisterMenuCommand
  12. // @run-at       document-end
  13. // ==/UserScript==
  14.  
  15. (async () => {
  16.     'use strict';
  17.  
  18.     // --- Configuration & Constants ---
  19.     const SCRIPT_NAME = 'Venice Enhanced';
  20.     const SCRIPT_VERSION = '0.5.3'; // Updated version
  21.     const SCRIPT_AUTHOR = 'kiranwayne';
  22.  
  23.     const CONFIG_PREFIX = 'veniceEnhancedControls_v3_'; // Prefix for GM_setValue keys
  24.     const MAX_WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx';
  25.     const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth';
  26.     const JUSTIFY_KEY = CONFIG_PREFIX + 'justifyEnabled';
  27.     const UI_VISIBLE_KEY = CONFIG_PREFIX + 'uiVisible';
  28.     const TOC_VISIBLE_KEY = CONFIG_PREFIX + 'tocVisible';
  29.     const TOC_COLLAPSED_KEY = CONFIG_PREFIX + 'tocCollapsed';
  30.     const TOC_POS_TOP_KEY = CONFIG_PREFIX + 'tocPosTop';
  31.     const TOC_POS_LEFT_KEY = CONFIG_PREFIX + 'tocPosLeft';
  32.  
  33.     const WIDTH_STYLE_ID = 'vm-venice-width-style';
  34.     const JUSTIFY_STYLE_ID = 'vm-venice-justify-style';
  35.     const GLOBAL_STYLE_ID = 'vm-venice-global-style';
  36.     const SCROLL_MARGIN_STYLE_ID = 'vm-venice-scroll-margin-style';
  37.     const SETTINGS_PANEL_ID = 'venice-userscript-settings-panel';
  38.     const TOC_PANEL_ID = 'venice-userscript-toc-panel';
  39.     const TOC_LIST_ID = 'venice-userscript-toc-list';
  40.     const TOC_TOGGLE_BTN_ID = 'venice-userscript-toc-toggle-btn';
  41.     const TOC_HEADER_CLASS = 'venice-toc-header';
  42.  
  43.     // Selectors based on v0.6.0/0.6.1 logic
  44.     const CHAT_MESSAGES_AREA_SELECTOR = 'div.chat-messages-content';
  45.     const USER_MESSAGE_SENTRY_COMPONENT = 'UserMessage';
  46.     const AI_MESSAGE_AVATAR_SENTRY_COMPONENT = 'AssistantAvatar';
  47.     const USER_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(div[data-sentry-component="${USER_MESSAGE_SENTRY_COMPONENT}"])`;
  48.     const AI_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(div[data-sentry-component="${AI_MESSAGE_AVATAR_SENTRY_COMPONENT}"])`;
  49.     const USER_MESSAGE_CONTENT_COMPONENT_SELECTOR = `div[data-sentry-component="${USER_MESSAGE_SENTRY_COMPONENT}"]`;
  50.     const CONFIRM_EDIT_BUTTON_SELECTOR = 'button[aria-label="Confirm edits"]';
  51.     const EDIT_MESSAGE_INPUT_SELECTOR = 'div[aria-label="editable markdown"], textarea[placeholder*="Edit message"]';
  52.     const CHAT_CONTAINER_SELECTOR_FOR_OBSERVER = CHAT_MESSAGES_AREA_SELECTOR;
  53.  
  54.     const WIDTH_TARGET_SELECTOR = `${USER_MESSAGE_BLOCK_SELECTOR}, ${AI_MESSAGE_BLOCK_SELECTOR}`;
  55.     const JUSTIFY_TARGET_SELECTOR = CHAT_MESSAGES_AREA_SELECTOR;
  56.     const SCROLL_MARGIN_TARGET_SELECTOR = USER_MESSAGE_BLOCK_SELECTOR;
  57.  
  58.     const SCRIPT_DEFAULT_WIDTH_PX = 1000;
  59.     const MIN_WIDTH_PX = 500;
  60.     const MAX_WIDTH_PX = 2000;
  61.     const STEP_WIDTH_PX = 10;
  62.  
  63.     const TOC_MAX_TEXT_LENGTH = 60;
  64.     const TOC_DEBOUNCE_DELAY_MS = 500;
  65.     const URL_CHANGE_RESCAN_DELAY_MS = 300;
  66.     const TOC_DEFAULT_TOP_PX = 50;
  67.     const SCROLL_TARGET_TOP_MARGIN_PX = 70;
  68.  
  69.     let config = {
  70.         maxWidthPx: SCRIPT_DEFAULT_WIDTH_PX, useDefaultWidth: false, justifyEnabled: true,
  71.         uiVisible: false, tocVisible: true, tocCollapsed: false,
  72.         tocPosTop: TOC_DEFAULT_TOP_PX + 'px', tocPosLeft: ''
  73.     };
  74.  
  75.     let settingsPanel = null, widthSlider = null, widthLabel = null, widthInput = null;
  76.     let defaultWidthCheckbox = null, justifyCheckbox = null;
  77.     let menuCommandId_ToggleUI = null, menuCommandId_ToggleToc = null;
  78.     const allStyleRoots = new Set();
  79.     let tocPanel = null, tocList = null, tocToggleButton = null, tocHeader = null;
  80.     let messageCounterForTocIds = 0;
  81.  
  82.     let pageObserverInstance = null;
  83.     let chatContentObserverInstance = null;
  84.     let currentChatContainerElement = null;
  85.     let lastKnownHref = window.location.href;
  86.  
  87.     let isDragging = false, initialMouseX = 0, initialMouseY = 0, initialPanelX = 0, initialPanelY = 0;
  88.  
  89.     // --- Helper Functions ---
  90.     function debounce(func, wait) {
  91.         let timeout;
  92.         return function executedFunction(...args) {
  93.             const later = () => { clearTimeout(timeout); func(...args); };
  94.             clearTimeout(timeout); timeout = setTimeout(later, wait);
  95.         };
  96.     }
  97.  
  98.     async function loadSettings() {
  99.         config.maxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_WIDTH_PX);
  100.         config.maxWidthPx = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, config.maxWidthPx));
  101.         config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, false);
  102.         config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, true);
  103.         config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false);
  104.         config.tocVisible = await GM_getValue(TOC_VISIBLE_KEY, true);
  105.         config.tocCollapsed = await GM_getValue(TOC_COLLAPSED_KEY, false);
  106.  
  107.         const defaultLeftDynamic = Math.max(10, window.innerWidth - 300 - 20) + 'px';
  108.         config.tocPosTop = await GM_getValue(TOC_POS_TOP_KEY, TOC_DEFAULT_TOP_PX + 'px');
  109.         config.tocPosLeft = await GM_getValue(TOC_POS_LEFT_KEY, defaultLeftDynamic);
  110.  
  111.         if (typeof config.tocPosTop !== 'string' || !config.tocPosTop.endsWith('px')) { config.tocPosTop = TOC_DEFAULT_TOP_PX + 'px'; }
  112.         if (typeof config.tocPosLeft !== 'string' || !config.tocPosLeft.endsWith('px')) { config.tocPosLeft = defaultLeftDynamic; }
  113.     }
  114.     async function saveSetting(key, value) {
  115.         if (key === MAX_WIDTH_PX_KEY) {
  116.             const numValue = parseInt(value, 10);
  117.             if (!isNaN(numValue)) {
  118.                 const clampedValue = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, numValue));
  119.                 await GM_setValue(key, clampedValue); config.maxWidthPx = clampedValue;
  120.             } else { return; }
  121.         } else {
  122.              await GM_setValue(key, value);
  123.              if (key === USE_DEFAULT_WIDTH_KEY) config.useDefaultWidth = value;
  124.              else if (key === JUSTIFY_KEY) config.justifyEnabled = value;
  125.              else if (key === UI_VISIBLE_KEY) config.uiVisible = value;
  126.              else if (key === TOC_VISIBLE_KEY) config.tocVisible = value;
  127.              else if (key === TOC_COLLAPSED_KEY) config.tocCollapsed = value;
  128.         }
  129.     }
  130.     async function saveTocPosition(top, left) {
  131.          const topStr = typeof top === 'number' ? `${top}px` : top;
  132.          const leftStr = typeof left === 'number' ? `${left}px` : left;
  133.          if (topStr && topStr.endsWith('px')) { await GM_setValue(TOC_POS_TOP_KEY, topStr); config.tocPosTop = topStr; }
  134.          if (leftStr && leftStr.endsWith('px')) { await GM_setValue(TOC_POS_LEFT_KEY, leftStr); config.tocPosLeft = leftStr; }
  135.     }
  136.  
  137.     // --- Style Generation Functions ---
  138.     function getWidthCss() {
  139.         if (config.useDefaultWidth) return '';
  140.         return `${WIDTH_TARGET_SELECTOR} { max-width: ${config.maxWidthPx}px !important; margin-left: auto !important; margin-right: auto !important; }`;
  141.     }
  142.     function getJustifyCss() {
  143.         if (!config.justifyEnabled) return '';
  144.         return `${JUSTIFY_TARGET_SELECTOR} { text-align: justify !important; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; }`;
  145.     }
  146.     function getScrollMarginCss() {
  147.         return `${SCROLL_MARGIN_TARGET_SELECTOR} { scroll-margin-top: ${SCROLL_TARGET_TOP_MARGIN_PX}px !important; }`;
  148.     }
  149.     function getGlobalSpinnerCss() {
  150.         return `
  151.             #${SETTINGS_PANEL_ID} input[type=number] { -moz-appearance: textfield !important; }
  152.             #${SETTINGS_PANEL_ID} input[type=number]::-webkit-inner-spin-button,
  153.             #${SETTINGS_PANEL_ID} input[type=number]::-webkit-outer-spin-button { -webkit-appearance: inner-spin-button !important; opacity: 1 !important; cursor: pointer; }
  154.             #${TOC_PANEL_ID} {
  155.                 position: fixed; z-index: 9998; background: #2a2b32; color: #ECECF1; border: 1px solid #4a4b54;
  156.                 border-radius: 6px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
  157.                 max-height: 70vh; overflow: hidden; min-width: 200px; max-width: 300px;
  158.                 font-size: 0.9em; display: flex; flex-direction: column;
  159.                 user-select: none; -webkit-user-select: none; -ms-user-select: none;
  160.                 transition: opacity 0.3s ease-out; opacity: 1;
  161.             }
  162.             #${TOC_PANEL_ID}.hidden { opacity: 0; pointer-events: none; }
  163.             #${TOC_PANEL_ID} .${TOC_HEADER_CLASS} {
  164.                 display: flex; justify-content: space-between; align-items: center;
  165.                 margin-bottom: 8px; padding-bottom: 5px; border-bottom: 1px solid #4a4b54;
  166.                 flex-shrink: 0; cursor: move;
  167.             }
  168.             #${TOC_PANEL_ID} h5 { margin: 0; font-size: 1em; font-weight: bold; color: #FFFFFF; border-bottom: none; padding-bottom: 0; flex-grow: 1; text-align: center; }
  169.             #${TOC_TOGGLE_BTN_ID} { background: none; border: none; color: #b7b9cc; cursor: pointer; padding: 2px 4px; font-size: 1.2em; line-height: 1; border-radius: 3px; margin-left: 5px; }
  170.             #${TOC_TOGGLE_BTN_ID}:hover { background-color: #4a4b54; color: #FFFFFF; }
  171.             #${TOC_LIST_ID} { list-style: none; padding: 0; margin: 0; overflow-y: auto; flex-grow: 1; min-height: 0; user-select: text; -webkit-user-select: text; -ms-user-select: text; }
  172.             #${TOC_PANEL_ID}[data-collapsed="true"] #${TOC_LIST_ID} { display: none; }
  173.             #${TOC_PANEL_ID}:not([data-collapsed="true"]) #${TOC_LIST_ID} { display: block; }
  174.             #${TOC_LIST_ID} li { margin-bottom: 5px; }
  175.             #${TOC_LIST_ID} a { color: #b7b9cc; text-decoration: none; display: block; padding: 3px 5px; border-radius: 3px; transition: background-color 0.2s, color 0.2s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  176.             #${TOC_LIST_ID} a:hover { background-color: #4a4b54; color: #FFFFFF; }
  177.             #${TOC_LIST_ID} .toc-placeholder { color: #999; font-style: italic; padding: 3px 5px; }
  178.             #${TOC_PANEL_ID}.dragging { opacity: 0.85; border: 1px dashed #aaa; }
  179.         `;
  180.     }
  181.  
  182.     // --- Style Injection / Update / Removal Functions ---
  183.     function injectOrUpdateStyle(root, styleId, cssContent) {
  184.          if (!root) return; let style = root.querySelector(`#${styleId}`); if (cssContent) { if (!style) { style = document.createElement('style'); style.id = styleId; style.textContent = cssContent; if (root === document.head || (root.nodeType === Node.ELEMENT_NODE && root.shadowRoot === null) || root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { root.appendChild(style); } else if (root.shadowRoot) { root.shadowRoot.appendChild(style); } } else if (style.textContent !== cssContent) { style.textContent = cssContent; } } else { if (style) { style.remove(); } }
  185.      }
  186.     function applyGlobalHeadStyles() {
  187.         if (document.head) { injectOrUpdateStyle(document.head, GLOBAL_STYLE_ID, getGlobalSpinnerCss()); }
  188.     }
  189.     function applyWidthStyleToAllRoots() {
  190.         const widthCss = getWidthCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, WIDTH_STYLE_ID, widthCss); });
  191.     }
  192.     function applyJustificationStyleToAllRoots() {
  193.         const justifyCss = getJustifyCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, JUSTIFY_STYLE_ID, justifyCss); });
  194.     }
  195.     function applyScrollMarginStyleToAllRoots() {
  196.         const scrollMarginCss = getScrollMarginCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, SCROLL_MARGIN_STYLE_ID, scrollMarginCss); });
  197.     }
  198.  
  199.     // --- UI State Update ---
  200.     function updateUIState() {
  201.         if (settingsPanel && defaultWidthCheckbox && justifyCheckbox && widthSlider && widthLabel && widthInput) { defaultWidthCheckbox.checked = config.useDefaultWidth; const isCustomWidthEnabled = !config.useDefaultWidth; widthSlider.disabled = !isCustomWidthEnabled; widthInput.disabled = !isCustomWidthEnabled; widthLabel.style.opacity = isCustomWidthEnabled ? 1 : 0.5; widthSlider.style.opacity = isCustomWidthEnabled ? 1 : 0.5; widthInput.style.opacity = isCustomWidthEnabled ? 1 : 0.5; widthSlider.value = config.maxWidthPx; widthInput.value = config.maxWidthPx; widthLabel.textContent = `${config.maxWidthPx}px`; justifyCheckbox.checked = config.justifyEnabled; }
  202.         if (tocPanel) { tocPanel.setAttribute('data-collapsed', config.tocCollapsed.toString()); if(tocToggleButton) { tocToggleButton.textContent = config.tocCollapsed ? '⊕' : '⊖'; tocToggleButton.setAttribute('aria-label', config.tocCollapsed ? 'Expand List' : 'Collapse List'); } }
  203.     }
  204.  
  205.     // --- Click Outside Handler ---
  206.     async function handleClickOutside(event) {
  207.          let clickedOutsideSettings = false; let clickedOutsideToc = false; if (settingsPanel && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target)) { clickedOutsideSettings = true; } if (tocPanel && document.body.contains(tocPanel) && !tocPanel.contains(event.target)) { clickedOutsideToc = true; } if (clickedOutsideSettings && (clickedOutsideToc || !tocPanel || !config.tocVisible) && config.uiVisible) { await saveSetting(UI_VISIBLE_KEY, false); removeSettingsUI(); updateTampermonkeyMenu(); updateUIState(); }
  208.     }
  209.  
  210.     // --- Drag Handlers ---
  211.     function handleMouseDown(e) {
  212.         if (e.button !== 0 || e.target.closest('button')) return; if (!tocPanel) return; isDragging = true; initialMouseX = e.clientX; initialMouseY = e.clientY; const rect = tocPanel.getBoundingClientRect(); initialPanelX = rect.left; initialPanelY = rect.top; tocPanel.classList.add('dragging'); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault();
  213.     }
  214.     function handleMouseMove(e) {
  215.         if (!isDragging || !tocPanel) return; const deltaX = e.clientX - initialMouseX; const deltaY = e.clientY - initialMouseY; let newPanelX = initialPanelX + deltaX; let newPanelY = initialPanelY + deltaY; const panelWidth = tocPanel.offsetWidth; const panelHeight = tocPanel.offsetHeight; const winWidth = window.innerWidth; const winHeight = window.innerHeight; newPanelX = Math.max(0, Math.min(newPanelX, winWidth - panelWidth - 5)); newPanelY = Math.max(0, Math.min(newPanelY, winHeight - panelHeight - 5)); tocPanel.style.left = newPanelX + 'px'; tocPanel.style.top = newPanelY + 'px';
  216.     }
  217.     function handleMouseUp(e) {
  218.         if (!isDragging || !tocPanel) return; isDragging = false; tocPanel.classList.remove('dragging'); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); saveTocPosition(tocPanel.style.top, tocPanel.style.left);
  219.     }
  220.  
  221.     // --- UI Creation/Removal ---
  222.     function removeSettingsUI() {
  223.          if (document && (!tocPanel || !config.tocVisible)) { document.removeEventListener('click', handleClickOutside, true); } settingsPanel = document.getElementById(SETTINGS_PANEL_ID); if (settingsPanel) { settingsPanel.remove(); settingsPanel = null; widthSlider = null; widthLabel = null; widthInput = null; defaultWidthCheckbox = null; justifyCheckbox = null; } if (tocPanel && config.tocVisible && !document.onclick) { if (document) document.addEventListener('click', handleClickOutside, true); }
  224.     }
  225.     function createSettingsUI() {
  226.          if (document.getElementById(SETTINGS_PANEL_ID) || !config.uiVisible) return; if (!document.body) { console.warn(`[${SCRIPT_NAME}] document.body not found, cannot create Settings UI.`); return; } settingsPanel = document.createElement('div'); settingsPanel.id = SETTINGS_PANEL_ID; Object.assign(settingsPanel.style, { position: 'fixed', top: '10px', right: '10px', zIndex: '9999', display: 'block', background: '#343541', color: '#ECECF1', border: '1px solid #565869', borderRadius: '6px', padding: '15px', boxShadow: '0 4px 10px rgba(0,0,0,0.3)', minWidth: '280px' }); const headerDiv = document.createElement('div'); headerDiv.style.marginBottom = '10px'; headerDiv.style.paddingBottom = '10px'; headerDiv.style.borderBottom = '1px solid #565869'; const titleElement = document.createElement('h4'); titleElement.textContent = SCRIPT_NAME; Object.assign(titleElement.style, { margin: '0 0 5px 0', fontSize: '1.1em', fontWeight: 'bold', color: '#FFFFFF'}); const versionElement = document.createElement('p'); versionElement.textContent = `Version: ${SCRIPT_VERSION}`; Object.assign(versionElement.style, { margin: '0 0 2px 0', fontSize: '0.85em', opacity: '0.8'}); const authorElement = document.createElement('p'); authorElement.textContent = `Author: ${SCRIPT_AUTHOR}`; Object.assign(authorElement.style, { margin: '0', fontSize: '0.85em', opacity: '0.8'}); headerDiv.appendChild(titleElement); headerDiv.appendChild(versionElement); headerDiv.appendChild(authorElement); settingsPanel.appendChild(headerDiv); const widthSection = document.createElement('div'); widthSection.style.marginTop = '10px'; const defaultWidthDiv = document.createElement('div'); defaultWidthDiv.style.marginBottom = '10px'; defaultWidthCheckbox = document.createElement('input'); defaultWidthCheckbox.type = 'checkbox'; defaultWidthCheckbox.id = 'venice-userscript-defaultwidth-toggle'; const defaultWidthLabel = document.createElement('label'); defaultWidthLabel.htmlFor = 'venice-userscript-defaultwidth-toggle'; defaultWidthLabel.textContent = ' Use Venice Default Width'; defaultWidthLabel.style.cursor = 'pointer'; defaultWidthDiv.appendChild(defaultWidthCheckbox); defaultWidthDiv.appendChild(defaultWidthLabel); const customWidthControlsDiv = document.createElement('div'); customWidthControlsDiv.style.display = 'flex'; customWidthControlsDiv.style.alignItems = 'center'; customWidthControlsDiv.style.gap = '10px'; widthLabel = document.createElement('span'); widthLabel.style.minWidth = '50px'; widthLabel.style.fontFamily = 'monospace'; widthLabel.style.textAlign = 'right'; widthSlider = document.createElement('input'); widthSlider.type = 'range'; widthSlider.min = MIN_WIDTH_PX; widthSlider.max = MAX_WIDTH_PX; widthSlider.step = STEP_WIDTH_PX; widthSlider.style.flexGrow = '1'; widthSlider.style.verticalAlign = 'middle'; widthInput = document.createElement('input'); widthInput.type = 'number'; widthInput.min = MIN_WIDTH_PX; widthInput.max = MAX_WIDTH_PX; widthInput.step = STEP_WIDTH_PX; widthInput.style.width = '60px'; widthInput.style.verticalAlign = 'middle'; widthInput.style.padding = '2px 4px'; widthInput.style.background = '#202123'; widthInput.style.color = '#ECECF1'; widthInput.style.border = '1px solid #565869'; widthInput.style.borderRadius = '4px'; customWidthControlsDiv.appendChild(widthLabel); customWidthControlsDiv.appendChild(widthSlider); customWidthControlsDiv.appendChild(widthInput); widthSection.appendChild(defaultWidthDiv); widthSection.appendChild(customWidthControlsDiv); const justifySection = document.createElement('div'); justifySection.style.borderTop = '1px solid #565869'; justifySection.style.paddingTop = '15px'; justifySection.style.marginTop = '15px'; justifyCheckbox = document.createElement('input'); justifyCheckbox.type = 'checkbox'; justifyCheckbox.id = 'venice-userscript-justify-toggle'; const justifyLabel = document.createElement('label'); justifyLabel.htmlFor = 'venice-userscript-justify-toggle'; justifyLabel.textContent = ' Enable Text Justification'; justifyLabel.style.cursor = 'pointer'; justifySection.appendChild(justifyCheckbox); justifySection.appendChild(justifyLabel); settingsPanel.appendChild(widthSection); settingsPanel.appendChild(justifySection); document.body.appendChild(settingsPanel); defaultWidthCheckbox.addEventListener('change', async (e) => { await saveSetting(USE_DEFAULT_WIDTH_KEY, e.target.checked); applyWidthStyleToAllRoots(); updateUIState(); }); widthSlider.addEventListener('input', (e) => { const nw = parseInt(e.target.value, 10); config.maxWidthPx = nw; if (widthLabel) widthLabel.textContent = `${nw}px`; if (widthInput) widthInput.value = nw; if (!config.useDefaultWidth) applyWidthStyleToAllRoots(); }); widthSlider.addEventListener('change', async (e) => { if (!config.useDefaultWidth) { const fw = parseInt(e.target.value, 10); await saveSetting(MAX_WIDTH_PX_KEY, fw); } }); widthInput.addEventListener('input', (e) => { let nw = parseInt(e.target.value, 10); if (isNaN(nw)) return; nw = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, nw)); config.maxWidthPx = nw; if (widthLabel) widthLabel.textContent = `${nw}px`; if (widthSlider) widthSlider.value = nw; if (!config.useDefaultWidth) applyWidthStyleToAllRoots(); }); widthInput.addEventListener('change', async (e) => { let fw = parseInt(e.target.value, 10); if (isNaN(fw)) { fw = config.maxWidthPx; } fw = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, fw)); e.target.value = fw; if (widthSlider) widthSlider.value = fw; if (widthLabel) widthLabel.textContent = `${fw}px`; if (!config.useDefaultWidth) { await saveSetting(MAX_WIDTH_PX_KEY, fw); applyWidthStyleToAllRoots(); } }); justifyCheckbox.addEventListener('change', async (e) => { await saveSetting(JUSTIFY_KEY, e.target.checked); applyJustificationStyleToAllRoots(); }); updateUIState(); if (document && (!tocPanel || !config.tocVisible)) { document.addEventListener('click', handleClickOutside, true); } applyGlobalHeadStyles();
  227.     }
  228.     function removeTocUI() {
  229.          if (document && (!settingsPanel || !config.uiVisible)) { document.removeEventListener('click', handleClickOutside, true); } if (tocHeader) { tocHeader.removeEventListener('mousedown', handleMouseDown); tocHeader = null; } tocPanel = document.getElementById(TOC_PANEL_ID); if (tocPanel) { tocPanel.remove(); tocPanel = null; tocList = null; tocToggleButton = null; } if (settingsPanel && config.uiVisible && !document.onclick) { if (document) document.addEventListener('click', handleClickOutside, true); }
  230.     }
  231.     function createTocUI() {
  232.         if (document.getElementById(TOC_PANEL_ID) || !config.tocVisible) return; if (!document.body) { console.warn(`[${SCRIPT_NAME}] document.body not found, cannot create ToC UI.`); return; } tocPanel = document.createElement('div'); tocPanel.id = TOC_PANEL_ID; tocPanel.style.top = config.tocPosTop; tocPanel.style.left = config.tocPosLeft; tocHeader = document.createElement('div'); tocHeader.className = TOC_HEADER_CLASS; const title = document.createElement('h5'); title.textContent = 'Table of Contents'; tocHeader.appendChild(title); tocToggleButton = document.createElement('button'); tocToggleButton.id = TOC_TOGGLE_BTN_ID; tocToggleButton.type = 'button'; tocToggleButton.addEventListener('click', async () => { const newState = !config.tocCollapsed; await saveSetting(TOC_COLLAPSED_KEY, newState); updateUIState(); }); tocHeader.appendChild(tocToggleButton); tocHeader.addEventListener('mousedown', handleMouseDown); tocPanel.appendChild(tocHeader); tocList = document.createElement('ul'); tocList.id = TOC_LIST_ID; tocPanel.appendChild(tocList); document.body.appendChild(tocPanel); scanMessagesAndBuildToc(); updateUIState(); applyGlobalHeadStyles(); if (document && (!settingsPanel || !config.uiVisible)) { document.addEventListener('click', handleClickOutside, true); }
  233.     }
  234.  
  235.     // --- ToC Logic ---
  236.     function scrollToMessage(event) {
  237.         event.preventDefault(); const targetId = event.currentTarget.getAttribute('data-target-id'); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); targetElement.style.transition = 'background-color 0.1s ease-in-out'; targetElement.style.backgroundColor = 'rgba(255, 255, 0, 0.1)'; setTimeout(() => { if(targetElement) { targetElement.style.backgroundColor = ''; } }, 500); } else { console.warn(`[${SCRIPT_NAME}] Target element ${targetId} not found for scrolling.`); }
  238.     }
  239.     const scanMessagesAndBuildToc = debounce(() => {
  240.         if (!tocList || !config.tocVisible) return; tocList.innerHTML = ''; messageCounterForTocIds = 0;
  241.         const allMessageBlocksNodeList = document.querySelectorAll(WIDTH_TARGET_SELECTOR);
  242.         const allMessageBlocksArray = Array.from(allMessageBlocksNodeList);
  243.         const sortedMessageBlocks = allMessageBlocksArray.sort((a, b) => { if (a === b) return 0; const position = a.compareDocumentPosition(b); if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; });
  244.         let questionCount = 0;
  245.         sortedMessageBlocks.forEach(messageBlockContainer => {
  246.             messageCounterForTocIds++; const messageId = `ve-msg-${messageCounterForTocIds}`;
  247.             if (!messageBlockContainer.id || !messageBlockContainer.id.startsWith('ve-msg-')) { messageBlockContainer.id = messageId; }
  248.             const userMessageContentElement = messageBlockContainer.querySelector(USER_MESSAGE_CONTENT_COMPONENT_SELECTOR);
  249.             if (userMessageContentElement) {
  250.                 questionCount++; let extractedText = '';
  251.                 const firstP = userMessageContentElement.querySelector('p:first-of-type');
  252.                 if (firstP && (firstP.textContent || '').trim()) { extractedText = (firstP.textContent || '').trim(); }
  253.                 if (!extractedText) {
  254.                     const firstListItem = userMessageContentElement.querySelector('ol > li:first-of-type, ul > li:first-of-type');
  255.                     if (firstListItem) {
  256.                         const pInLi = firstListItem.querySelector('p');
  257.                         if (pInLi && (pInLi.textContent || '').trim()) { extractedText = (pInLi.textContent || '').trim(); }
  258.                         else { const liClone = firstListItem.cloneNode(true); liClone.querySelectorAll('ol, ul, pre, code, img, figure, div[class*="code"], span[class*="code"], button, svg, a[href="#"]').forEach(el => el.remove()); extractedText = (liClone.textContent || '').trim().replace(/^\d+\.\s*/, ''); }
  259.                     }
  260.                 }
  261.                 if (!extractedText) { const contentClone = userMessageContentElement.cloneNode(true); contentClone.querySelectorAll('pre, code, img, figure, div[class*="code"], span[class*="code"], button, svg, a[href="#"]').forEach(el => el.remove()); extractedText = (contentClone.textContent || '').trim().replace(/^\d+\.\s*/, ''); }
  262.                 if (!extractedText) { extractedText = `(Question ${questionCount})`; }
  263.                 if (extractedText.length > TOC_MAX_TEXT_LENGTH) { extractedText = extractedText.substring(0, TOC_MAX_TEXT_LENGTH) + '...'; }
  264.                 const listItem = document.createElement('li'); const link = document.createElement('a'); link.href = `#${messageBlockContainer.id}`; link.setAttribute('data-target-id', messageBlockContainer.id); link.appendChild(document.createTextNode(extractedText)); link.addEventListener('click', scrollToMessage); listItem.appendChild(link); tocList.appendChild(listItem);
  265.             }
  266.         });
  267.         if (questionCount === 0 && tocList) { const noMessagesItem = document.createElement('li'); noMessagesItem.textContent = 'Empty (No user messages found)'; noMessagesItem.className = 'toc-placeholder'; tocList.appendChild(noMessagesItem); }
  268.         updateUIState();
  269.     }, TOC_DEBOUNCE_DELAY_MS);
  270.  
  271.     // --- Tampermonkey Menu ---
  272.     function updateTampermonkeyMenu() {
  273.         if (menuCommandId_ToggleUI !== null && typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(menuCommandId_ToggleUI); } catch (e) {} menuCommandId_ToggleUI = null; } if (menuCommandId_ToggleToc !== null && typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(menuCommandId_ToggleToc); } catch (e) {} menuCommandId_ToggleToc = null; } if (typeof GM_registerMenuCommand === 'function') { const labelUI = config.uiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; menuCommandId_ToggleUI = GM_registerMenuCommand(labelUI, async () => { const newState = !config.uiVisible; await saveSetting(UI_VISIBLE_KEY, newState); if (newState) { createSettingsUI(); } else { removeSettingsUI(); } updateTampermonkeyMenu(); updateUIState(); }); const labelToc = config.tocVisible ? 'Hide Table of Contents' : 'Show Table of Contents'; menuCommandId_ToggleToc = GM_registerMenuCommand(labelToc, async () => { const newState = !config.tocVisible; await saveSetting(TOC_VISIBLE_KEY, newState); if (newState) { createTocUI(); } else { removeTocUI(); } updateTampermonkeyMenu(); }); }
  274.     }
  275.  
  276.     // --- Shadow DOM Handling ---
  277.     function getShadowRoot(element) { try { return element.shadowRoot; } catch (e) { return null; } }
  278.     function processElement(element) {
  279.         const shadow = getShadowRoot(element); if (shadow && shadow.nodeType === Node.DOCUMENT_FRAGMENT_NODE && !allStyleRoots.has(shadow)) { allStyleRoots.add(shadow); injectOrUpdateStyle(shadow, WIDTH_STYLE_ID, getWidthCss()); injectOrUpdateStyle(shadow, JUSTIFY_STYLE_ID, getJustifyCss()); injectOrUpdateStyle(shadow, SCROLL_MARGIN_STYLE_ID, getScrollMarginCss()); return true; } return false;
  280.     }
  281.  
  282.     // --- Observer Setup and SPA Navigation Handling ---
  283.     const chatContentObserverCallback = (mutations) => {
  284.         let potentialMessageChanged = false;
  285.         mutations.forEach((mutation) => {
  286.             mutation.addedNodes.forEach((node) => {
  287.                 if (node.nodeType === Node.ELEMENT_NODE) {
  288.                     try { const elementsToCheck = [node, ...node.querySelectorAll('*')]; elementsToCheck.forEach(el => { processElement(el); }); } catch (e) { }
  289.                     if (node.matches && (node.matches(USER_MESSAGE_BLOCK_SELECTOR) || node.matches(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
  290.                     else if (node.querySelector && (node.querySelector(USER_MESSAGE_BLOCK_SELECTOR) || node.querySelector(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
  291.                 }
  292.             });
  293.             mutation.removedNodes.forEach((node) => {
  294.                 if (node.nodeType === Node.ELEMENT_NODE) {
  295.                     if (node.matches && (node.matches(USER_MESSAGE_BLOCK_SELECTOR) || node.matches(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
  296.                     else if (node.querySelector && (node.querySelector(USER_MESSAGE_BLOCK_SELECTOR) || node.querySelector(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
  297.                 }
  298.             });
  299.         });
  300.         if (potentialMessageChanged && config.tocVisible) {
  301.             scanMessagesAndBuildToc();
  302.         }
  303.     };
  304.  
  305.     function ensureChatContentObserverIsActive(targetChatElement) {
  306.         if (chatContentObserverInstance && currentChatContainerElement === targetChatElement) {
  307.             return;
  308.         }
  309.         if (chatContentObserverInstance) {
  310.             chatContentObserverInstance.disconnect();
  311.         }
  312.  
  313.         currentChatContainerElement = targetChatElement;
  314.         chatContentObserverInstance = new MutationObserver(chatContentObserverCallback);
  315.         chatContentObserverInstance.observe(targetChatElement, { childList: true, subtree: true });
  316.  
  317.         if (config.tocVisible) {
  318.             scanMessagesAndBuildToc();
  319.         }
  320.     }
  321.  
  322.     function startPageObserver() {
  323.         const bodyElement = document.body;
  324.         if (!bodyElement) {
  325.             setTimeout(startPageObserver, 1000);
  326.             return;
  327.         }
  328.  
  329.         pageObserverInstance = new MutationObserver((mutations) => {
  330.             const chatContainerElement = document.querySelector(CHAT_CONTAINER_SELECTOR_FOR_OBSERVER);
  331.  
  332.             if (chatContainerElement) {
  333.                 if (currentChatContainerElement !== chatContainerElement) {
  334.                     ensureChatContentObserverIsActive(chatContainerElement);
  335.                 }
  336.             } else {
  337.                 if (currentChatContainerElement) {
  338.                     if (chatContentObserverInstance) chatContentObserverInstance.disconnect();
  339.                     chatContentObserverInstance = null;
  340.                     currentChatContainerElement = null;
  341.                     if (tocList && config.tocVisible) {
  342.                         tocList.innerHTML = '<li class="toc-placeholder">Chat not active or empty.</li>';
  343.                     }
  344.                 }
  345.             }
  346.         });
  347.  
  348.         pageObserverInstance.observe(bodyElement, { childList: true, subtree: true });
  349.  
  350.         const initialChatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR_FOR_OBSERVER);
  351.         if (initialChatContainer) {
  352.             ensureChatContentObserverIsActive(initialChatContainer);
  353.         } else {
  354.              if (tocList && config.tocVisible) {
  355.                  // tocList.innerHTML = '<li class="toc-placeholder">Waiting for chat...</li>';
  356.              }
  357.         }
  358.     }
  359.  
  360.     function handleUrlChangeForSPA() {
  361.         if (window.location.href !== lastKnownHref) {
  362.             lastKnownHref = window.location.href;
  363.             if (config.tocVisible) {
  364.                 setTimeout(scanMessagesAndBuildToc, URL_CHANGE_RESCAN_DELAY_MS);
  365.             }
  366.         }
  367.     }
  368.  
  369.  
  370.     // --- Initialization ---
  371.     console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} starting...`);
  372.  
  373.     if (document.head) allStyleRoots.add(document.head);
  374.     else { const rootNode = document.documentElement || document; allStyleRoots.add(rootNode); }
  375.  
  376.     await loadSettings();
  377.  
  378.     applyGlobalHeadStyles(); applyWidthStyleToAllRoots();
  379.     applyJustificationStyleToAllRoots(); applyScrollMarginStyleToAllRoots();
  380.  
  381.     try { document.querySelectorAll('*').forEach(el => { processElement(el); }); }
  382.     catch (e) { console.error(`[${SCRIPT_NAME}] Error during initial Shadow DOM scan:`, e); }
  383.  
  384.     if (config.uiVisible) createSettingsUI();
  385.     if (config.tocVisible) createTocUI();
  386.  
  387.     updateTampermonkeyMenu();
  388.  
  389.     document.addEventListener('click', (event) => {
  390.         const confirmButton = event.target.closest(CONFIRM_EDIT_BUTTON_SELECTOR);
  391.         if (confirmButton) { setTimeout(scanMessagesAndBuildToc, 250); }
  392.     }, true);
  393.     document.addEventListener('keydown', (event) => {
  394.         if (event.key === 'Enter' && !event.shiftKey) {
  395.             const activeElement = document.activeElement;
  396.             if (activeElement && activeElement.closest(EDIT_MESSAGE_INPUT_SELECTOR)) {
  397.                  setTimeout(scanMessagesAndBuildToc, 250);
  398.             }
  399.         }
  400.     }, true);
  401.  
  402.     setTimeout(startPageObserver, 1000);
  403.  
  404.     const originalPushState = history.pushState;
  405.     history.pushState = function(...args) {
  406.         const result = originalPushState.apply(this, args);
  407.         window.dispatchEvent(new Event('pushstate'));
  408.         return result;
  409.     };
  410.     const originalReplaceState = history.replaceState;
  411.     history.replaceState = function(...args) {
  412.         const result = originalReplaceState.apply(this, args);
  413.         window.dispatchEvent(new Event('replacestate'));
  414.         return result;
  415.     };
  416.     window.addEventListener('popstate', handleUrlChangeForSPA);
  417.     window.addEventListener('pushstate', handleUrlChangeForSPA);
  418.     window.addEventListener('replacestate', handleUrlChangeForSPA);
  419.  
  420.     setTimeout(scanMessagesAndBuildToc, 1200);
  421.  
  422.     // console.log(`[${SCRIPT_NAME}] Initialization complete.`);
  423. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement