Last active
December 15, 2025 06:29
-
-
Save kiranwayne/b0ac1894b8b581855ec999fe840b2551 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Qwen Enhanced | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.5 | |
| // @description Customize max-width (slider/manual input), toggle justification. Show/hide via menu on chat.qwen.ai. Handles escaped class names & Shadow DOM. Header added. | |
| // @author kiranwayne | |
| // @match https://chat.qwen.ai/* | |
| // @updateURL https://gist.github.com/kiranwayne/b0ac1894b8b581855ec999fe840b2551/raw/qwen_enhanced.js | |
| // @downloadURL https://gist.github.com/kiranwayne/b0ac1894b8b581855ec999fe840b2551/raw/qwen_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 = 'Qwen Enhanced'; | |
| const SCRIPT_VERSION = '0.5'; | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| // --- UPDATED SELECTORS --- | |
| // 1. The container that restricts overall width (needs to be opened up) | |
| const CONTAINER_SELECTOR = '.chat-container'; | |
| // 2. The actual message bubbles (user and assistant) | |
| const MESSAGE_SELECTOR = '.qwen-chat-message'; | |
| const CONFIG_PREFIX = 'qwenEnhancedControls_v2_'; | |
| 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 WIDTH_STYLE_ID = 'vm-qwen-width-style'; | |
| const JUSTIFY_STYLE_ID = 'vm-qwen-justify-style'; | |
| const GLOBAL_STYLE_ID = 'vm-qwen-global-style'; | |
| const SETTINGS_PANEL_ID = 'qwen-userscript-settings-panel'; | |
| // Slider pixel config | |
| const SCRIPT_DEFAULT_WIDTH_PX = 1000; | |
| const MIN_WIDTH_PX = 500; | |
| const MAX_WIDTH_PX = 2500; // Increased max slightly for large screens | |
| const STEP_WIDTH_PX = 10; | |
| // --- State Variables --- | |
| let config = { | |
| maxWidthPx: SCRIPT_DEFAULT_WIDTH_PX, | |
| useDefaultWidth: false, | |
| justifyEnabled: false, | |
| uiVisible: false | |
| }; | |
| // UI and style references | |
| let settingsPanel = null; | |
| let widthSlider = null; | |
| let widthLabel = null; | |
| let widthInput = null; | |
| let defaultWidthCheckbox = null; | |
| let justifyCheckbox = null; | |
| let menuCommandId_ToggleUI = null; | |
| const allStyleRoots = new Set(); // Track document head and all shadow roots | |
| // --- Helper Functions --- | |
| 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, false); | |
| config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false); | |
| } | |
| 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 { | |
| 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; } | |
| } | |
| } | |
| // --- Style Generation Functions (UPDATED) --- | |
| function getWidthCss() { | |
| if (config.useDefaultWidth) return ''; // Remove rule if using site default | |
| // 1. Unrestrict the container (override max-width: 50rem) | |
| // 2. Apply the custom pixel width to the message bubbles | |
| return ` | |
| ${CONTAINER_SELECTOR} { | |
| max-width: 100% !important; | |
| } | |
| ${MESSAGE_SELECTOR} { | |
| max-width: ${config.maxWidthPx}px !important; | |
| /* Ensure centering logic remains if not present */ | |
| margin-left: auto !important; | |
| margin-right: auto !important; | |
| } | |
| `; | |
| } | |
| function getJustifyCss() { | |
| if (!config.justifyEnabled) return ''; // Remove rule if disabled | |
| // Apply justification to the specific message class | |
| return ` | |
| ${MESSAGE_SELECTOR} { | |
| text-align: justify !important; | |
| -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; | |
| } | |
| `; | |
| } | |
| 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; | |
| } | |
| `; | |
| } | |
| // --- Style Injection / Update / Removal Function --- | |
| function injectOrUpdateStyle(root, styleId, cssContent) { | |
| if (!root) return; | |
| let style = root.querySelector(`#${styleId}`); | |
| if (cssContent) { // Apply CSS | |
| 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 { // Remove CSS | |
| if (style) { style.remove(); } | |
| } | |
| } | |
| // --- Global Style Application Functions --- | |
| 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); }); | |
| } | |
| // --- UI State Update --- | |
| function updateUIState() { | |
| if (!settingsPanel || !defaultWidthCheckbox || !justifyCheckbox || !widthSlider || !widthLabel || !widthInput) return; | |
| 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; | |
| } | |
| // --- Click Outside Handler --- | |
| async function handleClickOutside(event) { | |
| if (settingsPanel && document.body && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target)) { | |
| await saveSetting(UI_VISIBLE_KEY, false); removeSettingsUI(); updateTampermonkeyMenu(); | |
| } | |
| } | |
| // --- UI Creation/Removal --- | |
| function removeSettingsUI() { | |
| if (document) 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; | |
| } | |
| } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID) || !config.uiVisible) return; | |
| if (!document.body) { console.warn("[Qwen Enhanced] document.body not found, cannot create 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 = 'qwen-userscript-defaultwidth-toggle'; | |
| const defaultWidthLabel = document.createElement('label'); defaultWidthLabel.htmlFor = 'qwen-userscript-defaultwidth-toggle'; defaultWidthLabel.textContent = ' Use Qwen 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 = 'qwen-userscript-justify-toggle'; | |
| const justifyLabel = document.createElement('label'); justifyLabel.htmlFor = 'qwen-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); | |
| // --- Event Listeners --- | |
| 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(); }); | |
| // --- Final UI Setup --- | |
| updateUIState(); | |
| if (document) document.addEventListener('click', handleClickOutside, true); | |
| applyGlobalHeadStyles(); | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| const cmdId = menuCommandId_ToggleUI; menuCommandId_ToggleUI = null; | |
| if (cmdId !== null && typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(cmdId); } catch (e) { console.warn('Failed unregister', e); } } | |
| const label = config.uiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; | |
| if (typeof GM_registerMenuCommand === 'function') { menuCommandId_ToggleUI = GM_registerMenuCommand(label, async () => { const newState = !config.uiVisible; await saveSetting(UI_VISIBLE_KEY, newState); if (newState) { createSettingsUI(); } else { removeSettingsUI(); } 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()); | |
| return true; | |
| } return false; | |
| } | |
| // --- Initialization --- | |
| console.log('[Qwen Enhanced] Script starting (v0.5)...'); | |
| if (document.head) allStyleRoots.add(document.head); | |
| else { const rootNode = document.documentElement || document; allStyleRoots.add(rootNode); } | |
| await loadSettings(); | |
| applyGlobalHeadStyles(); | |
| applyWidthStyleToAllRoots(); | |
| applyJustificationStyleToAllRoots(); | |
| try { document.querySelectorAll('*').forEach(el => { processElement(el); }); } | |
| catch(e) { console.error("[Qwen Enhanced] Error during initial scan:", e); } | |
| if (config.uiVisible) createSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| const observer = new MutationObserver((mutations) => { | |
| 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) {} | |
| } | |
| }); | |
| }); | |
| }); | |
| observer.observe(document.documentElement || document.body || document, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment