Enhancements for ChatGPT
// ==UserScript==
// @name ChatGPT Zero
// @namespace https://github.com/NextDev65/
// @version 0.60
// @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 GM_xmlhttpRequest
// @grant unsafeWindow
// @connect artificialanalysis.ai
// ==/UserScript==
(function () {
'use strict';
// --- Configuration ---
const PREFERRED_MODEL_KEY = 'preferredChatGPTModel';
const SETTINGS_KEY = 'chatgptZeroSettings';
const DEFAULT_MODEL = 'auto';
const MODELS = {
'gpt-5-3': {
label: 'GPT 5.3',
aaSlug: 'gpt-5-3-codex'
},
'gpt-5-mini': {
label: 'GPT 5 Mini',
aaSlug: 'gpt-5-mini-minimal'
},
'gpt-5-t-mini': {
label: 'GPT 5 Thinking Mini',
aaSlug: 'gpt-5-mini'
},
'auto': {
label: 'Auto',
aaSlug: null
}
};
// Cache configuration for Artificial Analysis stats
const AA_CACHE_KEY = 'aa_model_stats';
const AA_CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours
// User-provided API key storage
const AA_API_KEY_STORAGE = 'aa_api_key';
/**
* Initializes the model stats Promise (if not already created)
*/
let modelStatsPromise = null;
function initStats() {
if (!modelStatsPromise) {
modelStatsPromise = getStats(MODELS).catch(err => {
console.warn('Stats fetch failed', err);
return {};
});
}
}
/**
* Gets the user-provided API key from localStorage
* @returns {string|null}
*/
function getApiKey() {
try {
return localStorage.getItem(AA_API_KEY_STORAGE) || null;
} catch {
return null;
}
}
/**
* Sets or removes the API key in localStorage
* @param {string|null} key - The API key to store, or null/empty to remove
*/
function setApiKey(key) {
try {
if (key) {
localStorage.setItem(AA_API_KEY_STORAGE, key);
} else {
localStorage.removeItem(AA_API_KEY_STORAGE);
}
} catch (e) {
console.warn('Failed to store API key', e);
}
}
// Default settings
const DEFAULT_SETTINGS = {
modelSwitcher: true,
streamerMode: true,
animations: true
};
// Storage helpers (Settings)
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 };
}
}
function saveSettings(settings) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch (e) {
console.warn('Failed to save settings', e);
}
}
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 settings menu.
* @returns {HTMLDivElement}
*/
function createApiKeyInput() {
const container = document.createElement('div');
container.className = 'api-key-container';
const label = document.createElement('label');
label.className = 'api-key-label';
label.textContent = 'Artificial Analysis API Key';
const input = document.createElement('input');
input.type = 'password';
input.className = 'api-key-input';
input.placeholder = 'API key (for model stats)';
input.value = getApiKey() || '';
input.addEventListener('change', () => {
const value = input.value.trim();
// Only store if value starts with 'aa_' (Artificial Analysis key prefix)
if (value.startsWith('aa_')) {
setApiKey(value);
} else {
// Clear invalid key
setApiKey('');
input.value = '';
console.warn('API key should start with \'aa_\'')
}
});
container.appendChild(label);
container.appendChild(input);
return container;
}
function createSettingsMenu() {
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 streamerModeToggle = createToggleSwitch(
'Streamer Mode',
settings.streamerMode ?? true,
(e) => {
settings.streamerMode = e.target.checked;
saveSettings(settings);
updateStreamerModeStyles();
}
);
const animationsToggle = createToggleSwitch('Animations', settings.animations, (e) => {
settings.animations = e.target.checked;
saveSettings(settings);
updateAnimationStyles();
});
menu.appendChild(modelSwitcherToggle);
menu.appendChild(streamerModeToggle);
menu.appendChild(animationsToggle);
// Add API key input
menu.appendChild(createApiKeyInput());
// Append menu to body to avoid positioning issues
document.body.appendChild(menu);
return menu;
}
/**
* Creates and returns a <button> element with an attached settings menu.
* @param {div} menu - The settings menu to be attached
* @returns {HTMLButtonElement}
*/
function createSettingsCog(menu) {
const cog = document.createElement('button');
cog.id = 'settings-cog';
//cog.textContent = settings.animations ? '⚙️' : '⚙';
cog.setAttribute('aria-label', 'Settings');
// 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() {
// cog bounds, changes when cog is rotated (animations enabled) -> alignment inconsistencies
const cogRect = cog.getBoundingClientRect();
// page header bounds
const parentRect = cog.parentElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
menu.style.position = 'fixed';
menu.style.top = `${parentRect.bottom - 5}px`; // 5px above `page-header`
menu.style.zIndex = '10000';
const cogRight = cogRect.left + cogRect.width;
const rightOffset = viewportWidth - cogRight;
// prepare initial state
menu.style.right = `${rightOffset}px`;
menu.style.left = 'auto';
if (settings.animations) {
menu.style.opacity = '0';
menu.style.transform = 'translateX(10px)';
menu.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
/*// force a reflow so the browser registers the start state
// eslint-disable-next-line @microsoft/sdl/no-document-domain -- reflow hack
void menu.offsetWidth;*/
// slide into place
requestAnimationFrame(() => {
menu.style.opacity = '1';
menu.style.transform = 'translateX(0)';
});
}
}
// 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';
style.textContent = `
#settings-cog {
font-size: 20px;
margin-left: 12px;
padding: 4px 5px;
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);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transform: translateX(0.75px) translateY(-0.75px);
transition: background-color var(--anim-fast) var(--easing-standard),
box-shadow var(--anim-slow) var(--easing-standard),
transform var(--anim-normal) var(--easing-transform);
}
#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);
transform: translateX(0.75px) translateY(-0.75px) var(--cog-rotate);
}
#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-cog::before {
content: var(--cog-icon);
transform-origin: center;
transform: translateX(0.75px) translateY(-0.75px);
}
.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;
transition: background-color var(--anim-normal) var(--easing-slider-bg),
transform var(--anim-normal) var(--easing-transform);
}
.toggle-slider:before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: transform var(--anim-normal) var(--easing-transform);
}
.api-key-container {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.api-key-container:last-child {
margin-bottom: 0;
}
.api-key-label {
color: #fff;
font-size: 14px;
}
.api-key-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #555;
border-radius: 4px;
background-color: #1a1a1a;
color: #fff;
font-size: 13px;
box-sizing: border-box;
}
.api-key-input:focus {
outline: none;
border-color: #4CAF50;
}
.api-key-input::placeholder {
color: #777;
}
`;
document.head.appendChild(style);
}
/**
* Updates animation styles based on current settings with CSS custom properties
*/
function updateAnimationStyles() {
const root = document.documentElement;
const animate = settings.animations;
// Durations
root.style.setProperty('--anim-fast', animate ? '0.2s' : '0s');
root.style.setProperty('--anim-normal', animate ? '0.3s' : '0s');
root.style.setProperty('--anim-slow', animate ? '0.4s' : '0s');
// Easing functions
root.style.setProperty('--easing-standard', animate ? 'cubic-bezier(0.4, 0, 0.2, 1)' : '');
root.style.setProperty('--easing-transform', animate ? 'cubic-bezier(0.68, -0.55, 0.27, 1.55)' : '');
root.style.setProperty('--easing-slider-bg', animate ? 'cubic-bezier(0.68, -0.1, 0.27, 1.1)' : '');
// Cog styles
root.style.setProperty('--cog-rotate', animate ? 'rotate(45deg)' : 'rotate(0deg)');
root.style.setProperty('--cog-icon', animate ? "'⚙️'" : "'⚙'");
// Model Switcher glow (initially invisible, transitions from this on hover)
root.style.setProperty('--initial-glow', animate ? `0 0 0 0 rgba(33, 33, 33, 0) inset,
0 0 5px 0 rgba(33, 33, 33, 0)` : 'none');
}
function updateStreamerModeStyles() {
injectStreamerModeStyles();
document.body.classList.toggle('streamer-mode', settings.streamerMode);
}
function injectStreamerModeStyles() {
if (document.getElementById('streamer-styles')) return;
const style = document.createElement('style');
style.id = 'streamer-styles';
style.textContent = `
/* inactive chats */
.streamer-mode #history .__menu-item:not([data-active]) {
box-shadow: 0 0 2.5px 0 rgba(255, 255, 255, 0) inset,
0 0 5px 0 rgba(255, 255, 255, 0.2);
transition: background-color var(--anim-fast) var(--easing-standard),
box-shadow var(--anim-slow) var(--easing-standard);
}
/* inactive chat titles */
.streamer-mode #history .__menu-item:not([data-active]) .truncate span {
opacity: 0;
transition: opacity var(--anim-fast) var(--easing-standard),
box-shadow var(--anim-slow) var(--easing-standard);
}
.streamer-mode #history .__menu-item:not([data-active]):hover .truncate span {
opacity: 1;
}
/* accounts profile */
.streamer-mode [data-testid="accounts-profile-button"] {
display: none !important;
}
`;
document.head.appendChild(style);
}
/**
* 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';
style.textContent = `
#chatgpt-model-switcher {
margin: auto;
padding: 4px 8px;
border: none;
border-radius: 6px;
background-color: #212121;
color: #fff;
outline: none;
box-shadow: var(--initial-glow);
transition: background-color var(--anim-fast) var(--easing-standard),
box-shadow var(--anim-slow) var(--easing-standard);
}
#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);
}
/**
* Unified function to get model stats from Artificial Analysis API.
* Uses caching to avoid repeated API calls.
* @param {object} modelConfig - The MODELS configuration object
* @returns {Promise<object>} - Promise resolving to stats object { slug: index }
*/
function getStats(modelConfig) {
const API_KEY = getApiKey();
// If no API key, fail silently and return empty stats
if (!API_KEY) {
return Promise.resolve({});
}
const URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
const now = Date.now();
let cache = null;
try {
cache = JSON.parse(localStorage.getItem(AA_CACHE_KEY));
} catch {}
const cachedStats = cache?.stats || {};
const timestamp = cache?.timestamp || 0;
const isExpired = (now - timestamp) > AA_CACHE_TTL;
// Determine required models
const missing = Object.entries(modelConfig)
.filter(([slug, cfg]) => cfg.aaSlug && cachedStats[slug] === undefined)
.map(([slug]) => slug);
// Use cache if valid and complete
if (!isExpired && missing.length === 0) {
return Promise.resolve(cachedStats);
}
// Fetch from API
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: URL,
headers: { "x-api-key": API_KEY },
onload: function (response) {
try {
const json = JSON.parse(response.responseText);
const newStats = { ...cachedStats };
for (const [slug, config] of Object.entries(modelConfig)) {
if (!config.aaSlug) continue;
const model = json.data.find(m => m.slug === config.aaSlug);
if (!model) {
console.warn(config.aaSlug, "not found");
continue;
}
newStats[slug] =
model.evaluations?.artificial_analysis_intelligence_index ?? null;
}
// Save updated cache
localStorage.setItem(AA_CACHE_KEY, JSON.stringify({
timestamp: now,
stats: newStats
}));
resolve(newStats);
} catch (err) {
reject(err);
}
},
onerror: reject
});
});
}
/**
* Creates and returns a <select> element configured as the model switcher.
* @param {string} currentModel - Model to pre-select in the dropdown.
* @returns {HTMLSelectElement}
*/
async function createModelSwitcher(currentModel) {
const select = document.createElement('select');
select.id = 'chatgpt-model-switcher';
// Inject CSS for base styling, hover, focus, and transition effects
injectModelSwitcherStyles();
// Fetch stats
let stats = {};
try {
stats = await modelStatsPromise;
} catch (e) {
console.warn('Failed to load stats', e);
}
// Populate dropdown with model options
for (const [slug, config] of Object.entries(MODELS)) {
const option = document.createElement('option');
option.value = slug;
option.textContent = config.label || slug;
if (slug === currentModel) {
option.selected = true;
}
// Tooltip with index (only show if data is available)
if (stats[slug] != null) {
option.title = `AA Index: ${stats[slug]}`;
} else {
option.title = '';
}
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');
if (!modelSwitcher) return; // Wait until the model switcher is available
let cog = document.getElementById('settings-cog');
let menu = document.getElementById('settings-menu');
// Create menu if it doesn't exist yet
if (!menu) {
menu = createSettingsMenu();
}
// Create cog + Insert cog before toolbar
if (!cog) {
cog = createSettingsCog(menu);
//modelSwitcher.after(cog);
document.getElementById('page-header').lastChild.prepend(cog); // last child of page header
}
}, 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.
*/
async function injectModelSwitcher() {
const checkInterval = setInterval(async () => {
const nativeModelSwitchers = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
let switcher = document.getElementById('chatgpt-model-switcher');
const getPlusClassName = ['absolute start-1/2 flex flex-col items-center gap-2 ltr:-translate-x-1/2 rtl:translate-x-1/2',
'pointer-events-none absolute start-0 flex flex-col items-center gap-2 lg:start-1/2 ltr:-translate-x-1/2 rtl:translate-x-1/2'];
// Create switcher
if (!switcher) {
const savedModel = localStorage.getItem(PREFERRED_MODEL_KEY) || DEFAULT_MODEL;
switcher = await 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);
// move "Get Plus" button
let getPlus = null;
for (let className of getPlusClassName) {
let elements = document.getElementsByClassName(className);
if (elements.length > 0) {
// give getPlus styling to switcher
switcher.className = getPlusClassName;
getPlus = elements[0];
break;
}
}
nativeModelSwitcher.parentNode.appendChild(getPlus);
getPlus.className = '';
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 = unsafeWindow.fetch;
unsafeWindow.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
initStats();
injectModelSwitcher();
overrideModelInRequest();
updateStreamerModeStyles();
injectSettingsMenu();
updateAnimationStyles();
})();