// ==UserScript==
// @name ChatGPT Zero
// @namespace https://github.com/NextDev65/
// @version 0.40
// @description Enhancements for ChatGPT
// @author NextDev65
// @homepageURL https://github.com/NextDev65/ChatGPT-0
// @supportURL https://github.com/NextDev65/ChatGPT-0
// @match https://chatgpt.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- Configuration ---
const PREFERRED_MODEL_KEY = 'preferredChatGPTModel';
const SETTINGS_KEY = 'chatgptZeroSettings';
const DEFAULT_MODEL = 'gpt-4o-mini';
const MODELS = [
'gpt-3.5-turbo',
'gpt-4o-mini',
'gpt-4-1-mini',
'gpt-4o',
'o4-mini'
];
// Default settings
const DEFAULT_SETTINGS = {
modelSwitcher: true,
animations: true
};
// Load settings from localStorage
function loadSettings() {
try {
const saved = localStorage.getItem(SETTINGS_KEY);
return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : { ...DEFAULT_SETTINGS };
} catch (e) {
console.warn('Failed to load settings, using defaults', e);
return { ...DEFAULT_SETTINGS };
}
}
// Save settings to localStorage
function saveSettings(settings) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch (e) {
console.warn('Failed to save settings', e);
}
}
// Global settings object
let settings = loadSettings();
/**
* Creates a toggle switch element
* @param {string} label - The label text for the toggle
* @param {boolean} checked - Initial checked state
* @param {Function} onChange - Callback when toggle changes
* @returns {HTMLDivElement}
*/
function createToggleSwitch(label, checked, onChange) {
const container = document.createElement('div');
container.className = 'toggle-container';
const labelElement = document.createElement('label');
labelElement.className = 'toggle-label';
labelElement.textContent = label;
const switchContainer = document.createElement('label');
switchContainer.className = 'toggle-switch';
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = checked;
input.className = 'toggle-input';
input.addEventListener('change', onChange);
const slider = document.createElement('span');
slider.className = 'toggle-slider';
switchContainer.appendChild(input);
switchContainer.appendChild(slider);
container.appendChild(labelElement);
container.appendChild(switchContainer);
return container;
}
/**
* Creates and returns a <button> element with an attached settings menu.
* @returns {HTMLButtonElement}
*/
function createSettingsMenu() {
const cog = document.createElement('button');
cog.id = 'settings-cog';
cog.textContent = '⚙️';
cog.setAttribute('aria-label', 'Settings');
const menu = document.createElement('div');
menu.id = 'settings-menu';
menu.className = 'settings-dropdown';
menu.style.display = 'none';
// Create toggle switches
const modelSwitcherToggle = createToggleSwitch('Model Switcher', settings.modelSwitcher, (e) => {
settings.modelSwitcher = e.target.checked;
saveSettings(settings);
updateModelSwitcherVisibility();
});
const animationsToggle = createToggleSwitch('Animations', settings.animations, (e) => {
settings.animations = e.target.checked;
saveSettings(settings);
updateAnimationStyles();
});
menu.appendChild(modelSwitcherToggle);
menu.appendChild(animationsToggle);
// Append menu to body to avoid positioning issues
document.body.appendChild(menu);
// Toggle menu visibility
cog.addEventListener('click', (e) => {
e.stopPropagation();
//const isVisible = window.getComputedStyle(menu).display !== 'none';
if (menu.style.display === 'block')
{
menu.style.display = 'none';
}
else {
positionMenu();
menu.style.display = 'block';
}
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!cog.contains(e.target) && !menu.contains(e.target)) {
menu.style.display = 'none';
}
});
// Position menu relative to cog
function positionMenu() {
const cogRect = cog.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = `${cogRect.bottom + 5}px`;
menu.style.left = `${cogRect.left}px`;
menu.style.zIndex = '10000';
}
// Inject CSS for settings menu and toggle switches
injectSettingsStyles();
return cog;
}
/**
* Injects CSS styles for the settings menu and components
*/
function injectSettingsStyles() {
if (document.getElementById('settings-styles')) return;
const style = document.createElement('style');
style.id = 'settings-styles';
const animationStyles = settings.animations ? `
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
` : '';
const toggleAnimationStyles = settings.animations ? `
transition: 0.3s;
` : '';
style.textContent = `
#settings-cog {
font-size: 20px;
margin-left: 12px;
padding: 4px;
border: none;
border-radius: 50%;
background-color: #212121;
color: #fff;
cursor: pointer;
box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset, 0 0 5px 0 rgba(33, 33, 33, 0);
${animationStyles}
}
#settings-cog:hover {
background-color: #2f2f2f;
box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
0 0 5px 0 rgba(255, 255, 255, 0.2);
}
#settings-cog:focus {
outline: none;
box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
0 0 5px 0 rgba(255, 255, 255, 0.5);
}
.settings-dropdown {
display: none;
background-color: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 12px;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.toggle-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.toggle-container:last-child {
margin-bottom: 0;
}
.toggle-label {
color: #fff;
font-size: 14px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 1;
}
.toggle-input:checked + .toggle-slider {
background-color: #4CAF50;
}
.toggle-input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.toggle-input:checked + .toggle-slider:hover {
background-color: #45a049;
}
.toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #555;
border-radius: 24px;
${toggleAnimationStyles}
}
.toggle-slider:before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
${toggleAnimationStyles}
}
`;
document.head.appendChild(style);
}
/**
* Updates animation styles based on current settings
*/
function updateAnimationStyles() {
// Remove existing styles and re-inject with updated animation settings
document.getElementById('settings-styles')?.remove();
document.getElementById('model-switcher-styles')?.remove();
injectSettingsStyles();
// Re-inject model switcher styles if it exists
const modelSwitcher = document.getElementById('chatgpt-model-switcher');
if (modelSwitcher) {
injectModelSwitcherStyles();
}
}
/**
* Updates model switcher visibility based on settings
*/
function updateModelSwitcherVisibility() {
const modelSwitcher = document.getElementById('chatgpt-model-switcher');
if (modelSwitcher) {
modelSwitcher.style.display = settings.modelSwitcher ? 'block' : 'none';
}
}
/**
* Injects CSS styles for the model switcher
*/
function injectModelSwitcherStyles() {
if (document.getElementById('model-switcher-styles')) return;
const style = document.createElement('style');
style.id = 'model-switcher-styles';
const animationStyles = settings.animations ? `
transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
` : '';
style.textContent = `
#chatgpt-model-switcher {
margin: auto;
padding: 4px 8px;
border: none;
border-radius: 6px;
background-color: #212121;
color: #fff;
outline: none;
box-shadow: 0 0 0 0 rgba(33, 33, 33, 0) inset, 0 0 5px 0 rgba(33, 33, 33, 0);
${animationStyles}
}
#chatgpt-model-switcher:hover {
background-color: #2f2f2f;
box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
0 0 5px 0 rgba(255, 255, 255, 0.2);
}
#chatgpt-model-switcher:focus {
outline: none;
box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0.5) inset,
0 0 5px 0 rgba(255, 255, 255, 0.5);
}
`;
document.head.appendChild(style);
}
/**
* Creates and returns a <select> element configured as the model switcher.
* @param {string} currentModel - Model to pre-select in the dropdown.
* @returns {HTMLSelectElement}
*/
function createModelSwitcher(currentModel) {
const select = document.createElement('select');
select.id = 'chatgpt-model-switcher';
// Inject CSS for base styling, hover, focus, and transition effects
injectModelSwitcherStyles();
// Populate dropdown with model options
MODELS.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
if (model === currentModel) option.selected = true;
select.appendChild(option);
});
// Save selection to localStorage on change
select.addEventListener('change', () => {
localStorage.setItem(PREFERRED_MODEL_KEY, select.value);
});
// Set initial visibility based on settings
select.style.display = settings.modelSwitcher ? 'block' : 'none';
return select;
}
/**
* Finds our model switcher in the UI and inserts the settings cog after it.
* Retries every second until our model switcher is visible.
*/
function injectSettingsMenu() {
const checkInterval = setInterval(() => {
const modelSwitcher = document.getElementById('chatgpt-model-switcher');
let cog = document.getElementById('settings-cog');
// Create cog if it doesn't exist yet
if (!cog) {
cog = createSettingsMenu();
}
// Insert cog after visible model switcher
if (modelSwitcher && !cog.parentNode && modelSwitcher.parentNode) {
modelSwitcher.parentNode.insertBefore(cog, modelSwitcher.nextSibling);
}
}, 1000);
}
/**
* Finds the native model switcher in the UI and inserts our custom switcher beside it.
* Retries every second until the native element is visible.
*/
function injectModelSwitcher() {
const checkInterval = setInterval(() => {
const nativeModelSwitchers = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
let switcher = document.getElementById('chatgpt-model-switcher');
// Create switcher
if (!switcher) {
const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
switcher = createModelSwitcher(savedModel);
}
// Insert switcher next to the first visible native button
if (!switcher.parentNode) {
for (let nativeModelSwitcher of nativeModelSwitchers) {
if (nativeModelSwitcher.checkVisibility && nativeModelSwitcher.checkVisibility()) {
nativeModelSwitcher.parentNode.after(switcher);
break;
}
}
}
}, 1000);
}
/**
* Overrides window.fetch to intercept conversation requests and replace the model
* property in the request body with the user-selected model.
*/
function overrideModelInRequest() {
// Only override if model switcher is enabled
if (!settings.modelSwitcher) return;
const origFetch = window.fetch;
window.fetch = async (...args) => {
const [resource, config] = args;
const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
// Target only conversation API calls
if (
typeof resource === 'string' &&
resource.includes('/backend-api/f/conversation') &&
config?.body
) {
try {
const body = JSON.parse(config.body);
if (body && body.model) {
// Overwrite model
body.model = savedModel;
config.body = JSON.stringify(body);
}
} catch (e) {
console.warn('Model switcher failed to parse request body', e);
}
}
return origFetch(resource, config);
};
}
// Initialize the userscript
injectModelSwitcher();
overrideModelInRequest();
injectSettingsMenu();
})();