Last active
December 25, 2025 15:33
-
-
Save kiranwayne/8aa5f99af288298c7c069d6504efc109 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 Assistant.sh Enhanced | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.1 | |
| // @description Customize max-width (slider/manual input) and toggle text justification for web.assistant.sh | |
| // @author kiranwayne | |
| // @match https://web.assistant.sh/* | |
| // @updateURL https://gist.github.com/kiranwayne/8aa5f99af288298c7c069d6504efc109/raw/assistant.sh_enhanced.js | |
| // @downloadURL https://gist.github.com/kiranwayne/8aa5f99af288298c7c069d6504efc109/raw/assistant.sh_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 = 'Assistant.sh Enhanced'; | |
| const SCRIPT_VERSION = '1.1'; | |
| // --- TARGET SELECTORS --- | |
| // Target both the <main> chat area AND the floating input bar container | |
| const WIDTH_TARGET_SELECTORS = 'main[class*="max-w-"], .fixed.bottom-0 > div[class*="max-w-"]'; | |
| // Target typography container | |
| const TEXT_SELECTOR = '.prose'; | |
| const CONFIG_PREFIX = 'assistantSh_enhanced_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-as-width-style'; | |
| const JUSTIFY_STYLE_ID = 'vm-as-justify-style'; | |
| const GLOBAL_STYLE_ID = 'vm-as-global-style'; | |
| const SETTINGS_PANEL_ID = 'as-userscript-settings-panel'; | |
| // Slider pixel config | |
| const SCRIPT_DEFAULT_WIDTH_PX = 900; | |
| const MIN_WIDTH_PX = 600; | |
| const MAX_WIDTH_PX = 2000; | |
| 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; | |
| // --- 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; } | |
| } | |
| } | |
| // --- CSS Generation --- | |
| function getWidthCss() { | |
| if (config.useDefaultWidth) return ''; | |
| return ` | |
| ${WIDTH_TARGET_SELECTORS} { | |
| max-width: ${config.maxWidthPx}px !important; | |
| } | |
| `; | |
| } | |
| function getJustifyCss() { | |
| if (!config.justifyEnabled) return ''; | |
| return ` | |
| ${TEXT_SELECTOR}, ${TEXT_SELECTOR} p { | |
| 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 --- | |
| function injectOrUpdateStyle(styleId, cssContent) { | |
| let style = document.getElementById(styleId); | |
| if (cssContent) { | |
| if (!style) { | |
| style = document.createElement('style'); | |
| style.id = styleId; | |
| style.textContent = cssContent; | |
| document.head.appendChild(style); | |
| } else if (style.textContent !== cssContent) { | |
| style.textContent = cssContent; | |
| } | |
| } else { | |
| if (style) style.remove(); | |
| } | |
| } | |
| function applyGlobalHeadStyles() { | |
| injectOrUpdateStyle(GLOBAL_STYLE_ID, getGlobalSpinnerCss()); | |
| } | |
| function applyWidthStyle() { | |
| injectOrUpdateStyle(WIDTH_STYLE_ID, getWidthCss()); | |
| } | |
| function applyJustificationStyle() { | |
| injectOrUpdateStyle(JUSTIFY_STYLE_ID, getJustifyCss()); | |
| } | |
| // --- UI State Update --- | |
| function updateUIState() { | |
| if (!settingsPanel) return; | |
| defaultWidthCheckbox.checked = config.useDefaultWidth; | |
| const isCustomWidthEnabled = !config.useDefaultWidth; | |
| widthSlider.disabled = !isCustomWidthEnabled; | |
| widthInput.disabled = !isCustomWidthEnabled; | |
| const opacity = isCustomWidthEnabled ? 1 : 0.5; | |
| widthLabel.style.opacity = opacity; | |
| widthSlider.style.opacity = opacity; | |
| widthInput.style.opacity = opacity; | |
| 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.contains(settingsPanel) && !settingsPanel.contains(event.target)) { | |
| await saveSetting(UI_VISIBLE_KEY, false); | |
| removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| } | |
| // --- UI Creation/Removal --- | |
| function removeSettingsUI() { | |
| document.removeEventListener('click', handleClickOutside, true); | |
| settingsPanel = document.getElementById(SETTINGS_PANEL_ID); | |
| if (settingsPanel) { | |
| settingsPanel.remove(); | |
| settingsPanel = null; | |
| } | |
| } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID) || !config.uiVisible) return; | |
| settingsPanel = document.createElement('div'); | |
| settingsPanel.id = SETTINGS_PANEL_ID; | |
| // Dark theme matching the site's aesthetic | |
| Object.assign(settingsPanel.style, { | |
| position: 'fixed', | |
| top: '20px', | |
| right: '20px', | |
| zIndex: '99999', | |
| display: 'block', | |
| background: 'rgba(23, 23, 23, 0.95)', | |
| color: '#ECECF1', | |
| border: '1px solid #333', | |
| borderRadius: '12px', | |
| padding: '16px', | |
| boxShadow: '0 10px 25px rgba(0,0,0,0.5)', | |
| backdropFilter: 'blur(10px)', | |
| minWidth: '300px', | |
| fontFamily: 'sans-serif', | |
| fontSize: '14px' | |
| }); | |
| // Header | |
| const headerDiv = document.createElement('div'); | |
| headerDiv.style.marginBottom = '12px'; | |
| headerDiv.style.borderBottom = '1px solid #333'; | |
| headerDiv.style.paddingBottom = '8px'; | |
| const titleElement = document.createElement('h4'); | |
| titleElement.textContent = SCRIPT_NAME; | |
| Object.assign(titleElement.style, { margin: '0', fontSize: '16px', fontWeight: '600', color: '#fff' }); | |
| const subElement = document.createElement('div'); | |
| subElement.textContent = `v${SCRIPT_VERSION}`; | |
| Object.assign(subElement.style, { fontSize: '11px', color: '#888', marginTop: '2px' }); | |
| headerDiv.appendChild(titleElement); | |
| headerDiv.appendChild(subElement); | |
| settingsPanel.appendChild(headerDiv); | |
| // --- Width Section --- | |
| const widthSection = document.createElement('div'); | |
| // Toggle Default Checkbox | |
| const defaultWidthDiv = document.createElement('div'); | |
| defaultWidthDiv.style.marginBottom = '12px'; | |
| defaultWidthDiv.style.display = 'flex'; | |
| defaultWidthDiv.style.alignItems = 'center'; | |
| defaultWidthCheckbox = document.createElement('input'); | |
| defaultWidthCheckbox.type = 'checkbox'; | |
| defaultWidthCheckbox.id = 'as-userscript-defaultwidth-toggle'; | |
| defaultWidthCheckbox.style.marginRight = '8px'; | |
| const defaultWidthLabel = document.createElement('label'); | |
| defaultWidthLabel.htmlFor = 'as-userscript-defaultwidth-toggle'; | |
| defaultWidthLabel.textContent = 'Use Default Site Width'; | |
| defaultWidthLabel.style.cursor = 'pointer'; | |
| defaultWidthDiv.appendChild(defaultWidthCheckbox); | |
| defaultWidthDiv.appendChild(defaultWidthLabel); | |
| // Controls Container | |
| const customWidthControlsDiv = document.createElement('div'); | |
| customWidthControlsDiv.style.display = 'flex'; | |
| customWidthControlsDiv.style.alignItems = 'center'; | |
| customWidthControlsDiv.style.gap = '8px'; | |
| customWidthControlsDiv.style.marginBottom = '8px'; | |
| widthLabel = document.createElement('span'); | |
| widthLabel.style.minWidth = '45px'; | |
| widthLabel.style.fontFamily = 'monospace'; | |
| widthLabel.style.color = '#aaa'; | |
| widthLabel.style.fontSize = '12px'; | |
| 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.cursor = 'pointer'; | |
| widthInput = document.createElement('input'); | |
| widthInput.type = 'number'; | |
| // Note: we don't strictly enforce min/max on the HTML attribute alone to allow typing, | |
| // but we handle validation in JS. | |
| widthInput.min = MIN_WIDTH_PX; | |
| widthInput.max = MAX_WIDTH_PX; | |
| widthInput.step = STEP_WIDTH_PX; | |
| Object.assign(widthInput.style, { | |
| width: '60px', | |
| padding: '4px', | |
| background: '#222', | |
| color: '#fff', | |
| border: '1px solid #444', | |
| borderRadius: '4px', | |
| fontSize: '12px' | |
| }); | |
| customWidthControlsDiv.appendChild(widthLabel); | |
| customWidthControlsDiv.appendChild(widthSlider); | |
| customWidthControlsDiv.appendChild(widthInput); | |
| widthSection.appendChild(defaultWidthDiv); | |
| widthSection.appendChild(customWidthControlsDiv); | |
| // --- Justify Section --- | |
| const justifySection = document.createElement('div'); | |
| justifySection.style.borderTop = '1px solid #333'; | |
| justifySection.style.paddingTop = '12px'; | |
| justifySection.style.marginTop = '12px'; | |
| justifySection.style.display = 'flex'; | |
| justifySection.style.alignItems = 'center'; | |
| justifyCheckbox = document.createElement('input'); | |
| justifyCheckbox.type = 'checkbox'; | |
| justifyCheckbox.id = 'as-userscript-justify-toggle'; | |
| justifyCheckbox.style.marginRight = '8px'; | |
| const justifyLabel = document.createElement('label'); | |
| justifyLabel.htmlFor = 'as-userscript-justify-toggle'; | |
| justifyLabel.textContent = 'Justify Text Content'; | |
| justifyLabel.style.cursor = 'pointer'; | |
| justifySection.appendChild(justifyCheckbox); | |
| justifySection.appendChild(justifyLabel); | |
| // Append Sections | |
| 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); | |
| applyWidthStyle(); | |
| updateUIState(); | |
| }); | |
| // SLIDER Logic (Instant Update) | |
| widthSlider.addEventListener('input', (e) => { | |
| const val = parseInt(e.target.value, 10); | |
| config.maxWidthPx = val; | |
| if (widthLabel) widthLabel.textContent = `${val}px`; | |
| if (widthInput) widthInput.value = val; | |
| if (!config.useDefaultWidth) applyWidthStyle(); | |
| }); | |
| widthSlider.addEventListener('change', async (e) => { | |
| const val = parseInt(e.target.value, 10); | |
| if (!config.useDefaultWidth) await saveSetting(MAX_WIDTH_PX_KEY, val); | |
| }); | |
| // TEXT INPUT Logic (Debounced/Deferred validation) | |
| widthInput.addEventListener('input', (e) => { | |
| const val = parseInt(e.target.value, 10); | |
| if (isNaN(val)) return; | |
| // Only update visuals if the number is currently valid. | |
| // If user types "1", we don't update style or slider yet (too small). | |
| // If user types "1234", we update. | |
| if (val >= MIN_WIDTH_PX && val <= MAX_WIDTH_PX) { | |
| config.maxWidthPx = val; | |
| if (widthLabel) widthLabel.textContent = `${val}px`; | |
| if (widthSlider) widthSlider.value = val; | |
| if (!config.useDefaultWidth) applyWidthStyle(); | |
| } | |
| }); | |
| widthInput.addEventListener('change', async (e) => { | |
| // On blur/enter, strict clamp and save | |
| let val = parseInt(e.target.value, 10); | |
| if (isNaN(val)) val = config.maxWidthPx; | |
| const clamped = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, val)); | |
| // Update state | |
| config.maxWidthPx = clamped; | |
| e.target.value = clamped; // Force input to match clamp | |
| if (widthSlider) widthSlider.value = clamped; | |
| if (widthLabel) widthLabel.textContent = `${clamped}px`; | |
| if (!config.useDefaultWidth) { | |
| applyWidthStyle(); | |
| await saveSetting(MAX_WIDTH_PX_KEY, clamped); | |
| } | |
| }); | |
| justifyCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(JUSTIFY_KEY, e.target.checked); | |
| applyJustificationStyle(); | |
| }); | |
| // Initialize UI values | |
| updateUIState(); | |
| // Close when clicking outside | |
| setTimeout(() => { | |
| document.addEventListener('click', handleClickOutside, true); | |
| }, 100); | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId_ToggleUI !== null) { | |
| GM_unregisterMenuCommand(menuCommandId_ToggleUI); | |
| } | |
| const label = config.uiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; | |
| menuCommandId_ToggleUI = GM_registerMenuCommand(label, async () => { | |
| const newState = !config.uiVisible; | |
| await saveSetting(UI_VISIBLE_KEY, newState); | |
| if (newState) { | |
| createSettingsUI(); | |
| } else { | |
| removeSettingsUI(); | |
| } | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Initialization --- | |
| await loadSettings(); | |
| applyGlobalHeadStyles(); | |
| applyWidthStyle(); | |
| applyJustificationStyle(); | |
| if (config.uiVisible) { | |
| createSettingsUI(); | |
| } | |
| updateTampermonkeyMenu(); | |
| console.log(`[${SCRIPT_NAME}] Loaded.`); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment