Last active
November 14, 2025 16:56
-
-
Save kiranwayne/9b676d5973aad64fd4711da1f110ecac to your computer and use it in GitHub Desktop.
Adds a movable panel with metadata to adjust content width, justify text, or use default width.
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 AI Studio Enhanced | |
| // @namespace https://gist.github.com/kiranwayne | |
| // @version 0.5.4 | |
| // @description Adds a movable panel with metadata to adjust content width, justify text, or use default width. | |
| // @author kiranwayne | |
| // @match https://aistudio.google.com/* | |
| // @updateURL https://gist.githubusercontent.com/kiranwayne/9b676d5973aad64fd4711da1f110ecac/raw/ai_studio_enhanced.js | |
| // @downloadURL https://gist.githubusercontent.com/kiranwayne/9b676d5973aad64fd4711da1f110ecac/raw/ai_studio_enhanced.js | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // --- Configuration & Constants --- | |
| const SCRIPT_NAME = 'AI Studio Enhanced'; | |
| const SCRIPT_VERSION = '0.5.4'; // <-- Changed: Version updated | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| const CONFIG_PREFIX = 'aiStudioEnhanced_v2_'; | |
| const PANEL_VISIBLE_KEY = CONFIG_PREFIX + 'panelVisible'; | |
| const PANEL_COLLAPSED_KEY = CONFIG_PREFIX + 'panelCollapsed'; | |
| const PANEL_POS_TOP_KEY = CONFIG_PREFIX + 'posTop'; | |
| const PANEL_POS_LEFT_KEY = CONFIG_PREFIX + 'posLeft'; | |
| const STYLE_WIDTH_KEY = CONFIG_PREFIX + 'styleWidth'; | |
| const STYLE_JUSTIFY_KEY = CONFIG_PREFIX + 'styleJustify'; | |
| const STYLE_USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'styleUseDefaultWidth'; | |
| const DYNAMIC_STYLE_ID = 'ai-studio-enhanced-dynamic-style'; | |
| const PANEL_STYLE_ID = 'ai-studio-enhanced-panel-style'; | |
| const CONTROLS_PANEL_ID = 'ai-studio-enhanced-panel'; | |
| const HEADER_CLASS = 'ai-studio-enhanced-header'; | |
| const TURN_SELECTOR = 'ms-chat-turn'; | |
| const TEXT_CHUNK_SELECTOR = 'ms-text-chunk'; | |
| const CHAT_SESSION_SELECTOR = '.chat-session-content'; // <-- Added: New selector for the parent container | |
| // --- State Variables --- | |
| let panelConfig = { | |
| visible: true, | |
| collapsed: false, | |
| posTop: '80px', | |
| posLeft: 'calc(100vw - 320px)' | |
| }; | |
| let styleConfig = { | |
| width: 1500, | |
| justify: false, | |
| useDefault: false | |
| }; | |
| const allStyleRoots = new Set(); | |
| let controlsPanel = null; | |
| let menuCommandId = null; | |
| let observer = null; | |
| // Dragging state | |
| let isDragging = false; | |
| let initialMouseX = 0, initialMouseY = 0; | |
| let initialPanelX = 0, initialPanelY = 0; | |
| // --- Helper Functions --- | |
| async function loadSettings() { | |
| panelConfig.visible = await GM_getValue(PANEL_VISIBLE_KEY, true); | |
| panelConfig.collapsed = await GM_getValue(PANEL_COLLAPSED_KEY, false); | |
| panelConfig.posTop = await GM_getValue(PANEL_POS_TOP_KEY, '80px'); | |
| panelConfig.posLeft = await GM_getValue(PANEL_POS_LEFT_KEY, `calc(100vw - 320px)`); | |
| styleConfig.width = await GM_getValue(STYLE_WIDTH_KEY, 1500); | |
| styleConfig.justify = await GM_getValue(STYLE_JUSTIFY_KEY, false); | |
| styleConfig.useDefault = await GM_getValue(STYLE_USE_DEFAULT_WIDTH_KEY, false); | |
| } | |
| async function saveSetting(key, value) { | |
| await GM_setValue(key, value); | |
| } | |
| // --- Panel Dragging Logic --- | |
| function handleMouseDown(e) { | |
| if (e.button !== 0 || e.target.closest('button, input, label')) return; | |
| if (!controlsPanel) return; | |
| isDragging = true; | |
| initialMouseX = e.clientX; | |
| initialMouseY = e.clientY; | |
| const rect = controlsPanel.getBoundingClientRect(); | |
| initialPanelX = rect.left; | |
| initialPanelY = rect.top; | |
| controlsPanel.classList.add('dragging'); | |
| document.addEventListener('mousemove', handleMouseMove); | |
| document.addEventListener('mouseup', handleMouseUp); | |
| e.preventDefault(); | |
| } | |
| function handleMouseMove(e) { | |
| if (!isDragging || !controlsPanel) return; | |
| const deltaX = e.clientX - initialMouseX; | |
| const deltaY = e.clientY - initialMouseY; | |
| let newPanelX = initialPanelX + deltaX; | |
| let newPanelY = initialPanelY + deltaY; | |
| newPanelX = Math.max(0, Math.min(newPanelX, window.innerWidth - controlsPanel.offsetWidth - 5)); | |
| newPanelY = Math.max(0, Math.min(newPanelY, window.innerHeight - controlsPanel.offsetHeight - 5)); | |
| controlsPanel.style.left = `${newPanelX}px`; | |
| controlsPanel.style.top = `${newPanelY}px`; | |
| } | |
| function handleMouseUp() { | |
| if (!isDragging || !controlsPanel) return; | |
| isDragging = false; | |
| controlsPanel.classList.remove('dragging'); | |
| document.removeEventListener('mousemove', handleMouseMove); | |
| document.removeEventListener('mouseup', handleMouseUp); | |
| saveSetting(PANEL_POS_TOP_KEY, controlsPanel.style.top); | |
| saveSetting(PANEL_POS_LEFT_KEY, controlsPanel.style.left); | |
| } | |
| // --- CSS Generation --- | |
| function getPanelCss() { | |
| return ` | |
| #${CONTROLS_PANEL_ID} { position: fixed; z-index: 9999; background: rgba(30, 30, 35, 0.9); color: #E0E0E0; border: 1px solid #555; border-radius: 8px; padding: 12px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4); min-width: 300px; font-size: 14px; backdrop-filter: blur(5px); transition: opacity 0.3s ease-out; user-select: none; -webkit-user-select: none; } | |
| #${CONTROLS_PANEL_ID}.hidden { opacity: 0; pointer-events: none; } | |
| #${CONTROLS_PANEL_ID} .${HEADER_CLASS} { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; cursor: move; } | |
| #${CONTROLS_PANEL_ID} h5 { margin: 0; font-size: 1.1em; font-weight: bold; color: #FFFFFF; } | |
| #${CONTROLS_PANEL_ID} .collapse-btn { background: none; border: none; color: #ccc; cursor: pointer; padding: 2px 5px; font-size: 1.3em; line-height: 1; border-radius: 4px; transition: background-color 0.2s; } | |
| #${CONTROLS_PANEL_ID} .collapse-btn:hover { background-color: #4a4b54; } | |
| #${CONTROLS_PANEL_ID}[data-collapsed="true"] .panel-body { display: none; } | |
| .metadata-container { padding: 0 4px; } | |
| .metadata-text { margin: 4px 0; font-size: 0.9em; color: #ccc; } | |
| .separator { border: none; border-top: 1px solid #555; margin: 12px 0; } | |
| #${CONTROLS_PANEL_ID} .control-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; gap: 8px; } | |
| #${CONTROLS_PANEL_ID} .control-row label { flex-shrink: 0; } | |
| #${CONTROLS_PANEL_ID} .control-row input[type="range"] { flex-grow: 1; margin: 0 5px; } | |
| #${CONTROLS_PANEL_ID} .control-row input[type="number"] { width: 65px; background: #222; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 4px 5px; text-align: center; } | |
| #${CONTROLS_PANEL_ID} .control-row input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } | |
| #${CONTROLS_PANEL_ID} .control-row input:disabled { background: #444; color: #888; cursor: not-allowed; } | |
| #${CONTROLS_PANEL_ID} .width-display { font-family: monospace; min-width: 45px; text-align: left; } | |
| #${CONTROLS_PANEL_ID} .check-label { display: flex; align-items: center; cursor: pointer; width: 100%; } | |
| #${CONTROLS_PANEL_ID} .check-label input { margin-right: 8px; } | |
| #${CONTROLS_PANEL_ID}.dragging { opacity: 0.85; border: 1px dashed #aaa; } | |
| `; | |
| } | |
| // --- Changed: Added new rule to remove parent container width limit --- | |
| function generateDynamicCss() { | |
| let css = ''; | |
| if (!styleConfig.useDefault) { | |
| css += ` | |
| /* Remove the max-width constraint from the main content area */ | |
| ${CHAT_SESSION_SELECTOR} { | |
| max-width: none !important; | |
| } | |
| /* Apply custom width to individual chat turns */ | |
| ${TURN_SELECTOR} { | |
| max-width: ${styleConfig.width}px !important; | |
| margin-left: auto !important; | |
| margin-right: auto !important; | |
| } | |
| `; | |
| } | |
| if (styleConfig.justify) { | |
| css += ` | |
| ${TURN_SELECTOR} ${TEXT_CHUNK_SELECTOR} { | |
| text-align: justify !important; | |
| } | |
| `; | |
| } | |
| return css; | |
| } | |
| // --- End of Changes --- | |
| // --- Style Injection & Shadow DOM Handling --- | |
| function injectStyle(targetNode, styleId, cssContent) { | |
| if (!targetNode || !targetNode.querySelector) return; | |
| let styleElement = targetNode.querySelector('#' + styleId); | |
| if (!styleElement) { | |
| styleElement = document.createElement('style'); | |
| styleElement.id = styleId; | |
| styleElement.textContent = cssContent; | |
| targetNode.appendChild(styleElement); | |
| } else if (styleElement.textContent !== cssContent) { | |
| styleElement.textContent = cssContent; | |
| } | |
| } | |
| function findAndApplyStylesToAllRoots(styleId, cssGenerator) { | |
| const css = cssGenerator(); | |
| const rootsToStyle = [document.head || document.documentElement, ...allStyleRoots]; | |
| rootsToStyle.forEach(root => { | |
| if (root) injectStyle(root, styleId, css); | |
| }); | |
| } | |
| // --- Core Logic --- | |
| function updateDynamicStyles() { | |
| findAndApplyStylesToAllRoots(DYNAMIC_STYLE_ID, generateDynamicCss); | |
| } | |
| // --- UI Functions --- | |
| function removeControlsUI() { | |
| if (controlsPanel) { | |
| controlsPanel.remove(); | |
| controlsPanel = null; | |
| } | |
| } | |
| function updatePanelUIState() { | |
| if (!controlsPanel) return; | |
| controlsPanel.setAttribute('data-collapsed', panelConfig.collapsed); | |
| const collapseBtn = controlsPanel.querySelector('.collapse-btn'); | |
| if (collapseBtn) { | |
| collapseBtn.textContent = panelConfig.collapsed ? '⊕' : '⊖'; | |
| } | |
| controlsPanel.classList.toggle('hidden', !panelConfig.visible); | |
| } | |
| function createControlsUI() { | |
| if (document.getElementById(CONTROLS_PANEL_ID)) return; | |
| if (!panelConfig.visible) return; | |
| controlsPanel = document.createElement('div'); | |
| controlsPanel.id = CONTROLS_PANEL_ID; | |
| controlsPanel.style.top = panelConfig.posTop; | |
| controlsPanel.style.left = panelConfig.posLeft; | |
| // --- Header --- | |
| const header = document.createElement('div'); | |
| header.className = HEADER_CLASS; | |
| header.addEventListener('mousedown', handleMouseDown); | |
| const title = document.createElement('h5'); | |
| title.textContent = SCRIPT_NAME; | |
| const collapseBtn = document.createElement('button'); | |
| collapseBtn.className = 'collapse-btn'; | |
| collapseBtn.type = 'button'; | |
| collapseBtn.addEventListener('click', () => { | |
| panelConfig.collapsed = !panelConfig.collapsed; | |
| saveSetting(PANEL_COLLAPSED_KEY, panelConfig.collapsed); | |
| updatePanelUIState(); | |
| }); | |
| header.appendChild(title); | |
| header.appendChild(collapseBtn); | |
| controlsPanel.appendChild(header); | |
| // This div will hold everything that collapses | |
| const panelBody = document.createElement('div'); | |
| panelBody.className = 'panel-body'; | |
| // --- Metadata --- | |
| const metadataContainer = document.createElement('div'); | |
| metadataContainer.className = 'metadata-container'; | |
| const versionP = document.createElement('p'); | |
| versionP.className = 'metadata-text'; | |
| versionP.textContent = `Version: ${SCRIPT_VERSION}`; | |
| const authorP = document.createElement('p'); | |
| authorP.className = 'metadata-text'; | |
| authorP.textContent = `Author: ${SCRIPT_AUTHOR}`; | |
| metadataContainer.appendChild(versionP); | |
| metadataContainer.appendChild(authorP); | |
| panelBody.appendChild(metadataContainer); | |
| panelBody.appendChild(Object.assign(document.createElement('hr'), { className: 'separator' })); | |
| // --- Controls --- | |
| const controlsContainer = document.createElement('div'); | |
| controlsContainer.className = 'controls-container'; | |
| const defaultWidthRow = document.createElement('div'); | |
| defaultWidthRow.className = 'control-row'; | |
| const defaultWidthLabel = document.createElement('label'); | |
| defaultWidthLabel.className = 'check-label'; | |
| const defaultWidthCheckbox = document.createElement('input'); | |
| defaultWidthCheckbox.type = 'checkbox'; | |
| defaultWidthCheckbox.checked = styleConfig.useDefault; | |
| defaultWidthLabel.append(defaultWidthCheckbox, 'Use Default Width'); | |
| defaultWidthRow.appendChild(defaultWidthLabel); | |
| controlsContainer.appendChild(defaultWidthRow); | |
| const widthRow = document.createElement('div'); | |
| widthRow.className = 'control-row'; | |
| const widthDisplay = document.createElement('span'); | |
| widthDisplay.className = 'width-display'; | |
| widthDisplay.textContent = `${styleConfig.width}px`; | |
| const widthSlider = document.createElement('input'); | |
| widthSlider.type = 'range'; | |
| widthSlider.min = 800; | |
| widthSlider.max = 2000; | |
| widthSlider.step = 10; | |
| widthSlider.value = styleConfig.width; | |
| const widthInput = document.createElement('input'); | |
| widthInput.type = 'number'; | |
| widthInput.min = 800; | |
| widthInput.max = 2000; | |
| widthInput.step = 10; | |
| widthInput.value = styleConfig.width; | |
| widthRow.append(widthDisplay, widthSlider, widthInput); | |
| controlsContainer.appendChild(widthRow); | |
| const syncWidth = (newValue) => { | |
| const val = Math.max(parseInt(widthSlider.min), Math.min(parseInt(widthSlider.max), parseInt(newValue) || 0)); | |
| styleConfig.width = val; | |
| widthSlider.value = val; | |
| widthInput.value = val; | |
| widthDisplay.textContent = `${val}px`; | |
| updateDynamicStyles(); | |
| }; | |
| widthSlider.addEventListener('input', () => syncWidth(widthSlider.value)); | |
| widthInput.addEventListener('change', () => syncWidth(widthInput.value)); | |
| widthSlider.addEventListener('change', () => saveSetting(STYLE_WIDTH_KEY, styleConfig.width)); | |
| widthInput.addEventListener('change', () => saveSetting(STYLE_WIDTH_KEY, styleConfig.width)); | |
| controlsContainer.appendChild(Object.assign(document.createElement('hr'), { className: 'separator' })); | |
| const justifyRow = document.createElement('div'); | |
| justifyRow.className = 'control-row'; | |
| const justifyLabel = document.createElement('label'); | |
| justifyLabel.className = 'check-label'; | |
| const justifyCheckbox = document.createElement('input'); | |
| justifyCheckbox.type = 'checkbox'; | |
| justifyCheckbox.checked = styleConfig.justify; | |
| justifyCheckbox.addEventListener('change', () => { | |
| styleConfig.justify = justifyCheckbox.checked; | |
| saveSetting(STYLE_JUSTIFY_KEY, styleConfig.justify); | |
| updateDynamicStyles(); | |
| }); | |
| justifyLabel.append(justifyCheckbox, 'Justify Text'); | |
| justifyRow.appendChild(justifyLabel); | |
| controlsContainer.appendChild(justifyRow); | |
| panelBody.appendChild(controlsContainer); | |
| const updateWidthControlsState = () => { | |
| const disabled = styleConfig.useDefault; | |
| widthSlider.disabled = disabled; | |
| widthInput.disabled = disabled; | |
| }; | |
| defaultWidthCheckbox.addEventListener('change', () => { | |
| styleConfig.useDefault = defaultWidthCheckbox.checked; | |
| saveSetting(STYLE_USE_DEFAULT_WIDTH_KEY, styleConfig.useDefault); | |
| updateWidthControlsState(); | |
| updateDynamicStyles(); | |
| }); | |
| controlsPanel.appendChild(panelBody); | |
| document.body.appendChild(controlsPanel); | |
| updatePanelUIState(); | |
| updateWidthControlsState(); | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId !== null) GM_unregisterMenuCommand(menuCommandId); | |
| const label = panelConfig.visible ? 'Hide Layout Controls' : 'Show Layout Controls'; | |
| menuCommandId = GM_registerMenuCommand(label, () => { | |
| panelConfig.visible = !panelConfig.visible; | |
| saveSetting(PANEL_VISIBLE_KEY, panelConfig.visible); | |
| if (panelConfig.visible) createControlsUI(); | |
| else removeControlsUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Initialization & Observer --- | |
| function initializeWhenReady() { | |
| findAndApplyStylesToAllRoots(PANEL_STYLE_ID, getPanelCss); | |
| updateDynamicStyles(); | |
| if (panelConfig.visible) createControlsUI(); | |
| updateTampermonkeyMenu(); | |
| startObserver(); | |
| } | |
| function startObserver() { | |
| const styleNewRoot = (root) => { | |
| if (root && !allStyleRoots.has(root)) { | |
| allStyleRoots.add(root); | |
| injectStyle(root, PANEL_STYLE_ID, getPanelCss()); | |
| injectStyle(root, DYNAMIC_STYLE_ID, generateDynamicCss()); | |
| } | |
| }; | |
| observer = new MutationObserver((mutationsList) => { | |
| for (const mutation of mutationsList) { | |
| if (mutation.type === 'childList') { | |
| mutation.addedNodes.forEach(node => { | |
| if (node instanceof Element) { | |
| if (node.shadowRoot) styleNewRoot(node.shadowRoot); | |
| node.querySelectorAll('*').forEach(el => { | |
| if (el.shadowRoot) styleNewRoot(el.shadowRoot); | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} starting...`); | |
| await loadSettings(); | |
| if (document.readyState === 'complete' || document.readyState === 'interactive') { | |
| initializeWhenReady(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', initializeWhenReady, { once: true }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment