// ==UserScript==
// @name Eye in the Cloud - A Google AI Studio Focused Experience
// @namespace https://github.com/soitgoes-again/eyeinthecloud
// @version 0.369
// @description Get focused by hiding the clutter, hide chat history, lag free text box, VIBE Mode, and themes!
// @author so it goes...again
// @match https://aistudio.google.com/*
// @resource CUSTOM_CSS https://raw.githubusercontent.com/soitgoes-again/eyeinthecloud/main/css/custom.css
// @resource DOS_THEME_CSS https://raw.githubusercontent.com/soitgoes-again/eyeinthecloud/main/css/theme.dos.css
// @resource NATURE_THEME_CSS https://raw.githubusercontent.com/soitgoes-again/eyeinthecloud/main/css/theme.nature.css
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_getResourceText
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 1. Define window.Config, window.State, window.Settings, window.DOM
// shared.js
// Shared configuration, state, and settings logic for AI Studio Advanced Control Suite.
window.Config = {
selectors: {
leftSidebar: 'ms-navbar',
rightSidebar: 'ms-right-side-panel',
header: 'ms-header-root',
toolbar: 'ms-toolbar',
chatInput: 'textarea[aria-label="Type something"]',
runButton: 'button.run-button[aria-label="Run"]',
overallLayout: 'body > app-root > ms-app > div',
chatContainer: 'ms-autoscroll-container',
userTurn: 'ms-chat-turn:has([data-turn-role="User"])',
aiTurn: 'ms-chat-turn:has([data-turn-role="Model"])',
buttonContainer: 'div.right-side'
},
ids: {
scriptButton: 'advanced-control-toggle-button',
popup: 'advanced-control-popup',
fakeInput: 'advanced-control-fake-input',
fakeRunButton: 'advanced-control-fake-run-button'
},
classes: {
layoutHide: 'adv-controls-hide-ui'
},
settingsKey: 'aiStudioAdvancedControlSettings_v4',
defaultSettings: {
limitHistory: false,
numTurnsToShow: 2,
hideSidebars: false,
hideHeader: false,
hideToolbar: false,
headingText: 'Eye in the Cloud',
showPromptChips: false,
hidePromptChips: false,
hideFeedbackButtons: false,
activeTheme: null // Add activeTheme setting with null default (no theme)
// Note: isVibeModeActive and preVibeSettings are NOT persisted intentionally.
// Vibe mode is transient and should reset on page load/script reload.
},
icons: {
visible: 'visibility',
hidden: 'visibility_off'
}
};
window.State = {
settings: { ...window.Config.defaultSettings },
isVibeModeActive: false, // New state for VIBE mode
activeTheme: null, // 'dos', 'nature', or null
themeCSS: {}, // Store loaded theme CSS strings { dos: "...", nature: "..." }
preVibeSettings: null, // New state to store settings before VIBE mode
isCurrentlyHidden: false,
scriptToggleButton: null,
popupElement: null,
chatObserver: null,
debounceTimer: null,
realChatInput: null,
realRunButton: null,
fakeChatInput: null,
uiUpdateDebounceTimer: null // Added debounce timer for UI updates
};
window.Settings = {
async load() {
const storedSettings = await GM_getValue(window.Config.settingsKey, window.Config.defaultSettings);
window.State.settings = { ...window.Config.defaultSettings, ...storedSettings };
window.State.isCurrentlyHidden = false;
},
async save() {
// Save all settings, not just a subset
await GM_setValue(window.Config.settingsKey, { ...window.State.settings });
},
update(key, value) {
if (window.State.settings[key] === value) return;
window.State.settings[key] = value;
let needsChatRules = false;
let needsLayoutRules = false;
// Determine necessary updates based on the changed key
if (key === 'numTurnsToShow' || key === 'limitHistory') {
needsChatRules = true;
} else if (key === 'hideSidebars' || key === 'hideHeader' || key === 'hideToolbar') {
needsLayoutRules = true;
}
// No specific flags needed for headingText, hidePromptChips, hideFeedbackButtons as they are called directly below
this.save(); // Save the updated settings
// Apply necessary UI updates immediately
// Debounce UI updates slightly if multiple settings change rapidly (like in Vibe mode restore)
clearTimeout(window.State.uiUpdateDebounceTimer);
window.State.uiUpdateDebounceTimer = setTimeout(() => {
if (needsLayoutRules && window.UI) {
window.UI.applyLayoutRules();
}
if (needsChatRules && window.UI) {
window.UI.applyChatVisibilityRules(); // No need for extra delay here now
}
// --- Direct UI updates for specific settings ---
if (key === 'headingText') {
window.UI?.updateHeadingText();
}
if (key === 'hidePromptChips') {
window.UI?.updatePromptChipsVisibility();
}
if (key === 'hideFeedbackButtons') {
window.UI?.updateTurnFooterVisibility();
}
// Update popup UI if it's open
if (window.State.popupElement?.classList.contains('visible') && window.Popup) {
window.Popup.updateUIState();
}
}, 50); // Apply a small debounce
},
batchUpdate(settingsToUpdate) {
let needsChatRules = false;
let needsLayoutRules = false;
let updated = false;
for (const key in settingsToUpdate) {
if (window.State.settings.hasOwnProperty(key) && window.State.settings[key] !== settingsToUpdate[key]) {
window.State.settings[key] = settingsToUpdate[key];
updated = true;
if (key === 'numTurnsToShow' || key === 'limitHistory') {
needsChatRules = true;
} else if (key === 'hideSidebars' || key === 'hideHeader' || key === 'hideToolbar') {
needsLayoutRules = true;
}
// Check other keys if they have direct UI updates needed within the batch logic if necessary
}
}
if (!updated) return;
this.save(); // Save the updated settings
// Apply necessary UI updates immediately
clearTimeout(window.State.uiUpdateDebounceTimer);
window.State.uiUpdateDebounceTimer = setTimeout(() => {
if (needsLayoutRules && window.UI) {
window.UI.applyLayoutRules();
}
if (needsChatRules && window.UI) {
window.UI.applyChatVisibilityRules();
}
// --- Direct UI updates for specific settings ---
if (settingsToUpdate.hasOwnProperty('headingText')) {
window.UI?.updateHeadingText();
}
if (settingsToUpdate.hasOwnProperty('hidePromptChips')) {
window.UI?.updatePromptChipsVisibility();
}
if (settingsToUpdate.hasOwnProperty('hideFeedbackButtons')) {
window.UI?.updateTurnFooterVisibility();
}
// Update popup UI if it's open
if (window.State.popupElement?.classList.contains('visible') && window.Popup) {
window.Popup.updateUIState();
}
}, 50);
}
};
// dom.js
// DOM utility functions for creating and managing elements in AI Studio Advanced Control Suite.
window.DOM = {
/**
* Create an element with attributes and children
*/
createElement(tag, attributes = {}, children = []) {
const element = document.createElement(tag);
// Apply attributes
for (const [key, value] of Object.entries(attributes)) {
if (key === 'className') {
element.className = value;
} else if (key === 'textContent') {
element.textContent = value;
} else if (key === 'events') {
for (const [event, handler] of Object.entries(value)) {
element.addEventListener(event, handler);
}
} else {
element.setAttribute(key, value);
}
}
// Append children
if (!Array.isArray(children)) children = [children];
children.filter(child => child).forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
},
/**
* Create a toggle switch with label
*/
createToggle(id, labelText, checked, onChange) {
const container = this.createElement('div', { className: 'toggle-setting' });
const label = this.createElement('label', {
className: 'toggle-label',
htmlFor: id,
textContent: labelText
});
const toggle = this.createElement('input', {
type: 'checkbox',
className: 'basic-slide-toggle',
id: id,
checked: checked,
events: { change: (e) => onChange(e.target.checked) }
});
container.appendChild(label);
container.appendChild(toggle);
return container;
}
};
// 2. Define window.Styles (consolidated)
// styles.js
// Minimal core CSS logic for AI Studio Advanced Control Suite. All main styles are in custom.css.
window.Styles = {
coreStyles: `
/* Basic UI hiding classes - essential structure only */
.adv-controls-hide-ui-sidebars ms-navbar,
.adv-controls-hide-ui-sidebars ms-right-side-panel {
display: none !important;
}
.adv-controls-hide-ui-header ms-header-root {
display: none !important;
}
.adv-controls-hide-ui-toolbar ms-toolbar {
display: none !important;
}
`,
addCoreStyles() {
// Inject the core styles string defined above
if (this.coreStyles) {
GM_addStyle(this.coreStyles);
}
},
addPopupStyles() {
// This function should now do nothing as custom.css handles it.
// console.log("Popup styles handled by custom.css");
return;
}
};
// 3. Define window.InputLagFix
// inputfix.js
// Provides a modal to fix input lag and manage advanced input in AI Studio.
window.InputLagFix = {
modalElement: null,
modalTextarea: null,
modalContent: null, // Added reference for opacity
triggerButton: null,
persistentModalText: '', // Store text here
isInitialized: false,
init() {
// This ensures modal is created once, trigger button is attempted when needed
if (!this.isInitialized) {
this.createModal(); // Create modal structure once
this.isInitialized = true;
}
this.createTriggerButton(); // Attempt to create/find button
},
createTriggerButton() {
const buttonId = 'adv-modal-trigger-btn';
const targetContainerSelector = '.prompt-input-wrapper-container';
// Check if button already exists in the DOM
const existingButton = document.getElementById(buttonId);
if (existingButton && document.body.contains(existingButton)) {
this.triggerButton = existingButton; // Update reference if needed
// Ensure listener is attached (prevents issues if script reloads)
existingButton.removeEventListener('click', this.showModal); // Remove potential old listener
existingButton.addEventListener('click', () => this.showModal());
return; // Already exists
}
// If we have a reference but it's detached, clear it
if (this.triggerButton && !document.body.contains(this.triggerButton)) {
this.triggerButton = null;
}
// Create the button only if necessary
if (!this.triggerButton) {
const parentContainer = document.querySelector(targetContainerSelector);
if (!parentContainer) {
return; // Cannot append yet
}
const button = document.createElement('button');
button.id = buttonId;
// Keep existing classes for Material styling, add new class for default hiding
button.className = 'mdc-icon-button mat-mdc-icon-button mat-unthemed mat-mdc-button-base gmat-mdc-button adv-modal-trigger eic-hidden-by-default';
button.setAttribute('mat-icon-button', '');
button.setAttribute('aria-label', 'Open Advanced Input');
button.setAttribute('mattooltip', 'Open Advanced Input');
const iconSpan = document.createElement('span');
iconSpan.className = 'material-symbols-outlined notranslate';
iconSpan.textContent = 'chat_bubble';
button.appendChild(iconSpan);
// Add ripple/focus/touch elements
const spanRipple = document.createElement('span');
spanRipple.className = 'mat-mdc-button-persistent-ripple mdc-icon-button__ripple';
button.appendChild(spanRipple);
const focusIndicator = document.createElement('span');
focusIndicator.className = 'mat-focus-indicator';
button.appendChild(focusIndicator);
const touchTarget = document.createElement('span');
touchTarget.className = 'mat-mdc-button-touch-target';
button.appendChild(touchTarget);
// Add event listener only once during creation
button.addEventListener('click', () => this.showModal());
// Create a simple wrapper
const buttonWrapper = document.createElement('div');
buttonWrapper.className = 'button-wrapper'; // Match existing structure
buttonWrapper.appendChild(button);
// Append the wrapper simply to the end of the parent container
parentContainer.appendChild(buttonWrapper);
this.triggerButton = button; // Store reference
}
},
createModal() {
if (document.getElementById('adv-input-modal-overlay')) {
this.modalElement = document.getElementById('adv-input-modal-overlay');
this.modalContent = document.getElementById('adv-input-modal-content');
this.modalTextarea = document.getElementById('adv-input-modal-textarea');
return; // Already exists
}
// Outer Overlay - Let CSS handle positioning and visibility
this.modalElement = document.createElement('div');
this.modalElement.id = 'adv-input-modal-overlay';
// Close modal if clicking overlay background
this.modalElement.addEventListener('click', (event) => {
if (event.target === this.modalElement) {
this.handleCancel();
}
});
// Inner Content Container - Minimal inline styles
this.modalContent = document.createElement('div');
this.modalContent.id = 'adv-input-modal-content';
Object.assign(this.modalContent.style, {
width: '80%',
height: '80%',
maxWidth: '1000px',
maxHeight: '700px',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
padding: '20px'
});
// Textarea - Only layout-related styles
this.modalTextarea = document.createElement('textarea');
this.modalTextarea.id = 'adv-input-modal-textarea';
Object.assign(this.modalTextarea.style, {
flexGrow: '1',
width: 'calc(100% - 20px)',
borderRadius: '4px',
marginBottom: '15px',
padding: '10px',
fontSize: '1rem',
resize: 'none',
outline: 'none'
});
// Prevent clicks inside textarea from closing modal
this.modalTextarea.addEventListener('click', (event) => event.stopPropagation());
// Button Container
const buttonContainer = document.createElement('div');
buttonContainer.className = 'adv-modal-buttons';
Object.assign(buttonContainer.style, {
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
marginTop: 'auto' // Push buttons to bottom
});
// Prevent clicks inside button area from closing modal
buttonContainer.addEventListener('click', (event) => event.stopPropagation());
// Helper to create styled buttons
const createModalButton = (text, onClick) => {
const button = document.createElement('button');
button.textContent = text;
Object.assign(button.style, {
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem'
});
button.addEventListener('click', onClick);
return button;
};
// Create Buttons
const cancelButton = createModalButton('Cancel', this.handleCancel.bind(this));
const addButton = createModalButton('Add to Input', this.handleAdd.bind(this));
const sendButton = createModalButton('Send', this.handleSend.bind(this));
// No Object.assign for sendButton color/background/border
// No mouseover/mouseout listeners for any button
// Append elements
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(addButton);
buttonContainer.appendChild(sendButton);
this.modalContent.appendChild(this.modalTextarea);
this.modalContent.appendChild(buttonContainer);
this.modalElement.appendChild(this.modalContent);
document.body.appendChild(this.modalElement);
},
showModal() {
if (!this.modalElement) this.createModal(); // Ensure it exists
if (!this.modalElement) return; // Bail if creation failed
this.modalTextarea.value = this.persistentModalText;
this.modalElement.classList.add('visible');
this.modalTextarea.focus();
// Add keydown listener for Escape key
document.addEventListener('keydown', this.handleEscKey);
},
hideModal() {
if (this.modalElement) {
this.modalElement.classList.remove('visible');
}
// Remove keydown listener
document.removeEventListener('keydown', this.handleEscKey);
},
// Bind 'this' correctly or use arrow function
handleEscKey: (event) => {
if (event.key === 'Escape') {
// Check if 'this' refers to InputLagFix object
if (window.InputLagFix && window.InputLagFix.modalElement?.classList.contains('visible')) { // New check
window.InputLagFix.handleCancel();
}
}
},
handleCancel() {
if (!this.modalTextarea) return;
this.persistentModalText = this.modalTextarea.value; // Save text
this.hideModal();
},
handleAdd() {
if (!this.modalTextarea) return;
const realInput = document.querySelector(window.Config.selectors.chatInput);
if (!realInput) {
this.hideModal();
return;
}
const textToAdd = this.modalTextarea.value;
this.persistentModalText = textToAdd; // Save text
// Append text, adding a newline if real input already has content
realInput.value += (realInput.value.trim() ? '\n' : '') + textToAdd;
// Dispatch events
realInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
realInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
// Optional focus/blur might help some frameworks update
// realInput.focus();
// realInput.blur();
this.hideModal();
},
handleSend() {
if (!this.modalTextarea) return;
const realInput = document.querySelector(window.Config.selectors.chatInput);
const realRunButton = document.querySelector(window.Config.selectors.runButton);
if (!realInput || !realRunButton) {
this.hideModal(); // Hide modal even if elements aren't found
return;
}
const textToSend = this.modalTextarea.value;
if (!textToSend.trim()) {
this.handleCancel(); // Treat empty send as cancel
return;
}
// Append text
realInput.value += (realInput.value.trim() ? '\n' : '') + textToSend;
// Dispatch events
realInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
realInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
// Click the real button after a short delay
setTimeout(() => {
if (realRunButton && !realRunButton.disabled) {
realRunButton.click();
this.persistentModalText = ''; // Clear persistent text on successful send
if(this.modalTextarea) this.modalTextarea.value = ''; // Clear textarea visually
} else {
this.persistentModalText = textToSend; // Keep text if send failed
}
this.hideModal(); // Hide modal after attempt
}, 150); // 150ms delay
}
};
// 4. Define window.Popup
// popup.js
// Popup dialog and settings UI for AI Studio Advanced Control Suite.
window.Popup = {
/**
* Create the settings popup
*/
create() {
if (document.getElementById(Config.ids.popup)) {
State.popupElement = document.getElementById(Config.ids.popup);
return;
}
// Create the popup element
State.popupElement = window.DOM.createElement('div', { id: Config.ids.popup });
// Build the popup header
const headerDiv = window.DOM.createElement('div', { className: 'popup-header' });
// --- Editable Title Display ---
const titleDisplay = window.DOM.createElement('div', {
id: 'popup-editable-title',
className: 'popup-title popup-editable-title',
textContent: State.settings.headingText || 'Eye in the Cloud',
title: 'Click to edit title',
tabindex: '0',
style: 'cursor: text;',
events: {
click: (e) => window.Popup.enterEditTitleMode(e.target),
focus: (e) => window.Popup.enterEditTitleMode(e.target),
mousedown: (e) => { if (e.detail > 1) e.preventDefault(); }
}
});
const closeButton = window.DOM.createElement('button',
{ className: 'close-popup-button', events: { click: this.hide } },
[window.DOM.createElement('span', { className: 'material-symbols-outlined notranslate', textContent: 'close' })]
);
headerDiv.appendChild(titleDisplay);
headerDiv.appendChild(closeButton);
State.popupElement.appendChild(headerDiv);
// Build the popup content
const contentDiv = window.DOM.createElement('div', { className: 'popup-content' });
// --- Section 1: VIBE Mode Button ---
const vibeSection = window.DOM.createElement('div', { className: 'popup-section vibe-section' });
const vibeButton = window.DOM.createElement('button', {
id: 'vibe-mode-toggle',
className: 'vibe-button',
events: {
click: this.toggleVibeMode
}
}, [
window.DOM.createElement('span', {
className: 'material-symbols-outlined notranslate',
textContent: 'bolt'
}),
'VIBE'
]);
vibeSection.appendChild(vibeButton);
contentDiv.appendChild(vibeSection); // Add VIBE section first
// --- Section 2: History Settings ---
const historyFieldset = window.DOM.createElement('fieldset', { className: 'popup-section' });
const historyLegend = window.DOM.createElement('legend', { textContent: 'History' });
historyFieldset.appendChild(historyLegend);
// Add Show All toggle (inverted logic for limitHistory)
historyFieldset.appendChild(
window.DOM.createToggle(
'show-all-history-toggle',
'Show All',
!State.settings.limitHistory,
checked => Settings.update('limitHistory', !checked)
)
);
// Turns slider
const sliderContainer = window.DOM.createElement('div', { className: 'slider-container' });
const sliderLabel = window.DOM.createElement('label', { htmlFor: 'num-turns-slider' });
sliderLabel.appendChild(window.DOM.createElement('span', { textContent: 'Currently Showing: ' }));
sliderLabel.appendChild(window.DOM.createElement('span', { id: 'num-turns-value', textContent: State.settings.limitHistory ? State.settings.numTurnsToShow : 'All' }));
const slider = window.DOM.createElement('input', {
id: 'num-turns-slider',
type: 'range',
min: '1',
max: '10', // Will be updated dynamically
value: State.settings.numTurnsToShow,
events: {
input: (e) => {
const sliderElement = e.target;
const value = parseInt(sliderElement.value);
const min = parseInt(sliderElement.min);
const max = parseInt(sliderElement.max);
// --- *** START: Added code for track fill *** ---
// Calculate percentage for CSS variable
const percentage = ((value - min) / (max - min)) * 100;
sliderElement.style.setProperty('--_slider-fill-percent', `${percentage}%`);
// --- *** END: Added code for track fill *** ---
// Original logic to update settings and display
if (State.settings.limitHistory) {
document.getElementById('num-turns-value').textContent = value;
Settings.update('numTurnsToShow', value);
}
},
change: (e) => {
const sliderElement = e.target;
const value = parseInt(sliderElement.value);
const min = parseInt(sliderElement.min);
const max = parseInt(sliderElement.max);
const percentage = ((value - min) / (max - min)) * 100;
sliderElement.style.setProperty('--_slider-fill-percent', `${percentage}%`);
}
}
});
// --- *** ADD Initial Setting of CSS variable *** ---
const initialValue = parseInt(slider.value);
const initialMin = parseInt(slider.min);
const initialMax = parseInt(slider.max);
const initialPercentage = ((initialValue - initialMin) / (initialMax - initialMin)) * 100;
slider.style.setProperty('--_slider-fill-percent', `${initialPercentage}%`);
// --- *** END Initial Setting *** ---
sliderContainer.appendChild(sliderLabel);
sliderContainer.appendChild(slider);
historyFieldset.appendChild(sliderContainer);
contentDiv.appendChild(historyFieldset);
// --- Section 3: UI Settings ---
const uiFieldset = window.DOM.createElement('fieldset', { className: 'popup-section' });
uiFieldset.appendChild(window.DOM.createElement('legend', { textContent: 'Hide' }));
// Add toggle settings using our helper function
uiFieldset.appendChild(
window.DOM.createToggle('hide-sidebars-toggle', 'Sidebars', State.settings.hideSidebars,
checked => Settings.update('hideSidebars', checked))
);
uiFieldset.appendChild(
window.DOM.createToggle('hide-header-toggle', 'Header', State.settings.hideHeader,
checked => Settings.update('hideHeader', checked))
);
uiFieldset.appendChild(
window.DOM.createToggle('hide-toolbar-toggle', 'Toolbar', State.settings.hideToolbar,
checked => Settings.update('hideToolbar', checked))
);
// Add toggle for hide prompt chips (was showPromptChips)
uiFieldset.appendChild(
window.DOM.createToggle('hide-prompt-chips-toggle', 'Prompt Chips', State.settings.hidePromptChips,
checked => Settings.update('hidePromptChips', checked))
);
// Add toggle for hide feedback buttons
uiFieldset.appendChild(
window.DOM.createToggle('hide-feedback-buttons-toggle', 'Feedback Buttons', State.settings.hideFeedbackButtons,
checked => Settings.update('hideFeedbackButtons', checked))
);
contentDiv.appendChild(uiFieldset);
// --- Section: Themes (moved here after UI Settings) ---
const themeSection = window.DOM.createElement('fieldset', { id: 'theme-selector-section', className: 'popup-section theme-section' });
themeSection.appendChild(window.DOM.createElement('legend', { textContent: 'Themes' }));
const themeButtonsContainer = window.DOM.createElement('div', { className: 'theme-buttons-container'});
// DOS Theme Button
const dosButton = window.DOM.createElement('button', {
id: 'theme-btn-dos',
className: 'theme-select-button',
title: 'DOS Terminal Theme',
events: { click: () => {
window.Popup.handleThemeButtonClick('dos');
}}
}, [window.DOM.createElement('span', {className: 'material-symbols-outlined notranslate', textContent: 'code'})]);
// Nature Theme Button
const natureButton = window.DOM.createElement('button', {
id: 'theme-btn-nature',
className: 'theme-select-button',
title: 'Light Nature Theme',
events: { click: () => {
window.Popup.handleThemeButtonClick('nature');
}}
}, [window.DOM.createElement('span', {className: 'material-symbols-outlined notranslate', textContent: 'eco'})]); // or 'grass'
themeButtonsContainer.appendChild(dosButton);
themeButtonsContainer.appendChild(natureButton);
themeSection.appendChild(themeButtonsContainer);
contentDiv.appendChild(themeSection);
State.popupElement.appendChild(contentDiv);
// Add popup to document body
document.body.appendChild(State.popupElement);
},
/**
* Switches the title display element to an input field for editing.
*/
enterEditTitleMode(displayElement) {
if (!displayElement || displayElement.tagName === 'INPUT') return;
const currentText = displayElement.textContent;
const headerDiv = displayElement.parentNode;
const closeButton = headerDiv.querySelector('.close-popup-button');
// Create the input element
const inputField = window.DOM.createElement('input', {
type: 'text',
id: 'popup-title-input',
className: 'popup-title popup-title-input',
value: currentText,
'data-original-value': currentText,
style: `width: ${headerDiv.offsetWidth - closeButton.offsetWidth - 40}px; background: transparent; border: none; border-bottom: 1px solid var(--eic-popup-accent); outline: none; color: inherit; font-size: inherit; font-weight: inherit; padding: 0; margin: 0;`,
events: {
blur: (e) => window.Popup.exitEditTitleMode(e.target),
keydown: (e) => {
if (e.key === 'Enter') {
e.preventDefault();
window.Popup.exitEditTitleMode(e.target, true);
} else if (e.key === 'Escape') {
window.Popup.exitEditTitleMode(e.target, false);
}
}
}
});
headerDiv.replaceChild(inputField, displayElement);
inputField.focus();
inputField.select();
},
/**
* Switches the input field back to a display element, saving if requested.
*/
exitEditTitleMode(inputField, shouldSave = true) {
if (!inputField || inputField.tagName !== 'INPUT') return;
const headerDiv = inputField.parentNode;
const closeButton = headerDiv.querySelector('.close-popup-button');
const newValue = inputField.value.trim();
const originalValue = inputField.getAttribute('data-original-value');
let finalValue = originalValue;
if (shouldSave) {
if (newValue && newValue !== originalValue) {
Settings.update('headingText', newValue);
finalValue = newValue;
} else {
finalValue = originalValue;
}
} else {
finalValue = originalValue;
}
if (!finalValue) {
finalValue = 'Eye in the Cloud';
if (shouldSave && State.settings.headingText !== finalValue) {
Settings.update('headingText', finalValue);
}
}
const titleDisplay = window.DOM.createElement('div', {
id: 'popup-editable-title',
className: 'popup-title popup-editable-title',
textContent: finalValue,
title: 'Click to edit title',
tabindex: '0',
style: 'cursor: text;',
events: {
click: (e) => window.Popup.enterEditTitleMode(e.target),
focus: (e) => window.Popup.enterEditTitleMode(e.target),
mousedown: (e) => { if (e.detail > 1) e.preventDefault(); }
}
});
if (headerDiv && inputField) {
headerDiv.replaceChild(titleDisplay, inputField);
}
},
/**
* Show the popup dialog
*/
show() {
if (!State.popupElement) {
this.create();
}
// Remove call to Styles.addPopupStyles() - rely only on custom.css
this.updateUIState();
const blurOverlay = document.createElement('div');
blurOverlay.id = 'adv-controls-blur-overlay';
blurOverlay.style.position = 'fixed';
blurOverlay.style.top = '0';
blurOverlay.style.left = '0';
blurOverlay.style.width = '100%';
blurOverlay.style.height = '100%';
blurOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
blurOverlay.style.zIndex = '9998';
blurOverlay.style.opacity = '0';
blurOverlay.addEventListener('click', this.hide);
document.body.appendChild(blurOverlay);
// Trigger the fade-in using requestAnimationFrame
requestAnimationFrame(() => {
blurOverlay.style.opacity = '1';
});
State.popupElement.classList.add('visible');
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--eic-popup-accent') || '#8ab4f8';
document.documentElement.style.setProperty('--eic-popup-accent', accentColor);
setTimeout(() => {
document.addEventListener('click', this.handleOutsideClick);
}, 10);
},
/**
* Hide the popup dialog
*/
hide() {
if (!State.popupElement) return;
State.popupElement.classList.remove('visible');
const blurOverlay = document.getElementById('adv-controls-blur-overlay');
if (blurOverlay) blurOverlay.remove();
document.removeEventListener('click', window.Popup.handleOutsideClick);
},
/**
* Handle clicks outside the popup
*/
handleOutsideClick(e) {
if (State.popupElement &&
!State.popupElement.contains(e.target) &&
e.target.id !== Config.ids.scriptButton) {
window.Popup.hide();
}
},
/**
* Toggle popup visibility
*/
toggle(event) {
if (event) event.stopPropagation();
// Remove call to Styles.addPopupStyles() here too
if (State.popupElement?.classList.contains('visible')) {
window.Popup.hide();
} else {
window.Popup.show();
}
},
/**
* Toggle VIBE mode on/off
*/
toggleVibeMode() {
if (State.isVibeModeActive) {
// --- Deactivate VIBE mode ---
State.isVibeModeActive = false;
if (State.preVibeSettings) {
// Restore previous settings using batchUpdate
Settings.batchUpdate(State.preVibeSettings);
State.preVibeSettings = null; // Clear saved state
}
} else {
// --- Activate VIBE mode ---
State.isVibeModeActive = true;
// Deep copy current settings to save them
// Using JSON parse/stringify for a simple deep clone suitable here
State.preVibeSettings = JSON.parse(JSON.stringify(State.settings));
// Define VIBE settings
const vibeSettings = {
limitHistory: true,
numTurnsToShow: 1,
hideSidebars: true,
hideHeader: true,
hideToolbar: true,
hidePromptChips: true,
hideFeedbackButtons: true
};
Settings.batchUpdate(vibeSettings); // Apply VIBE settings
}
// Update the popup UI immediately to reflect the change
Popup.updateUIState();
},
/**
* Handle theme button click
*/
handleThemeButtonClick(themeName) {
if (State.isVibeModeActive) return; // Don't change theme if VIBE is on
if (State.activeTheme === themeName) {
ThemeManager.removeActiveTheme(); // Toggle off
} else {
ThemeManager.applyTheme(themeName); // Activate new theme
}
// No need to call updateUIState here, apply/remove Theme will do it.
},
/**
* Update UI elements in the popup to match current settings
*/
updateUIState() {
if (!State.popupElement) return;
// --- Update editable title display if not editing ---
const titleDisplay = State.popupElement.querySelector('#popup-editable-title');
const titleInput = State.popupElement.querySelector('#popup-title-input');
if (titleDisplay && !titleInput && titleDisplay.textContent !== State.settings.headingText) {
titleDisplay.textContent = State.settings.headingText || 'Eye in the Cloud';
}
// --- Update VIBE button and section state ---
const vibeButton = State.popupElement.querySelector('#vibe-mode-toggle');
const sectionsToDisable = State.popupElement.querySelectorAll('.popup-content .popup-section:not(.vibe-section)'); // Select all sections except vibe
if (vibeButton) {
vibeButton.classList.toggle('active', State.isVibeModeActive);
}
sectionsToDisable.forEach(section => section.classList.toggle('disabled-by-vibe', State.isVibeModeActive));
// --- Update Slider Max Value (Crucial: Do this BEFORE setting slider value/disabled state) ---
const turnsSlider = State.popupElement?.querySelector('#num-turns-slider');
if (turnsSlider) {
let maxExchanges = 1; // Default to 1 if no turns found
try {
const chatContainer = document.querySelector(window.Config.selectors.chatContainer);
if (chatContainer) {
const aiTurns = chatContainer.querySelectorAll(window.Config.selectors.aiTurn);
// Set max to at least 1, even if there are 0 AI turns, to avoid range errors.
maxExchanges = Math.max(1, aiTurns.length);
}
} catch (error) {}
// Only update if the max value is actually different
if (parseInt(turnsSlider.max) !== maxExchanges) {
turnsSlider.max = maxExchanges;
}
}
// --- History Section Update ---
const showAllToggle = State.popupElement?.querySelector('#show-all-history-toggle');
// Note: turnsSlider is already defined and checked above
const turnsValueDisplay = State.popupElement?.querySelector('#num-turns-value');
const userWantsLimit = State.settings.limitHistory; // What the user explicitly set
const isEffectivelyLimited = State.isVibeModeActive || userWantsLimit; // Is history actually limited?
if (showAllToggle && turnsSlider && turnsValueDisplay) {
showAllToggle.checked = !userWantsLimit; // Toggle reflects user's choice
showAllToggle.disabled = State.isVibeModeActive; // Disable toggle in Vibe
// Determine slider state and display text
if (State.isVibeModeActive) {
turnsSlider.disabled = true;
turnsSlider.parentElement.style.opacity = '0.5';
turnsValueDisplay.textContent = '1 (VIBE)';
// Ensure slider visually shows 1, though disabled
turnsSlider.value = 1;
} else if (userWantsLimit) { // Vibe OFF, User wants limit ON
turnsSlider.disabled = false;
turnsSlider.parentElement.style.opacity = '1';
// Ensure the current value doesn't exceed the calculated max
let currentVal = State.settings.numTurnsToShow;
let currentMax = parseInt(turnsSlider.max); // Use the max we just set
if (currentVal > currentMax) {
currentVal = currentMax; // Cap the value if needed
}
turnsSlider.value = currentVal;
turnsValueDisplay.textContent = State.settings.numTurnsToShow;
} else { // Vibe OFF, User wants Show All
turnsSlider.disabled = true;
turnsSlider.parentElement.style.opacity = '0.5';
turnsValueDisplay.textContent = 'All';
}
}
const sidebarsToggle = State.popupElement.querySelector('#hide-sidebars-toggle');
if (sidebarsToggle) {
sidebarsToggle.checked = State.settings.hideSidebars;
sidebarsToggle.disabled = State.isVibeModeActive;
}
const headerToggle = State.popupElement.querySelector('#hide-header-toggle');
if (headerToggle) {
headerToggle.checked = State.settings.hideHeader;
headerToggle.disabled = State.isVibeModeActive;
}
const toolbarToggle = State.popupElement.querySelector('#hide-toolbar-toggle');
if (toolbarToggle) {
toolbarToggle.checked = State.settings.hideToolbar;
toolbarToggle.disabled = State.isVibeModeActive;
}
const promptChipsToggle = State.popupElement.querySelector('#hide-prompt-chips-toggle');
if (promptChipsToggle) {
promptChipsToggle.checked = State.settings.hidePromptChips;
promptChipsToggle.disabled = State.isVibeModeActive;
}
const feedbackButtonsToggle = State.popupElement.querySelector('#hide-feedback-buttons-toggle');
if (feedbackButtonsToggle) {
feedbackButtonsToggle.checked = State.settings.hideFeedbackButtons;
feedbackButtonsToggle.disabled = State.isVibeModeActive;
}
// Also disable theme section when Vibe is active
const themeSection = State.popupElement?.querySelector('#theme-selector-section');
if (themeSection) {
themeSection.classList.toggle('disabled-by-vibe', State.isVibeModeActive);
}
// --- Update Theme Button States ---
const dosBtn = State.popupElement?.querySelector('#theme-btn-dos');
const natureBtn = State.popupElement?.querySelector('#theme-btn-nature');
if (dosBtn) dosBtn.classList.toggle('active', State.activeTheme === 'dos');
if (natureBtn) natureBtn.classList.toggle('active', State.activeTheme === 'nature');
// --- Update Slider Track Fill ---
if (turnsSlider) {
try {
const currentValue = parseInt(turnsSlider.value);
const currentMin = parseInt(turnsSlider.min);
const currentMax = parseInt(turnsSlider.max);
const range = currentMax - currentMin;
const currentPercentage = (range > 0) ? (((currentValue - currentMin) / range) * 100) : 0;
turnsSlider.style.setProperty('--_slider-fill-percent', `${currentPercentage}%`);
} catch (err) {}
}
}
};
// 5. Define window.ThemeManager
// thememanager.js
// Theme management logic for Eye in the Cloud (AI Studio Advanced Control Suite).
// --- Embedded Theme CSS ---
const dosThemeCSS = `
/* == Theme: DOS Green Terminal == */
body.theme-dos-applied {
/* --- Core Palette --- */
--mdc-theme-primary: #00ff00; /* Bright Green */
--mdc-theme-on-primary: #000000; /* Black text on green */
--mdc-theme-background: #000000; /* Black background */
--mdc-theme-on-background: #00ff00; /* Green text on black */
--mdc-theme-surface: #111111; /* Very dark grey for surfaces */
--mdc-theme-on-surface: #00ff00; /* Green text on surfaces */
--mdc-theme-surface-variant: #222222; /* Slightly lighter dark grey */
--mdc-theme-on-surface-variant: #00cc00; /* Slightly dimmer green */
--mdc-theme-outline: #008000; /* Darker green for borders/outlines */
--mdc-theme-outline-variant: #005000; /* Even darker green */
--mdc-theme-error: #ff0000; /* Standard red for errors */
--mdc-theme-on-error: #000000; /* Black text on red */
/* --- Typography --- */
--mdc-typography-font-family: 'Courier New', Courier, monospace;
font-family: 'Courier New', Courier, monospace !important;
/* --- Shape (Optional) --- */
--mdc-shape-small-component-radius: 0px;
--mdc-shape-medium-component-radius: 0px;
--mdc-shape-large-component-radius: 0px;
}
body.theme-dos-applied ms-code-block {
background-color: #1a1a1a !important;
border: 1px solid #005000 !important;
}
body.theme-dos-applied ms-code-block code {
color: #00ff00 !important;
}
body.theme-dos-applied .material-symbols-outlined {
color: var(--mdc-theme-on-surface);
}
body.theme-dos-applied button .material-symbols-outlined {
color: inherit;
}
`;
const natureThemeCSS = `
/* == Theme: Light Nature == */
body.theme-nature-applied {
--mdc-theme-primary: #4caf50;
--mdc-theme-on-primary: #ffffff;
--mdc-theme-background: #f5f5f5;
--mdc-theme-on-background: #444444;
--mdc-theme-surface: #ffffff;
--mdc-theme-on-surface: #333333;
--mdc-theme-surface-variant: #e0e0e0;
--mdc-theme-on-surface-variant: #555555;
--mdc-theme-outline: #bdbdbd;
--mdc-theme-outline-variant: #cccccc;
--mdc-theme-error: #d32f2f;
--mdc-theme-on-error: #ffffff;
--mdc-typography-font-family: 'Roboto', 'Helvetica Neue', sans-serif;
font-family: 'Roboto', 'Helvetica Neue', sans-serif !important;
--mdc-shape-small-component-radius: 6px;
--mdc-shape-medium-component-radius: 12px;
--mdc-shape-large-component-radius: 16px;
}
body.theme-nature-applied .material-symbols-outlined {
color: var(--mdc-theme-on-surface);
}
body.theme-nature-applied button .material-symbols-outlined {
color: inherit;
}
body.theme-nature-applied .mdc-button--raised .mdc-button__icon,
body.theme-nature-applied .mat-mdc-raised-button .mat-icon {
color: var(--mdc-theme-on-primary);
}
`;
// --- End Embedded CSS ---
window.ThemeManager = {
styleElements: {},
loadThemes() {
// Ensure resource names are mapped for theme switching
window.State.themeResourceNames = {
'dos': 'DOS_THEME_CSS',
'nature': 'NATURE_THEME_CSS'
};
},
applyTheme(themeName) {
// --- Re-enable this function ---
if (!window.State.themeResourceNames) {
this.loadThemes();
}
const resourceName = window.State.themeResourceNames[themeName];
if (!resourceName) {
return;
}
this.removeActiveThemeClasses();
// Inject Theme Override CSS if not already present or re-enable it
if (!this.styleElements[themeName]) {
const cssText = GM_getResourceText(resourceName);
if (cssText) {
// IMPORTANT: Theme CSS should ONLY contain variable overrides now
this.styleElements[themeName] = GM_addStyle(cssText);
} else {
return;
}
} else {
this.styleElements[themeName].disabled = false; // Re-enable if previously disabled
}
// Ensure other theme stylesheets are disabled
for (const name in this.styleElements) {
if (name !== themeName && this.styleElements[name]) {
this.styleElements[name].disabled = true;
}
}
// Apply theme class ONLY to body, like the old version
document.body.classList.add(`theme-${themeName}-applied`);
window.State.activeTheme = themeName;
window.Settings.update('activeTheme', themeName); // Use Settings.update to handle saving
// Update Popup UI if visible
if (window.State.popupElement?.classList.contains('visible') && window.Popup) {
window.Popup.updateUIState();
}
},
removeActiveTheme() {
// --- Re-enable this function ---
if (!window.State.activeTheme) {
return;
}
const currentTheme = window.State.activeTheme;
this.removeActiveThemeClasses();
// Disable the theme override stylesheet
if (this.styleElements[currentTheme]) {
this.styleElements[currentTheme].disabled = true;
}
window.State.activeTheme = null;
window.Settings.update('activeTheme', null); // Use Settings.update to handle saving
// Update Popup UI if visible
if (window.State.popupElement?.classList.contains('visible') && window.Popup) {
window.Popup.updateUIState();
}
},
removeActiveThemeClasses() {
// Ensure class is removed ONLY from body if that's where applyTheme adds it
document.body.classList.remove('theme-dos-applied', 'theme-nature-applied');
}
};
// 6. Define window.UI
// ui.js
// UI Control Module
window.UI = {
applyChatVisibilityRules() {
const chatContainer = document.querySelector(window.Config.selectors.chatContainer);
if (!chatContainer) {
return; // Exit if container not found
}
const allUserTurns = Array.from(chatContainer.querySelectorAll(window.Config.selectors.userTurn));
const allAiTurns = Array.from(chatContainer.querySelectorAll(window.Config.selectors.aiTurn));
const allTurns = Array.from(chatContainer.querySelectorAll(
`${window.Config.selectors.userTurn}, ${window.Config.selectors.aiTurn}`
));
let turnsToShow = [];
let localDidHideSomething = false;
const setDisplay = (element, visible) => {
const targetDisplay = visible ? '' : 'none';
if (element.style.display !== targetDisplay) {
element.style.display = targetDisplay;
}
};
const limitEnabled = window.State.settings.limitHistory;
const numExchangesToShow = window.State.settings.numTurnsToShow;
if (!limitEnabled) {
allTurns.forEach(turn => setDisplay(turn, true));
localDidHideSomething = false;
} else {
if (numExchangesToShow <= 0) {
allTurns.forEach(turn => setDisplay(turn, true));
localDidHideSomething = false;
} else {
// Robust: Show last N AI turns and their preceding user turns
const aiTurns = Array.from(chatContainer.querySelectorAll(window.Config.selectors.aiTurn));
const recentAiTurns = aiTurns.slice(-numExchangesToShow);
const turnElementsSet = new Set();
recentAiTurns.forEach(aiTurn => {
turnElementsSet.add(aiTurn);
// Find the immediately preceding user turn, if any
let previousElement = aiTurn.previousElementSibling;
while(previousElement && !previousElement.matches(window.Config.selectors.userTurn) && !previousElement.matches(window.Config.selectors.aiTurn)) {
previousElement = previousElement.previousElementSibling;
}
if (previousElement && previousElement.matches(window.Config.selectors.userTurn)) {
turnElementsSet.add(previousElement);
}
});
// Edge case: No AI turns, but user turns exist
if (aiTurns.length === 0 && numExchangesToShow >= 1) {
const userTurns = Array.from(chatContainer.querySelectorAll(window.Config.selectors.userTurn));
if (userTurns.length > 0) {
turnElementsSet.add(userTurns[userTurns.length - 1]);
}
}
allTurns.forEach(turn => {
const shouldBeVisible = turnElementsSet.has(turn);
setDisplay(turn, shouldBeVisible);
if (!shouldBeVisible) localDidHideSomething = true;
});
}
}
if (window.State.isCurrentlyHidden !== localDidHideSomething) {
window.State.isCurrentlyHidden = localDidHideSomething;
if (window.Button && typeof window.Button.updateAppearance === 'function') {
window.Button.updateAppearance();
}
}
},
updateHeadingText() {
const heading = document.querySelector('h1.gradient-text');
if (heading && window.State?.settings) {
heading.textContent = window.State.settings.headingText;
}
},
updatePromptChipsVisibility() {
const chips = document.querySelector('.chips-container');
if (chips && window.State?.settings) {
chips.style.display = window.State.settings.hidePromptChips ? 'none' : '';
}
},
updateInputPlaceholder() {
const overlay = document.querySelector('.placeholder-overlay');
if (overlay) {
overlay.textContent = 'If I tried to write a million words a day...';
}
},
updateTurnFooterVisibility() {
if (!window.State?.settings) return;
const footers = document.querySelectorAll('.turn-footer');
if (footers.length === 0) {
return;
}
const shouldHide = window.State.settings.hideFeedbackButtons;
footers.forEach(footer => {
footer.style.display = shouldHide ? 'none' : '';
});
},
applyLayoutRules() {
const layoutContainer = document.querySelector(window.Config.selectors.overallLayout);
if (!layoutContainer || !window.State?.settings) {
return;
}
const shouldHideSidebars = window.State.settings.hideSidebars;
const shouldHideHeader = window.State.settings.hideHeader;
const shouldHideToolbar = window.State.settings.hideToolbar;
layoutContainer.classList.toggle(`${window.Config.classes.layoutHide}-sidebars`, shouldHideSidebars);
layoutContainer.classList.toggle(`${window.Config.classes.layoutHide}-header`, shouldHideHeader);
layoutContainer.classList.toggle(`${window.Config.classes.layoutHide}-toolbar`, shouldHideToolbar);
if (window.State.popupElement?.style.display === 'block' && window.Popup) {
window.Popup.updateUIState();
}
}
};
// 7. Define window.Button
// button.js
// Provides the floating toggle button for chat visibility and options in AI Studio.
window.Button = {
create() {
if (document.getElementById(window.Config.ids.scriptButton)) {
window.State.scriptToggleButton = document.getElementById(window.Config.ids.scriptButton);
this.updateAppearance();
return;
}
// Create the floating button and append to body
window.State.scriptToggleButton = document.createElement('button');
window.State.scriptToggleButton.id = window.Config.ids.scriptButton;
window.State.scriptToggleButton.className = 'mdc-icon-button mat-mdc-icon-button mat-unthemed mat-mdc-button-base gmat-mdc-button advanced-control-button';
// Remove inline margin/order styles for floating
window.State.scriptToggleButton.removeAttribute('style');
const spanRipple = document.createElement('span');
spanRipple.className = 'mat-mdc-button-persistent-ripple mdc-icon-button__ripple';
window.State.scriptToggleButton.appendChild(spanRipple);
const icon = document.createElement('span');
icon.className = 'material-symbols-outlined notranslate';
icon.setAttribute('aria-hidden', 'true');
window.State.scriptToggleButton.appendChild(icon);
const focusIndicator = document.createElement('span');
focusIndicator.className = 'mat-focus-indicator';
window.State.scriptToggleButton.appendChild(focusIndicator);
const touchTarget = document.createElement('span');
touchTarget.className = 'mat-mdc-button-touch-target';
window.State.scriptToggleButton.appendChild(touchTarget);
window.State.scriptToggleButton.addEventListener('click', window.Popup.toggle);
document.body.appendChild(window.State.scriptToggleButton);
this.updateAppearance();
},
updateAppearance() {
if (!window.State.scriptToggleButton) return;
const iconSpan = window.State.scriptToggleButton.querySelector('.material-symbols-outlined');
if (iconSpan) {
iconSpan.textContent = window.State.isCurrentlyHidden ? window.Config.icons.hidden : window.Config.icons.visible;
}
const tooltipText = window.State.isCurrentlyHidden ?
'Chat history hidden (Click for options)' :
'Chat history visible (Click for options)';
window.State.scriptToggleButton.setAttribute('aria-label', tooltipText);
window.State.scriptToggleButton.setAttribute('mattooltip', tooltipText);
// Reregister command in case text changed
GM_registerMenuCommand(
window.State.isCurrentlyHidden ?
'Show All History (via settings)' :
'Hide History (via settings)',
window.Popup.toggle
);
}
};
// 8. Define window.ElementWatcher
// watcher.js
// Watches for DOM and settings changes to update UI and controls in AI Studio.
window.ElementWatcher = {
observer: null,
debounceTimer: null,
// Map logical UI areas to their corresponding update functions
uiUpdateFunctions: {
layout: () => window.UI?.applyLayoutRules(), // Covers sidebars, header, toolbar, input fix
heading: () => window.UI?.updateHeadingText(),
promptChips: () => window.UI?.updatePromptChipsVisibility(),
turnFooters: () => window.UI?.updateTurnFooterVisibility(),
placeholder: () => window.UI?.updateInputPlaceholder(),
},
// Debounced function to handle DOM changes
handleDomChange() {
if (!window.UI || !window.State?.settings) return;
// Ensure the InputLagFix button/modal logic runs if elements appear
if (window.InputLagFix && typeof window.InputLagFix.init === 'function') {
window.InputLagFix.init();
}
// --- START: Input Lag Fix Button Visibility Control ---
try {
const triggerButton = document.getElementById('adv-modal-trigger-btn');
if (triggerButton) {
// Check if the zero-state wrapper exists OR if there are no chat turns yet
const isZeroState = !!document.querySelector('.zero-state-wrapper');
const hasChatTurns = !!document.querySelector('ms-chat-turn'); // Check if any chat turns exist
// Determine if the button should be visible
const shouldBeVisible = !isZeroState && hasChatTurns;
// Toggle the visibility class based on the state
triggerButton.classList.toggle('eic-visible', shouldBeVisible);
// Optional cleanup of the default hidden class once visibility is managed
if (shouldBeVisible) {
triggerButton.classList.remove('eic-hidden-by-default');
}
// --- Ensure our icon is first in the button container ---
// Find the wrapper and its parent container
const buttonWrapper = triggerButton.closest('.button-wrapper');
const parentContainer = buttonWrapper?.parentElement;
if (buttonWrapper && parentContainer && parentContainer.children[0] !== buttonWrapper) {
parentContainer.insertBefore(buttonWrapper, parentContainer.firstChild);
}
} else {
// If button isn't found, InputLagFix.init() should try to create it on next run
// This check prevents errors if InputLagFix hasn't loaded yet
if (window.InputLagFix && typeof window.InputLagFix.init === 'function') {
window.InputLagFix.init();
}
}
} catch (error) {}
// --- END: Input Lag Fix Button Visibility Control ---
// Always call all UI update functions on DOM change
window.UI.applyLayoutRules();
window.UI.updateHeadingText();
window.UI.updatePromptChipsVisibility();
window.UI.updateTurnFooterVisibility();
window.UI.updateInputPlaceholder();
// --- START: Disclaimer Text Modification ---
try {
const disclaimerSpan = document.querySelector('.disclaimer-container span.disclaimer');
if (disclaimerSpan) {
const newDisclaimerText = "This reality is for testing only. No production use.";
// Only update if the text is different to avoid unnecessary changes
if (disclaimerSpan.textContent.trim() !== newDisclaimerText) {
disclaimerSpan.textContent = newDisclaimerText;
}
}
} catch (error) {}
// --- END: Disclaimer Text Modification ---
if (window.UI) {
window.UI.applyChatVisibilityRules();
}
// Update slider max if popup is open
// Use classList.contains for reliability, as display might be handled by transitions
if (window.State.popupElement?.classList.contains('visible')) {
try {
const chatContainer = document.querySelector(window.Config.selectors.chatContainer);
if (chatContainer) {
const aiTurns = chatContainer.querySelectorAll(window.Config.selectors.aiTurn);
const maxExchanges = aiTurns.length > 0 ? aiTurns.length : 1;
const slider = window.State.popupElement.querySelector('#num-turns-slider');
const valueDisplay = window.State.popupElement.querySelector('#num-turns-value');
if (slider && valueDisplay) {
if (parseInt(slider.max) !== maxExchanges) {
slider.max = maxExchanges;
}
let currentValue = parseInt(slider.value);
if (currentValue > maxExchanges) {
slider.value = maxExchanges;
// Only update value display if NOT in VIBE mode and limiting is ON
if (!window.State.isVibeModeActive && window.State.settings.limitHistory) {
valueDisplay.textContent = maxExchanges;
}
// Update the actual setting if it was capped
if (window.State.settings.numTurnsToShow !== maxExchanges) {
// Use setTimeout to avoid potential conflicts if called during another update cycle
setTimeout(() => Settings.update('numTurnsToShow', maxExchanges), 0);
}
}
}
}
} catch (error) {}
}
},
start() {
if (this.observer) return; // Already started
if (!window.UI || !window.State?.settings) {
// Retry starting after a short delay if UI/State aren't ready
setTimeout(() => this.start(), 500);
return;
}
// --- Setup Mutation Observer ---
this.observer = new MutationObserver(() => {
// Debounce the handler
clearTimeout(this.debounceTimer);
// Use a reasonable debounce time (e.g., 150-250ms)
this.debounceTimer = setTimeout(() => this.handleDomChange(), 200);
});
// Observe the body for subtree and child list changes
// Important: Start observing *before* the initial call to handleDomChange
this.observer.observe(document.body, { childList: true, subtree: true });
// --- Initial UI Application ---
// Call handler once shortly after starting observer to catch initial state
// This ensures elements potentially added *during* script load are handled.
setTimeout(() => this.handleDomChange(), 50); // Small delay after observer starts
},
stop() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
}
};
// 9. Define window.App (ONCE)
// app.js
// Main application initialization and control logic for AI Studio Advanced Control Suite.
window.App = {
themeManagerInitialized: false,
customStyleElement: null,
async init() {
console.log("Combined App Init Start");
await window.Settings.load();
// Inject Custom CSS FIRST
try {
const customCSSText = GM_getResourceText('CUSTOM_CSS');
if (customCSSText) {
this.customStyleElement = GM_addStyle(customCSSText);
console.log("AC Script: Custom CSS injected.");
} else { console.error("AC Script: Failed to load CUSTOM_CSS"); }
} catch (e) { console.error("AC Script: Error injecting custom CSS:", e); }
// Inject Core Styles
if(window.Styles) window.Styles.addCoreStyles();
// Register menu command
if (window.Popup && typeof window.Popup.toggle === 'function') {
GM_registerMenuCommand('Adv. Control Settings (AI Studio)', window.Popup.toggle);
} else { console.warn("Popup.toggle not ready for menu command."); }
// Initialize Theme Manager & Apply Saved Theme
if (!this.themeManagerInitialized && window.ThemeManager) {
console.log("[App] Initializing ThemeManager...");
// Call loadThemes HERE
if(typeof window.ThemeManager.loadThemes === 'function') {
window.ThemeManager.loadThemes();
} else { console.error("ThemeManager.loadThemes missing!");}
this.themeManagerInitialized = true;
console.log("[App] ThemeManager initialized.");
const savedTheme = window.State.settings.activeTheme;
if (savedTheme && typeof window.ThemeManager.applyTheme === 'function') {
console.log(`[App] Applying saved theme: ${savedTheme}`);
try {
window.ThemeManager.applyTheme(savedTheme);
console.log(`[App] Applied saved theme '${savedTheme}'.`);
} catch (error) { console.error(`[App] Error applying saved theme '${savedTheme}':`, error); }
}
} else if(!window.ThemeManager) { console.error("ThemeManager not found during App init"); }
// Initialize UI parts and watcher
this.initializeProgressively();
console.log("Combined App Init End");
},
initializeProgressively() {
console.log("Combined App Init Progressive Start");
// Call UI methods
if(window.UI) {
window.UI.applyChatVisibilityRules();
window.UI.applyLayoutRules();
} else { console.error("window.UI not found for progressive init"); }
// Start Watcher
if (window.ElementWatcher) {
window.ElementWatcher.start();
} else { console.error("window.ElementWatcher not found"); }
console.log("Combined App Init Progressive End");
}
};
// 10. Button Creation Logic
function createToggleButton() {
if (window.Button && typeof window.Button.create === 'function') {
console.log("Calling Button.create");
window.Button.create();
} else { console.error("window.Button.create not found");}
}
// 11. Final Initialization Execution (AFTER everything is defined)
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', () => {
console.log("DOM Loaded - Creating Button & Initializing App");
createToggleButton(); // Create button first
if (window.App && window.App.init) window.App.init(); // Then init app
else console.error("App not ready on DOMContentLoaded");
});
} else {
console.log("DOM Ready - Creating Button & Initializing App");
createToggleButton(); // Create button first
if (window.App && window.App.init) window.App.init(); // Then init app
else console.error("App not ready (DOM already loaded)");
}
})(); // End of SINGLE main IIFE