Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Last active November 28, 2025 05:53
Show Gist options
  • Select an option

  • Save kiranwayne/76e1415d5c0ac38fa66e8043ba5b2e69 to your computer and use it in GitHub Desktop.

Select an option

Save kiranwayne/76e1415d5c0ac38fa66e8043ba5b2e69 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Venice Enhanced
// @namespace http://tampermonkey.net/
// @version 0.7.1
// @description Customize chat width, justification, set custom model selector height, 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/*
// @updateURL https://gist.github.com/kiranwayne/76e1415d5c0ac38fa66e8043ba5b2e69/raw/venice_enhanced.js
// @downloadURL https://gist.github.com/kiranwayne/76e1415d5c0ac38fa66e8043ba5b2e69/raw/venice_enhanced.js
// @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.7.1'; // Consistent "Use Default" Logic
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';
// Model Selector Config Keys
const USE_DEFAULT_MODEL_HEIGHT_KEY = CONFIG_PREFIX + 'useDefaultModelHeight'; // Renamed
const MODEL_HEIGHT_PX_KEY = CONFIG_PREFIX + 'modelHeightPx';
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 MODEL_SELECTOR_STYLE_ID = 'vm-venice-model-selector-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 ---
const CHAT_MESSAGES_AREA_SELECTOR = 'div.chat-messages-content';
// Selectors for specific content blocks
const USER_CONTENT_SELECTOR = 'div[data-testid="user-message"]';
const AI_CONTENT_SELECTOR = 'div[data-testid="text-chat-message"]';
// Top level row containers
const USER_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(${USER_CONTENT_SELECTOR})`;
const AI_MESSAGE_BLOCK_SELECTOR = `${CHAT_MESSAGES_AREA_SELECTOR} > div:has(${AI_CONTENT_SELECTOR})`;
// For TOC extraction
const USER_MESSAGE_CONTENT_COMPONENT_SELECTOR = USER_CONTENT_SELECTOR;
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;
// This selector targets the outermost message containers for sizing and centering.
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;
// Dimensions
const SCRIPT_DEFAULT_WIDTH_PX = 1000;
const MIN_WIDTH_PX = 500;
const MAX_WIDTH_PX = 2000;
const STEP_WIDTH_PX = 10;
const DEFAULT_MODEL_HEIGHT_PX = 600;
const MIN_MODEL_HEIGHT_PX = 300;
const MAX_MODEL_HEIGHT_PX = 1200;
const STEP_MODEL_HEIGHT_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,
useDefaultModelHeight: true, modelHeightPx: DEFAULT_MODEL_HEIGHT_PX,
uiVisible: false, tocVisible: true, tocCollapsed: false,
tocPosTop: TOC_DEFAULT_TOP_PX + 'px', tocPosLeft: ''
};
// UI Refs
let settingsPanel = null;
let widthSlider = null, widthLabel = null, widthInput = null, defaultWidthCheckbox = null;
let modelHeightSlider = null, modelHeightLabel = null, modelHeightInput = null, defaultModelHeightCheckbox = null;
let 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() {
// Width
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);
// Justify
config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, true);
// Model Selector
config.useDefaultModelHeight = await GM_getValue(USE_DEFAULT_MODEL_HEIGHT_KEY, true);
config.modelHeightPx = await GM_getValue(MODEL_HEIGHT_PX_KEY, DEFAULT_MODEL_HEIGHT_PX);
config.modelHeightPx = Math.max(MIN_MODEL_HEIGHT_PX, Math.min(MAX_MODEL_HEIGHT_PX, config.modelHeightPx));
// UI/TOC
config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false);
config.tocVisible = await GM_getValue(TOC_VISIBLE_KEY, false);
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 if (key === MODEL_HEIGHT_PX_KEY) {
const numValue = parseInt(value, 10);
if (!isNaN(numValue)) {
const clampedValue = Math.max(MIN_MODEL_HEIGHT_PX, Math.min(MAX_MODEL_HEIGHT_PX, numValue));
await GM_setValue(key, clampedValue); config.modelHeightPx = clampedValue;
}
} 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 === USE_DEFAULT_MODEL_HEIGHT_KEY) config.useDefaultModelHeight = 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 '';
const containerRule = `${WIDTH_TARGET_SELECTOR} {
max-width: ${config.maxWidthPx}px !important;
margin-left: auto !important;
margin-right: auto !important;
width: 100% !important;
}`;
const innerConstraintsSelector = `
${USER_CONTENT_SELECTOR}, ${USER_CONTENT_SELECTOR} .prose,
${AI_CONTENT_SELECTOR}, ${AI_CONTENT_SELECTOR} > div, ${AI_CONTENT_SELECTOR} .prose,
div:has(> ${AI_CONTENT_SELECTOR}), div:has(> div > ${AI_CONTENT_SELECTOR})
`;
const innerConstraintsRule = `${innerConstraintsSelector} {
max-width: none !important;
width: 100% !important;
}`;
return containerRule + '\n' + innerConstraintsRule;
}
function getJustifyCss() {
if (!config.justifyEnabled) return '';
return `${JUSTIFY_TARGET_SELECTOR} { text-align: justify !important; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; }`;
}
function getModelSelectorCss() {
if (config.useDefaultModelHeight) return '';
// Forces a fixed height based on user setting
return `
@media screen and (min-width: 768px) {
div:has(> div > div > div[data-sentry-component="ModelList"]) {
height: ${config.modelHeightPx}px !important;
min-height: 0 !important; /* Allow override */
max-height: 90vh !important;
}
}
`;
}
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; }
#${SETTINGS_PANEL_ID}, #${TOC_PANEL_ID} { cursor: default !important; }
#${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; cursor: default; }
#${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; cursor: pointer; }
#${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 applyModelSelectorStyleToAllRoots() {
const modelCss = getModelSelectorCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, MODEL_SELECTOR_STYLE_ID, modelCss); });
}
function applyScrollMarginStyleToAllRoots() {
const scrollMarginCss = getScrollMarginCss(); allStyleRoots.forEach(root => { if (root) injectOrUpdateStyle(root, SCROLL_MARGIN_STYLE_ID, scrollMarginCss); });
}
// --- UI State Update ---
function updateUIState() {
if (settingsPanel) {
// Width
if(defaultWidthCheckbox) defaultWidthCheckbox.checked = config.useDefaultWidth;
const isCustomWidthEnabled = !config.useDefaultWidth;
if(widthSlider) widthSlider.disabled = !isCustomWidthEnabled;
if(widthInput) widthInput.disabled = !isCustomWidthEnabled;
if(widthLabel) widthLabel.style.opacity = isCustomWidthEnabled ? 1 : 0.5;
if(widthSlider) { widthSlider.style.opacity = isCustomWidthEnabled ? 1 : 0.5; widthSlider.value = config.maxWidthPx; }
if(widthInput) { widthInput.style.opacity = isCustomWidthEnabled ? 1 : 0.5; widthInput.value = config.maxWidthPx; }
if(widthLabel) widthLabel.textContent = `${config.maxWidthPx}px`;
// Model Height
if(defaultModelHeightCheckbox) defaultModelHeightCheckbox.checked = config.useDefaultModelHeight;
const isCustomModelHeightEnabled = !config.useDefaultModelHeight;
if(modelHeightSlider) modelHeightSlider.disabled = !isCustomModelHeightEnabled;
if(modelHeightInput) modelHeightInput.disabled = !isCustomModelHeightEnabled;
if(modelHeightLabel) modelHeightLabel.style.opacity = isCustomModelHeightEnabled ? 1 : 0.5;
if(modelHeightSlider) { modelHeightSlider.style.opacity = isCustomModelHeightEnabled ? 1 : 0.5; modelHeightSlider.value = config.modelHeightPx; }
if(modelHeightInput) { modelHeightInput.style.opacity = isCustomModelHeightEnabled ? 1 : 0.5; modelHeightInput.value = config.modelHeightPx; }
if(modelHeightLabel) modelHeightLabel.textContent = `${config.modelHeightPx}px`;
// Justify
if(justifyCheckbox) 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; }
// Nullify all refs
widthSlider = null; widthLabel = null; widthInput = null; defaultWidthCheckbox = null;
modelHeightSlider = null; modelHeightLabel = null; modelHeightInput = null; defaultModelHeightCheckbox = null;
justifyCheckbox = null;
if (tocPanel && config.tocVisible && !document.onclick) { if (document) document.addEventListener('click', handleClickOutside, true); }
}
function createControlRow(labelText, sliderMin, sliderMax, sliderStep, inputValue) {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '10px';
const label = document.createElement('span');
label.style.minWidth = '50px';
label.style.fontFamily = 'monospace';
label.style.textAlign = 'right';
label.textContent = `${inputValue}px`;
const slider = document.createElement('input');
slider.type = 'range';
slider.min = sliderMin;
slider.max = sliderMax;
slider.step = sliderStep;
slider.value = inputValue;
slider.style.flexGrow = '1';
slider.style.verticalAlign = 'middle';
const input = document.createElement('input');
input.type = 'number';
input.min = sliderMin;
input.max = sliderMax;
input.step = sliderStep;
input.value = inputValue;
input.style.width = '60px';
input.style.verticalAlign = 'middle';
input.style.padding = '2px 4px';
input.style.background = '#202123';
input.style.color = '#ECECF1';
input.style.border = '1px solid #565869';
input.style.borderRadius = '4px';
container.appendChild(label);
container.appendChild(slider);
container.appendChild(input);
return { container, label, slider, input };
}
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: '300px' });
// Header
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'});
headerDiv.appendChild(titleElement); headerDiv.appendChild(versionElement);
settingsPanel.appendChild(headerDiv);
// --- CHAT WIDTH SECTION ---
const widthSection = document.createElement('div');
widthSection.style.marginTop = '10px';
const widthTitle = document.createElement('div'); widthTitle.textContent = "Chat Width"; widthTitle.style.fontWeight = "bold"; widthTitle.style.marginBottom = "5px"; widthTitle.style.fontSize = "0.9em"; widthTitle.style.color = "#aaa"; widthSection.appendChild(widthTitle);
const defaultWidthDiv = document.createElement('div'); defaultWidthDiv.style.marginBottom = '5px';
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 Default Width'; defaultWidthLabel.style.cursor = 'pointer'; defaultWidthLabel.style.fontSize = "0.9em";
defaultWidthDiv.appendChild(defaultWidthCheckbox); defaultWidthDiv.appendChild(defaultWidthLabel);
widthSection.appendChild(defaultWidthDiv);
const widthControls = createControlRow('', MIN_WIDTH_PX, MAX_WIDTH_PX, STEP_WIDTH_PX, config.maxWidthPx);
widthLabel = widthControls.label; widthSlider = widthControls.slider; widthInput = widthControls.input;
widthSection.appendChild(widthControls.container);
settingsPanel.appendChild(widthSection);
// --- MODEL HEIGHT SECTION ---
const modelSection = document.createElement('div');
modelSection.style.marginTop = '15px'; modelSection.style.borderTop = '1px solid #565869'; modelSection.style.paddingTop = '10px';
const modelTitle = document.createElement('div'); modelTitle.textContent = "Model Menu Height"; modelTitle.style.fontWeight = "bold"; modelTitle.style.marginBottom = "5px"; modelTitle.style.fontSize = "0.9em"; modelTitle.style.color = "#aaa"; modelSection.appendChild(modelTitle);
const modelToggleDiv = document.createElement('div'); modelToggleDiv.style.marginBottom = '5px';
defaultModelHeightCheckbox = document.createElement('input'); defaultModelHeightCheckbox.type = 'checkbox'; defaultModelHeightCheckbox.id = 'venice-userscript-modelheight-toggle';
const modelToggleLabel = document.createElement('label'); modelToggleLabel.htmlFor = 'venice-userscript-modelheight-toggle'; modelToggleLabel.textContent = ' Use Default Height'; modelToggleLabel.style.cursor = 'pointer'; modelToggleLabel.style.fontSize = "0.9em";
modelToggleDiv.appendChild(defaultModelHeightCheckbox); modelToggleDiv.appendChild(modelToggleLabel);
modelSection.appendChild(modelToggleDiv);
const modelControls = createControlRow('', MIN_MODEL_HEIGHT_PX, MAX_MODEL_HEIGHT_PX, STEP_MODEL_HEIGHT_PX, config.modelHeightPx);
modelHeightLabel = modelControls.label; modelHeightSlider = modelControls.slider; modelHeightInput = modelControls.input;
modelSection.appendChild(modelControls.container);
settingsPanel.appendChild(modelSection);
// --- MISC SECTION ---
const miscSection = document.createElement('div');
miscSection.style.marginTop = '15px'; miscSection.style.borderTop = '1px solid #565869'; miscSection.style.paddingTop = '10px';
const justifyDiv = document.createElement('div');
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 = ' Justify Text'; justifyLabel.style.cursor = 'pointer'; justifyLabel.style.fontSize = "0.9em";
justifyDiv.appendChild(justifyCheckbox); justifyDiv.appendChild(justifyLabel);
miscSection.appendChild(justifyDiv);
settingsPanel.appendChild(miscSection);
document.body.appendChild(settingsPanel);
// Event Listeners - Width
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; widthLabel.textContent = `${nw}px`; widthInput.value = nw; if (!config.useDefaultWidth) applyWidthStyleToAllRoots(); });
widthSlider.addEventListener('change', async (e) => { if (!config.useDefaultWidth) { await saveSetting(MAX_WIDTH_PX_KEY, parseInt(e.target.value, 10)); } });
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; widthLabel.textContent = `${nw}px`; 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; widthSlider.value = fw; widthLabel.textContent = `${fw}px`; if (!config.useDefaultWidth) { await saveSetting(MAX_WIDTH_PX_KEY, fw); applyWidthStyleToAllRoots(); } });
// Event Listeners - Model Height
defaultModelHeightCheckbox.addEventListener('change', async (e) => { await saveSetting(USE_DEFAULT_MODEL_HEIGHT_KEY, e.target.checked); applyModelSelectorStyleToAllRoots(); updateUIState(); });
modelHeightSlider.addEventListener('input', (e) => { const nw = parseInt(e.target.value, 10); config.modelHeightPx = nw; modelHeightLabel.textContent = `${nw}px`; modelHeightInput.value = nw; if (!config.useDefaultModelHeight) applyModelSelectorStyleToAllRoots(); });
modelHeightSlider.addEventListener('change', async (e) => { if (!config.useDefaultModelHeight) { await saveSetting(MODEL_HEIGHT_PX_KEY, parseInt(e.target.value, 10)); } });
modelHeightInput.addEventListener('input', (e) => { let nw = parseInt(e.target.value, 10); if (isNaN(nw)) return; nw = Math.max(MIN_MODEL_HEIGHT_PX, Math.min(MAX_MODEL_HEIGHT_PX, nw)); config.modelHeightPx = nw; modelHeightLabel.textContent = `${nw}px`; modelHeightSlider.value = nw; if (!config.useDefaultModelHeight) applyModelSelectorStyleToAllRoots(); });
modelHeightInput.addEventListener('change', async (e) => { let fw = parseInt(e.target.value, 10); if (isNaN(fw)) fw = config.modelHeightPx; fw = Math.max(MIN_MODEL_HEIGHT_PX, Math.min(MAX_MODEL_HEIGHT_PX, fw)); e.target.value = fw; modelHeightSlider.value = fw; modelHeightLabel.textContent = `${fw}px`; if (!config.useDefaultModelHeight) { await saveSetting(MODEL_HEIGHT_PX_KEY, fw); applyModelSelectorStyleToAllRoots(); } });
// Event Listeners - Justify
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(USER_MESSAGE_BLOCK_SELECTOR); // ToC is questions only
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, MODEL_SELECTOR_STYLE_ID, getModelSelectorCss()); 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(); applyModelSelectorStyleToAllRoots(); 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.`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment