Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Venice Enhanced
- // @namespace http://tampermonkey.net/
- // @version 0.5.3
- // @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.
- // @author kiranwayne
- // @match https://venice.ai/*
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @run-at document-end
- // ==/UserScript==
- (async () => {
- 'use strict';
- // --- Configuration & Constants ---
- const SCRIPT_NAME = 'Venice Enhanced';
- const SCRIPT_VERSION = '0.5.3'; // Updated version
- const SCRIPT_AUTHOR = 'kiranwayne';
- const CONFIG_PREFIX = 'veniceEnhancedControls_v3_'; // Prefix for GM_setValue keys
- const MAX_WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx';
- const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth';
- const JUSTIFY_KEY = CONFIG_PREFIX + 'justifyEnabled';
- const UI_VISIBLE_KEY = CONFIG_PREFIX + 'uiVisible';
- const TOC_VISIBLE_KEY = CONFIG_PREFIX + 'tocVisible';
- const TOC_COLLAPSED_KEY = CONFIG_PREFIX + 'tocCollapsed';
- const TOC_POS_TOP_KEY = CONFIG_PREFIX + 'tocPosTop';
- const TOC_POS_LEFT_KEY = CONFIG_PREFIX + 'tocPosLeft';
- const WIDTH_STYLE_ID = 'vm-venice-width-style';
- const JUSTIFY_STYLE_ID = 'vm-venice-justify-style';
- const GLOBAL_STYLE_ID = 'vm-venice-global-style';
- const SCROLL_MARGIN_STYLE_ID = 'vm-venice-scroll-margin-style';
- const SETTINGS_PANEL_ID = 'venice-userscript-settings-panel';
- const TOC_PANEL_ID = 'venice-userscript-toc-panel';
- const TOC_LIST_ID = 'venice-userscript-toc-list';
- const TOC_TOGGLE_BTN_ID = 'venice-userscript-toc-toggle-btn';
- const TOC_HEADER_CLASS = 'venice-toc-header';
- // Selectors based on v0.6.0/0.6.1 logic
- const CHAT_MESSAGES_AREA_SELECTOR = 'div.chat-messages-content';
- const USER_MESSAGE_SENTRY_COMPONENT = 'UserMessage';
- const AI_MESSAGE_AVATAR_SENTRY_COMPONENT = 'AssistantAvatar';
- const USER_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(div[data-sentry-component="${USER_MESSAGE_SENTRY_COMPONENT}"])`;
- const AI_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(div[data-sentry-component="${AI_MESSAGE_AVATAR_SENTRY_COMPONENT}"])`;
- const USER_MESSAGE_CONTENT_COMPONENT_SELECTOR = `div[data-sentry-component="${USER_MESSAGE_SENTRY_COMPONENT}"]`;
- const CONFIRM_EDIT_BUTTON_SELECTOR = 'button[aria-label="Confirm edits"]';
- const EDIT_MESSAGE_INPUT_SELECTOR = 'div[aria-label="editable markdown"], textarea[placeholder*="Edit message"]';
- const CHAT_CONTAINER_SELECTOR_FOR_OBSERVER = CHAT_MESSAGES_AREA_SELECTOR;
- const WIDTH_TARGET_SELECTOR = `${USER_MESSAGE_BLOCK_SELECTOR}, ${AI_MESSAGE_BLOCK_SELECTOR}`;
- const JUSTIFY_TARGET_SELECTOR = CHAT_MESSAGES_AREA_SELECTOR;
- const SCROLL_MARGIN_TARGET_SELECTOR = USER_MESSAGE_BLOCK_SELECTOR;
- const SCRIPT_DEFAULT_WIDTH_PX = 1000;
- const MIN_WIDTH_PX = 500;
- const MAX_WIDTH_PX = 2000;
- const STEP_WIDTH_PX = 10;
- const TOC_MAX_TEXT_LENGTH = 60;
- const TOC_DEBOUNCE_DELAY_MS = 500;
- const URL_CHANGE_RESCAN_DELAY_MS = 300;
- const TOC_DEFAULT_TOP_PX = 50;
- const SCROLL_TARGET_TOP_MARGIN_PX = 70;
- let config = {
- maxWidthPx: SCRIPT_DEFAULT_WIDTH_PX, useDefaultWidth: false, justifyEnabled: true,
- uiVisible: false, tocVisible: true, tocCollapsed: false,
- tocPosTop: TOC_DEFAULT_TOP_PX + 'px', tocPosLeft: ''
- };
- let settingsPanel = null, widthSlider = null, widthLabel = null, widthInput = null;
- let defaultWidthCheckbox = null, justifyCheckbox = null;
- let menuCommandId_ToggleUI = null, menuCommandId_ToggleToc = null;
- const allStyleRoots = new Set();
- let tocPanel = null, tocList = null, tocToggleButton = null, tocHeader = null;
- let messageCounterForTocIds = 0;
- let pageObserverInstance = null;
- let chatContentObserverInstance = null;
- let currentChatContainerElement = null;
- let lastKnownHref = window.location.href;
- let isDragging = false, initialMouseX = 0, initialMouseY = 0, initialPanelX = 0, initialPanelY = 0;
- // --- Helper Functions ---
- function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => { clearTimeout(timeout); func(...args); };
- clearTimeout(timeout); timeout = setTimeout(later, wait);
- };
- }
- async function loadSettings() {
- config.maxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_WIDTH_PX);
- config.maxWidthPx = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, config.maxWidthPx));
- config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, false);
- config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, true);
- config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false);
- config.tocVisible = await GM_getValue(TOC_VISIBLE_KEY, true);
- config.tocCollapsed = await GM_getValue(TOC_COLLAPSED_KEY, false);
- const defaultLeftDynamic = Math.max(10, window.innerWidth - 300 - 20) + 'px';
- config.tocPosTop = await GM_getValue(TOC_POS_TOP_KEY, TOC_DEFAULT_TOP_PX + 'px');
- config.tocPosLeft = await GM_getValue(TOC_POS_LEFT_KEY, defaultLeftDynamic);
- if (typeof config.tocPosTop !== 'string' || !config.tocPosTop.endsWith('px')) { config.tocPosTop = TOC_DEFAULT_TOP_PX + 'px'; }
- if (typeof config.tocPosLeft !== 'string' || !config.tocPosLeft.endsWith('px')) { config.tocPosLeft = defaultLeftDynamic; }
- }
- async function saveSetting(key, value) {
- if (key === MAX_WIDTH_PX_KEY) {
- const numValue = parseInt(value, 10);
- if (!isNaN(numValue)) {
- const clampedValue = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, numValue));
- await GM_setValue(key, clampedValue); config.maxWidthPx = clampedValue;
- } else { return; }
- } else {
- await GM_setValue(key, value);
- if (key === USE_DEFAULT_WIDTH_KEY) config.useDefaultWidth = value;
- else if (key === JUSTIFY_KEY) config.justifyEnabled = value;
- else if (key === UI_VISIBLE_KEY) config.uiVisible = value;
- else if (key === TOC_VISIBLE_KEY) config.tocVisible = value;
- else if (key === TOC_COLLAPSED_KEY) config.tocCollapsed = value;
- }
- }
- async function saveTocPosition(top, left) {
- const topStr = typeof top === 'number' ? `${top}px` : top;
- const leftStr = typeof left === 'number' ? `${left}px` : left;
- if (topStr && topStr.endsWith('px')) { await GM_setValue(TOC_POS_TOP_KEY, topStr); config.tocPosTop = topStr; }
- if (leftStr && leftStr.endsWith('px')) { await GM_setValue(TOC_POS_LEFT_KEY, leftStr); config.tocPosLeft = leftStr; }
- }
- // --- Style Generation Functions ---
- function getWidthCss() {
- if (config.useDefaultWidth) return '';
- return `${WIDTH_TARGET_SELECTOR} { max-width: ${config.maxWidthPx}px !important; margin-left: auto !important; margin-right: auto !important; }`;
- }
- function getJustifyCss() {
- if (!config.justifyEnabled) return '';
- return `${JUSTIFY_TARGET_SELECTOR} { text-align: justify !important; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; }`;
- }
- function getScrollMarginCss() {
- return `${SCROLL_MARGIN_TARGET_SELECTOR} { scroll-margin-top: ${SCROLL_TARGET_TOP_MARGIN_PX}px !important; }`;
- }
- function getGlobalSpinnerCss() {
- return `
- #${SETTINGS_PANEL_ID} input[type=number] { -moz-appearance: textfield !important; }
- #${SETTINGS_PANEL_ID} input[type=number]::-webkit-inner-spin-button,
- #${SETTINGS_PANEL_ID} input[type=number]::-webkit-outer-spin-button { -webkit-appearance: inner-spin-button !important; opacity: 1 !important; cursor: pointer; }
- #${TOC_PANEL_ID} {
- position: fixed; z-index: 9998; background: #2a2b32; color: #ECECF1; border: 1px solid #4a4b54;
- border-radius: 6px; padding: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
- max-height: 70vh; overflow: hidden; min-width: 200px; max-width: 300px;
- font-size: 0.9em; display: flex; flex-direction: column;
- user-select: none; -webkit-user-select: none; -ms-user-select: none;
- transition: opacity 0.3s ease-out; opacity: 1;
- }
- #${TOC_PANEL_ID}.hidden { opacity: 0; pointer-events: none; }
- #${TOC_PANEL_ID} .${TOC_HEADER_CLASS} {
- display: flex; justify-content: space-between; align-items: center;
- margin-bottom: 8px; padding-bottom: 5px; border-bottom: 1px solid #4a4b54;
- flex-shrink: 0; cursor: move;
- }
- #${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; }
- #${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; }
- #${TOC_TOGGLE_BTN_ID}:hover { background-color: #4a4b54; color: #FFFFFF; }
- #${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; }
- #${TOC_PANEL_ID}[data-collapsed="true"] #${TOC_LIST_ID} { display: none; }
- #${TOC_PANEL_ID}:not([data-collapsed="true"]) #${TOC_LIST_ID} { display: block; }
- #${TOC_LIST_ID} li { margin-bottom: 5px; }
- #${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; }
- #${TOC_LIST_ID} a:hover { background-color: #4a4b54; color: #FFFFFF; }
- #${TOC_LIST_ID} .toc-placeholder { color: #999; font-style: italic; padding: 3px 5px; }
- #${TOC_PANEL_ID}.dragging { opacity: 0.85; border: 1px dashed #aaa; }
- `;
- }
- // --- Style Injection / Update / Removal Functions ---
- function injectOrUpdateStyle(root, styleId, cssContent) {
- 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(); } }
- }
- function applyGlobalHeadStyles() {
- if (document.head) { injectOrUpdateStyle(document.head, GLOBAL_STYLE_ID, getGlobalSpinnerCss()); }
- }
- function applyWidthStyleToAllRoots() {
- const widthCss = getWidthCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, WIDTH_STYLE_ID, widthCss); });
- }
- function applyJustificationStyleToAllRoots() {
- const justifyCss = getJustifyCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, JUSTIFY_STYLE_ID, justifyCss); });
- }
- function applyScrollMarginStyleToAllRoots() {
- const scrollMarginCss = getScrollMarginCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, SCROLL_MARGIN_STYLE_ID, scrollMarginCss); });
- }
- // --- UI State Update ---
- function updateUIState() {
- 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; }
- 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'); } }
- }
- // --- Click Outside Handler ---
- async function handleClickOutside(event) {
- 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(); }
- }
- // --- Drag Handlers ---
- function handleMouseDown(e) {
- 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();
- }
- function handleMouseMove(e) {
- 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';
- }
- function handleMouseUp(e) {
- 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);
- }
- // --- UI Creation/Removal ---
- function removeSettingsUI() {
- 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); }
- }
- function createSettingsUI() {
- 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();
- }
- function removeTocUI() {
- 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); }
- }
- function createTocUI() {
- 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); }
- }
- // --- ToC Logic ---
- function scrollToMessage(event) {
- 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.`); }
- }
- const scanMessagesAndBuildToc = debounce(() => {
- if (!tocList || !config.tocVisible) return; tocList.innerHTML = ''; messageCounterForTocIds = 0;
- const allMessageBlocksNodeList = document.querySelectorAll(WIDTH_TARGET_SELECTOR);
- const allMessageBlocksArray = Array.from(allMessageBlocksNodeList);
- 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; });
- let questionCount = 0;
- sortedMessageBlocks.forEach(messageBlockContainer => {
- messageCounterForTocIds++; const messageId = `ve-msg-${messageCounterForTocIds}`;
- if (!messageBlockContainer.id || !messageBlockContainer.id.startsWith('ve-msg-')) { messageBlockContainer.id = messageId; }
- const userMessageContentElement = messageBlockContainer.querySelector(USER_MESSAGE_CONTENT_COMPONENT_SELECTOR);
- if (userMessageContentElement) {
- questionCount++; let extractedText = '';
- const firstP = userMessageContentElement.querySelector('p:first-of-type');
- if (firstP && (firstP.textContent || '').trim()) { extractedText = (firstP.textContent || '').trim(); }
- if (!extractedText) {
- const firstListItem = userMessageContentElement.querySelector('ol > li:first-of-type, ul > li:first-of-type');
- if (firstListItem) {
- const pInLi = firstListItem.querySelector('p');
- if (pInLi && (pInLi.textContent || '').trim()) { extractedText = (pInLi.textContent || '').trim(); }
- 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*/, ''); }
- }
- }
- 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*/, ''); }
- if (!extractedText) { extractedText = `(Question ${questionCount})`; }
- if (extractedText.length > TOC_MAX_TEXT_LENGTH) { extractedText = extractedText.substring(0, TOC_MAX_TEXT_LENGTH) + '...'; }
- 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);
- }
- });
- if (questionCount === 0 && tocList) { const noMessagesItem = document.createElement('li'); noMessagesItem.textContent = 'Empty (No user messages found)'; noMessagesItem.className = 'toc-placeholder'; tocList.appendChild(noMessagesItem); }
- updateUIState();
- }, TOC_DEBOUNCE_DELAY_MS);
- // --- Tampermonkey Menu ---
- function updateTampermonkeyMenu() {
- 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(); }); }
- }
- // --- Shadow DOM Handling ---
- function getShadowRoot(element) { try { return element.shadowRoot; } catch (e) { return null; } }
- function processElement(element) {
- 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;
- }
- // --- Observer Setup and SPA Navigation Handling ---
- const chatContentObserverCallback = (mutations) => {
- let potentialMessageChanged = false;
- mutations.forEach((mutation) => {
- mutation.addedNodes.forEach((node) => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- try { const elementsToCheck = [node, ...node.querySelectorAll('*')]; elementsToCheck.forEach(el => { processElement(el); }); } catch (e) { }
- if (node.matches && (node.matches(USER_MESSAGE_BLOCK_SELECTOR) || node.matches(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
- else if (node.querySelector && (node.querySelector(USER_MESSAGE_BLOCK_SELECTOR) || node.querySelector(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
- }
- });
- mutation.removedNodes.forEach((node) => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- if (node.matches && (node.matches(USER_MESSAGE_BLOCK_SELECTOR) || node.matches(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
- else if (node.querySelector && (node.querySelector(USER_MESSAGE_BLOCK_SELECTOR) || node.querySelector(AI_MESSAGE_BLOCK_SELECTOR))) { potentialMessageChanged = true; }
- }
- });
- });
- if (potentialMessageChanged && config.tocVisible) {
- scanMessagesAndBuildToc();
- }
- };
- function ensureChatContentObserverIsActive(targetChatElement) {
- if (chatContentObserverInstance && currentChatContainerElement === targetChatElement) {
- return;
- }
- if (chatContentObserverInstance) {
- chatContentObserverInstance.disconnect();
- }
- currentChatContainerElement = targetChatElement;
- chatContentObserverInstance = new MutationObserver(chatContentObserverCallback);
- chatContentObserverInstance.observe(targetChatElement, { childList: true, subtree: true });
- if (config.tocVisible) {
- scanMessagesAndBuildToc();
- }
- }
- function startPageObserver() {
- const bodyElement = document.body;
- if (!bodyElement) {
- setTimeout(startPageObserver, 1000);
- return;
- }
- pageObserverInstance = new MutationObserver((mutations) => {
- const chatContainerElement = document.querySelector(CHAT_CONTAINER_SELECTOR_FOR_OBSERVER);
- if (chatContainerElement) {
- if (currentChatContainerElement !== chatContainerElement) {
- ensureChatContentObserverIsActive(chatContainerElement);
- }
- } else {
- if (currentChatContainerElement) {
- if (chatContentObserverInstance) chatContentObserverInstance.disconnect();
- chatContentObserverInstance = null;
- currentChatContainerElement = null;
- if (tocList && config.tocVisible) {
- tocList.innerHTML = '<li class="toc-placeholder">Chat not active or empty.</li>';
- }
- }
- }
- });
- pageObserverInstance.observe(bodyElement, { childList: true, subtree: true });
- const initialChatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR_FOR_OBSERVER);
- if (initialChatContainer) {
- ensureChatContentObserverIsActive(initialChatContainer);
- } else {
- if (tocList && config.tocVisible) {
- // tocList.innerHTML = '<li class="toc-placeholder">Waiting for chat...</li>';
- }
- }
- }
- function handleUrlChangeForSPA() {
- if (window.location.href !== lastKnownHref) {
- lastKnownHref = window.location.href;
- if (config.tocVisible) {
- setTimeout(scanMessagesAndBuildToc, URL_CHANGE_RESCAN_DELAY_MS);
- }
- }
- }
- // --- Initialization ---
- console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} starting...`);
- if (document.head) allStyleRoots.add(document.head);
- else { const rootNode = document.documentElement || document; allStyleRoots.add(rootNode); }
- await loadSettings();
- applyGlobalHeadStyles(); applyWidthStyleToAllRoots();
- applyJustificationStyleToAllRoots(); applyScrollMarginStyleToAllRoots();
- try { document.querySelectorAll('*').forEach(el => { processElement(el); }); }
- catch (e) { console.error(`[${SCRIPT_NAME}] Error during initial Shadow DOM scan:`, e); }
- if (config.uiVisible) createSettingsUI();
- if (config.tocVisible) createTocUI();
- updateTampermonkeyMenu();
- document.addEventListener('click', (event) => {
- const confirmButton = event.target.closest(CONFIRM_EDIT_BUTTON_SELECTOR);
- if (confirmButton) { setTimeout(scanMessagesAndBuildToc, 250); }
- }, true);
- document.addEventListener('keydown', (event) => {
- if (event.key === 'Enter' && !event.shiftKey) {
- const activeElement = document.activeElement;
- if (activeElement && activeElement.closest(EDIT_MESSAGE_INPUT_SELECTOR)) {
- setTimeout(scanMessagesAndBuildToc, 250);
- }
- }
- }, true);
- setTimeout(startPageObserver, 1000);
- const originalPushState = history.pushState;
- history.pushState = function(...args) {
- const result = originalPushState.apply(this, args);
- window.dispatchEvent(new Event('pushstate'));
- return result;
- };
- const originalReplaceState = history.replaceState;
- history.replaceState = function(...args) {
- const result = originalReplaceState.apply(this, args);
- window.dispatchEvent(new Event('replacestate'));
- return result;
- };
- window.addEventListener('popstate', handleUrlChangeForSPA);
- window.addEventListener('pushstate', handleUrlChangeForSPA);
- window.addEventListener('replacestate', handleUrlChangeForSPA);
- setTimeout(scanMessagesAndBuildToc, 1200);
- // console.log(`[${SCRIPT_NAME}] Initialization complete.`);
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement