// © 2023–2024 KudoAI & contributors under the MIT license.
// Source: https://github.com/KudoAI/chatgpt.js
// User guide: https://chatgptjs.org/userguide
// Latest minified release: https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js/chatgpt.min.js
// Init endpoints
const endpoints = {
assets: 'https://cdn.jsdelivr.net/gh/KudoAI/chatgpt.js',
openAI: {
session: 'https://chatgpt.com/api/auth/session',
chats: 'https://chat.openai.com/backend-api/conversations',
chat: 'https://chat.openai.com/backend-api/conversation',
share_create: 'https://chat.openai.com/backend-api/share/create',
share: 'https://chat.openai.com/backend-api/share',
instructions: 'https://chat.openai.com/backend-api/user_system_messages'
}
};
// Init feedback properties
localStorage.alertQueue = JSON.stringify([]);
localStorage.notifyProps = JSON.stringify({ queue: { topRight: [], bottomRight: [], bottomLeft: [], topLeft: [] }});
// Define chatgpt.methods
const chatgpt = { // eslint-disable-line no-redeclare
openAIaccessToken: {},
actAs: function(persona) {
// Prompts ChatGPT to act as a persona from https://github.com/KudoAI/chat-prompts/blob/main/personas.json
const promptsUrl = 'https://raw.githubusercontent.com/KudoAI/chat-prompts/main/dist/personas.min.json';
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', promptsUrl, true); xhr.send();
xhr.onload = () => {
if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve prompts data.');
const data = JSON.parse(xhr.responseText).personas;
if (!persona) {
console.log('\n%c🤖 chatgpt.js personas\n',
'font-family: sans-serif ; font-size: xxx-large ; font-weight: bold');
for (const prompt of data) // list personas
console.log(`%c${ prompt.title }`, 'font-family: monospace ; font-size: larger ;');
return resolve();
}
const selectedPrompt = data.find(obj => obj.title.toLowerCase() == persona.toLowerCase());
if (!selectedPrompt)
return reject(`🤖 chatgpt.js >> Persona '${ persona }' was not found!`);
chatgpt.send(selectedPrompt.prompt, 'click');
console.info(`Loading ${ persona } persona...`);
chatgpt.isIdle().then(() => { console.info('Persona activated!'); });
return resolve();
};
});
},
activateDarkMode: function() {
document.documentElement.classList.replace('light', 'dark');
document.documentElement.style.colorScheme = 'dark';
localStorage.setItem('theme', 'dark');
},
activateLightMode: function() {
document.documentElement.classList.replace('dark', 'light');
document.documentElement.style.colorScheme = 'light';
localStorage.setItem('theme', 'light');
},
alert: function(title, msg, btns, checkbox, width) {
// [ title/msg = strings, btns = [named functions], checkbox = named function, width (px) = int ] = optional
// * Spaces are inserted into button labels by parsing function names in camel/kebab/snake case
const scheme = chatgpt.isDarkMode() ? 'dark' : 'light',
isMobile = chatgpt.browser.isMobile();
// Create modal parent/children elements
const modalContainer = document.createElement('div');
modalContainer.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now();
modalContainer.classList.add('chatgpt-modal'); // add class to main div
const modal = document.createElement('div'),
modalTitle = document.createElement('h2'),
modalMessage = document.createElement('p');
// Create/append/update modal style (if missing or outdated)
const thisUpdated = 20231203; // datestamp of last edit for this file's `modalStyle`
let modalStyle = document.querySelector('#chatgpt-modal-style'); // try to select existing style
if (!modalStyle || parseInt(modalStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
if (!modalStyle) { // outright missing, create/id/attr/append it first
modalStyle = document.createElement('style'); modalStyle.id = 'chatgpt-modal-style';
modalStyle.setAttribute('last-updated', thisUpdated.toString());
document.head.append(modalStyle);
}
modalStyle.innerText = ( // update prev/new style contents
// Background styles
'.chatgpt-modal {'
+ 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
+ 'background-color: rgba(67, 70, 72, 0) ;' // init dim bg but no opacity
+ 'transition: background-color 0.05s ease ;' // speed to transition in show alert routine
+ 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
// Alert styles
+ '.chatgpt-modal > div {'
+ 'opacity: 0 ; transform: translateX(-2px) translateY(5px) ; max-width: 75vw ; word-wrap: break-word ;'
+ 'transition: opacity 0.1s cubic-bezier(.165,.84,.44,1), transform 0.2s cubic-bezier(.165,.84,.44,1) ;'
+ `background-color: ${ scheme == 'dark' ? 'black' : 'white' } ;`
+ ( scheme != 'dark' ? 'border: 1px solid rgba(0, 0, 0, 0.3) ;' : '' )
+ 'padding: 20px ; margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
+ ' -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ; }'
+ '.chatgpt-modal h2 { margin-bottom: 9px }'
+ `.chatgpt-modal a { color: ${ scheme == 'dark' ? '#00cfff' : '#1e9ebb' }}`
+ '.chatgpt-modal.animated > div { opacity: 1 ; transform: translateX(0) translateY(0) }'
+ '@keyframes alert-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }'
+ '50% { opacity: 0.25 ; transform: scale(1.05) }'
+ '100% { opacity: 0 ; transform: scale(1.35) }}'
// Button styles
+ '.modal-buttons { display: flex ; justify-content: flex-end ; margin: 20px -5px -3px 0 ;'
+ ( isMobile ? 'flex-direction: column-reverse' : '' ) + '}'
+ '.chatgpt-modal button {'
+ `margin-left: ${ isMobile ? 0 : 10}px ; padding: ${ isMobile ? 15 : 4}px 18px ; border-radius: 15px ;`
+ ( isMobile ? 'margin-top: 5px ; margin-bottom: 3px ;' : '')
+ `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}`
+ '.primary-modal-btn {'
+ `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;`
+ `background: ${ scheme == 'dark' ? 'white' : 'black' } ;`
+ `color: ${ scheme == 'dark' ? 'black' : 'white' }}`
+ '.chatgpt-modal button:hover { color: #3d5d71 ; border-color: #6d9cb9 ;'
+ 'background-color: ' + ( scheme == 'dark' ? '#00cfff' : '#9cdaff' ) + ';'
+ 'box-shadow: 2px 1px ' + ( scheme == 'dark' ? '54px #00cfff' : '30px #9cdaff' ) + '}'
+ '.modal-close-btn {'
+ 'cursor: pointer ; width: 20px ; height: 20px ; float: right ; position: relative ; right: -2px }'
+ '.modal-close-btn svg { margin: 5px 5px }' // center SVG for hover overlay
+ `.modal-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
// Checkbox styles
+ '.chatgpt-modal .checkbox-group { display: flex ; margin-top: -18px }'
+ '.chatgpt-modal .checkbox-group label {'
+ 'font-size: .7rem ; margin: -.04rem 0 0px .3rem ;'
+ `color: ${ scheme == 'dark' ? '#e1e1e1' : '#1e1e1e' }}`
+ '.chatgpt-modal input[type="checkbox"] { transform: scale(0.7) ;'
+ `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}`
+ '.chatgpt-modal input[type="checkbox"]:checked {'
+ `border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' } ;`
+ 'background-color: black ; position: inherit }'
+ '.chatgpt-modal input[type="checkbox"]:focus { outline: none ; box-shadow: none }'
);
}
// Insert text into elements
modalTitle.innerText = title || '';
modalMessage.innerText = msg || ''; this.renderHTML(modalMessage);
// Create/append buttons (if provided) to buttons div
const modalButtons = document.createElement('div');
modalButtons.classList.add('modal-buttons');
if (btns) { // are supplied
if (!Array.isArray(btns)) btns = [btns]; // convert single button to array if necessary
btns.forEach((buttonFn) => { // create title-cased labels + attach listeners
const button = document.createElement('button');
button.textContent = buttonFn.name
.replace(/[_-]\w/g, match => match.slice(1).toUpperCase()) // convert snake/kebab to camel case
.replace(/([A-Z])/g, ' $1') // insert spaces
.replace(/^\w/, firstChar => firstChar.toUpperCase()); // capitalize first letter
button.addEventListener('click', () => { dismissAlert(); buttonFn(); });
modalButtons.insertBefore(button, modalButtons.firstChild); // insert button to left
});
}
// Create/append OK/dismiss button to buttons div
const dismissBtn = document.createElement('button');
dismissBtn.textContent = btns ? 'Dismiss' : 'OK';
modalButtons.insertBefore(dismissBtn, modalButtons.firstChild);
// Highlight primary button
modalButtons.lastChild.classList.add('primary-modal-btn');
// Create/append checkbox (if provided) to checkbox group div
const checkboxDiv = document.createElement('div');
if (checkbox) { // is supplied
checkboxDiv.classList.add('checkbox-group');
const checkboxFn = checkbox, // assign the named function to checkboxFn
checkboxInput = document.createElement('input');
checkboxInput.type = 'checkbox';
checkboxInput.addEventListener('change', checkboxFn);
// Create/show label
const checkboxLabel = document.createElement('label');
checkboxLabel.addEventListener('click', () => {
checkboxInput.checked = !checkboxInput.checked; checkboxFn(); });
checkboxLabel.textContent = checkboxFn.name.charAt(0).toUpperCase() // capitalize first char
+ checkboxFn.name.slice(1) // format remaining chars
.replace(/([A-Z])/g, (match, letter) => ' ' + letter.toLowerCase()) // insert spaces, convert to lowercase
.replace(/\b(\w+)nt\b/gi, '$1n\'t') // insert apostrophe in 'nt' suffixes
.trim(); // trim leading/trailing spaces
checkboxDiv.append(checkboxInput); checkboxDiv.append(checkboxLabel);
}
// Create close button
const closeBtn = document.createElement('div');
closeBtn.title = 'Close'; closeBtn.classList.add('modal-close-btn');
const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
closeSVG.setAttribute('height', '10px');
closeSVG.setAttribute('viewBox', '0 0 14 14');
closeSVG.setAttribute('fill', 'none');
const closeSVGpath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
closeSVGpath.setAttribute('fill-rule', 'evenodd');
closeSVGpath.setAttribute('clip-rule', 'evenodd');
closeSVGpath.setAttribute('fill', chatgpt.isDarkMode() ? 'white' : 'black');
closeSVGpath.setAttribute('d', 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z');
closeSVG.append(closeSVGpath); closeBtn.append(closeSVG);
// Assemble/append div
const modalElems = [closeBtn, modalTitle, modalMessage, modalButtons, checkboxDiv];
modalElems.forEach((elem) => { modal.append(elem); });
modal.style.width = `${ width || 458 }px`;
modalContainer.append(modal); document.body.append(modalContainer);
// Enqueue alert
let alertQueue = JSON.parse(localStorage.alertQueue);
alertQueue.push(modalContainer.id);
localStorage.alertQueue = JSON.stringify(alertQueue);
// Show alert if none active
modalContainer.style.display = 'none';
if (alertQueue.length === 1) {
modalContainer.style.display = '';
setTimeout(() => { // delay non-0 opacity's for transition fx
modalContainer.style.backgroundColor = (
`rgba(67, 70, 72, ${ scheme === 'dark' ? 0.62 : 0 })`);
modalContainer.classList.add('animated'); }, 100);
}
// Define click/key handlers
const clickHandler = event => { // explicitly defined to support removal post-dismissal
if (event.target == event.currentTarget || event.target instanceof SVGPathElement) dismissAlert(); };
const keyHandler = event => { // to dismiss active alert
const dismissKeys = [13, 27]; // enter/esc
if (dismissKeys.includes(event.keyCode)) {
for (const alertId of alertQueue) { // look to handle only if triggering alert is active
const alert = document.getElementById(alertId);
if (alert && alert.style.display !== 'none') { // active alert found
if (event.keyCode === 27) dismissAlert(); // if esc pressed, dismiss alert & do nothing
else if (event.keyCode === 13) { // else if enter pressed
const mainButton = alert.querySelector('.modal-buttons').lastChild; // look for main button
if (mainButton) { mainButton.click(); event.preventDefault(); } // click if found
} return;
}}}};
// Add listeners to dismiss alert
const dismissElems = [modalContainer, closeBtn, closeSVG, dismissBtn];
dismissElems.forEach(elem => {
elem.addEventListener('click', clickHandler); });
document.addEventListener('keydown', keyHandler);
// Define alert dismisser
const dismissAlert = () => {
modalContainer.style.backgroundColor = 'transparent';
modal.style.animation = 'alert-zoom-fade-out 0.075s ease-out';
setTimeout(() => { // delay removal for fade-out
// Remove alert
modalContainer.remove(); // ...from DOM
alertQueue = JSON.parse(localStorage.alertQueue);
alertQueue.shift(); // + memory
localStorage.alertQueue = JSON.stringify(alertQueue); // + storage
// Remove all listeners to prevent memory leaks
dismissElems.forEach(elem => { elem.removeEventListener('click', clickHandler); });
document.removeEventListener('keydown', keyHandler);
// Check for pending alerts in queue
if (alertQueue.length > 0) {
const nextAlert = document.getElementById(alertQueue[0]);
setTimeout(() => {
nextAlert.style.display = '';
setTimeout(() => { nextAlert.classList.add('animated'); }, 100);
}, 500);
}
}, 50);
};
return modalContainer.id; // if assignment used
},
askAndGetReply: async function(query) {
chatgpt.send(query); await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
autoRefresh: {
activate: function(interval) {
if (this.isActive) { // already running, do nothing
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Auto refresh already active!'); return; }
const autoRefresh = this;
// Run main activate routine
this.toggle.refreshFrame();
const scheduleRefreshes = interval => {
const randomDelay = Math.max(2, Math.floor(chatgpt.randomFloat() * 21 - 10)); // set random delay up to ±10 secs
autoRefresh.isActive = setTimeout(() => {
const manifestScript = document.querySelector('script[src*="_ssgManifest.js"]');
document.querySelector('#refresh-frame').src = manifestScript.src + '?' + Date.now();
console.log('↻ ChatGPT >> [' + autoRefresh.nowTimeStamp() + '] ChatGPT session refreshed');
scheduleRefreshes(interval);
}, (interval + randomDelay) * 1000);
};
scheduleRefreshes( interval ? parseInt(interval, 10) : 30 );
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Auto refresh activated');
// Add listener to send beacons in Chromium to thwart auto-discards if Page Visibility API supported
if (navigator.userAgent.includes('Chrome') && typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', this.toggle.beacons); }
},
deactivate: function() {
if (this.isActive) {
this.toggle.refreshFrame();
document.removeEventListener('visibilitychange', this.toggle.beacons);
clearTimeout(this.isActive); this.isActive = null;
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Auto refresh de-activated');
} else { console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Auto refresh already inactive!'); }
},
nowTimeStamp: function() {
const now = new Date();
const hours = now.getHours() % 12 || 12; // Convert to 12-hour format
let minutes = now.getMinutes(), seconds = now.getSeconds();
if (minutes < 10) minutes = '0' + minutes; if (seconds < 10) seconds = '0' + seconds;
const meridiem = now.getHours() < 12 ? 'AM' : 'PM';
return hours + ':' + minutes + ':' + seconds + ' ' + meridiem;
},
toggle: {
beacons: function() {
if (chatgpt.autoRefresh.beaconID) {
clearInterval(chatgpt.autoRefresh.beaconID); chatgpt.autoRefresh.beaconID = null;
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Beacons de-activated');
} else {
chatgpt.autoRefresh.beaconID = setInterval(() => {
navigator.sendBeacon('https://httpbin.org/post', new Uint8Array());
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Beacon sent');
}, 90000);
console.log('↻ ChatGPT >> [' + chatgpt.autoRefresh.nowTimeStamp() + '] Beacons activated');
}
},
refreshFrame: function() {
let refreshFrame = document.querySelector('#refresh-frame');
if (refreshFrame) refreshFrame.remove();
else {
refreshFrame = Object.assign(document.createElement('iframe'),
{ id: 'refresh-frame', style: 'display: none' });
document.head.prepend(refreshFrame);
}
}
}
},
browser: {
isLightMode: function() { return window.matchMedia?.('(prefers-color-scheme: light)')?.matches; },
isDarkMode: function() { return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches; },
isChromium: function() { return navigator.userAgent.includes('Chrome'); },
isFirefox: function() { return navigator.userAgent.includes('Firefox'); },
isFullScreen: function() {
const userAgentStr = navigator.userAgent;
return userAgentStr.includes('Chrome') ? window.matchMedia('(display-mode: fullscreen)').matches
: userAgentStr.includes('Firefox') ? window.fullScreen
: /MSIE|rv:/.test(userAgentStr) ? document.msFullscreenElement : document.webkitIsFullScreen;
},
isMobile: function() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); }
},
clearChats: async function(method) {
// Validate method arg
const validMethods = ['api', 'dom'];
method = (method || 'dom').trim().toLowerCase(); // set to 'dom' by default
if (method && !validMethods.includes(method))
return console.log(`Method argument must be one of: [${ validMethods }]`);
if (method == 'dom') {
try { await chatgpt.getChatData(); } catch { return; } // check if chat history exists
chatgpt.menu.open(); setTimeout(() => { // open settings
const settingsBtn = document.querySelector(
'a[role="menuitem"] svg path[d*="M12.003 10.5a1.5"]')?.parentNode.parentNode;
if (settingsBtn) settingsBtn.click();
setTimeout(() => { // clear chats
const settingsBtns = document.querySelectorAll('[id*=radix] button'),
deleteBtn = settingsBtns[settingsBtns.length - 1];
if (deleteBtn) deleteBtn.click();
setTimeout(() => { // confirm clear
document.querySelector('button[class*="danger"').click();
}, 10); }, 333); }, 10);
} else { // API method
// NOTE: DOM is not updated to reflect new empty chat list (until session refresh)
return new Promise((resolve, reject) => {
chatgpt.getAccessToken().then(token => {
const xhr = new XMLHttpRequest();
xhr.open('PATCH', endpoints.openAI.chats, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot clear chats.');
console.info('Chats successfully cleared'); resolve();
};
xhr.send(JSON.stringify({ is_visible: false }));
}).catch(reject);
});
}
},
code: {
// Tip: Use template literals for easier passing of code arguments. Ensure backticks and `$`s are escaped (using `\`)
execute: async function(code) {
if (!code) return console.error('Code argument not supplied. Pass some code!');
if (typeof code !== 'string') return console.error('Code argument must be a string!');
chatgpt.send('Display the output as if you were terminal:\n\n' + code);
console.info('Executing code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
},
extract: function(msg) { // extract pure code from response (targets last block)
const codeBlocks = msg.match(/(?<=```.*\n)[\s\S]*?(?=```)/g);
return codeBlocks ? codeBlocks[codeBlocks.length - 1] : msg;
},
minify: async function(code) {
if (!code) return console.error('Code argument not supplied. Pass some code!');
if (typeof code !== 'string') return console.error('Code argument must be a string!');
chatgpt.send('Minify the following code:\n\n' + code);
console.info('Minifying code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
},
obfuscate: async function(code) {
if (!code) return console.error('Code argument not supplied. Pass some code!');
if (typeof code !== 'string') return console.error('Code argument must be a string!');
chatgpt.send('Obfuscate the following code:\n\n' + code);
console.info('Obfuscating code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
},
refactor: async function(code, objective) {
if (!code) return console.error('Code (1st) argument not supplied. Pass some code!');
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string.`);
chatgpt.send('Refactor the following code for ' + (objective || 'brevity') + ':\n\n' + code);
console.info('Refactoring code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
},
review: async function(code) {
if (!code) return console.error('Code argument not supplied. Pass some code!');
if (typeof code !== 'string') return console.error('Code argument must be a string!');
chatgpt.send('Review the following code for me:\n\n' + code);
console.info('Reviewing code...');
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
unminify: async function(code) {
if (!code) return console.error('Code argument not supplied. Pass some code!');
if (typeof code !== 'string') return console.error('Code argument must be a string!');
chatgpt.send('Unminify the following code.:\n\n' + code);
console.info('Unminifying code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
},
write: async function(prompt, outputLang) {
if (!prompt) return console.error('Prompt (1st) argument not supplied. Pass a prompt!');
if (!outputLang) return console.error('outputLang (2nd) argument not supplied. Pass a language!');
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string.`);
chatgpt.send(prompt + '\n\nWrite this as code in ' + outputLang);
console.info('Writing code...');
await chatgpt.isIdle();
return chatgpt.code.extract(await chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'));
}
},
detectLanguage: async function(text) {
if (!text) return console.error('Text argument not supplied. Pass some text!');
if (typeof text !== 'string') return console.error('Text argument must be a string!');
chatgpt.send('Detect the language of the following text:\n\n' + text
+ '\n\nOnly respond with the name of the language');
console.info('Reviewing text...');
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
executeCode: function() { chatgpt.code.execute(); },
exportChat: async function(chatToGet, format) {
// chatToGet = 'active' (default) | 'latest' | index|title|id of chat to get
// format = 'html' (default) | 'md' | 'pdf' | 'text'
// Init args
chatToGet = !chatToGet ? 'active' // default to 'active' if unpassed
: Number.isInteger(chatToGet) || /^\d+$/.test(chatToGet) ? // else if string/int num passed
parseInt(chatToGet, 10) // parse as integer
: chatToGet; // else preserve non-num string as 'active', 'latest' or title/id of chat to get
format = format.toLowerCase() || 'html'; // default to 'html' if unpassed
// Create transcript + filename
console.info('Generating transcript...');
let transcript = '', filename;
if (/te?xt/.test(format)) { // generate plain transcript + filename for TXT export
// Format filename using date/time
const now = new Date(),
day = now.getDate().toString().padStart(2, '0'),
month = (now.getMonth() + 1).toString().padStart(2, '0'),
year = now.getFullYear(),
hour = now.getHours().toString().padStart(2, '0'),
minute = now.getMinutes().toString().padStart(2, '0');
filename = `ChatGPT_${ day }-${ month }-${ year }_${ hour }-${ minute }.txt`;
// Create transcript from active chat
if (chatToGet == 'active' && /\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(window.location.href)) {
const chatDivs = document.querySelectorAll('main > div > div > div > div > div > div[class*=group]');
if (chatDivs.length === 0) return console.error('Chat is empty!');
const msgs = []; let isUserMsg = true;
chatDivs.forEach((div) => {
const sender = isUserMsg ? 'USER' : 'CHATGPT'; isUserMsg = !isUserMsg;
let msg = Array.from(div.childNodes).map(node => node.innerText)
.join('\n\n') // insert double line breaks between paragraphs
.replace('Copy code', '');
msgs.push(sender + ': ' + msg);
});
transcript = msgs.join('\n\n');
// ...or from getChatData(chatToGet)
} else {
for (const entry of await chatgpt.getChatData(chatToGet, 'msg', 'both', 'all')) {
transcript += `USER: ${ entry.user }\n\n`;
transcript += `CHATGPT: ${ entry.chatgpt }\n\n`;
}}
} else { // generate rich transcript + filename for HTML/MD/PDF export
// Fetch HTML transcript from OpenAI
const response = await fetch(await chatgpt.shareChat(chatToGet)),
htmlContent = await response.text();
// Format filename after
const parser = new DOMParser(),
parsedHtml = parser.parseFromString(htmlContent, 'text/html');
filename = parsedHtml.querySelector('title').textContent + '.html';
// Convert relative CSS paths to absolute ones
const cssLinks = parsedHtml.querySelectorAll('link[rel="stylesheet"]');
cssLinks.forEach(link => {
const href = link.getAttribute('href');
if (href?.startsWith('/')) link.setAttribute('href', 'https://chat.openai.com' + href);
});
// Serialize updated HTML to string
transcript = new XMLSerializer().serializeToString(parsedHtml);
}
// Export transcript
console.info(`Exporting transcript as ${ format.toUpperCase() }...`);
if (format == 'pdf') { // convert SVGs + launch PDF printer
// Convert SVG icons to data URLs for proper PDF rendering
transcript = transcript.replace(//g, (match) => {
const dataURL = 'data:image/svg+xml,' + encodeURIComponent(match);
return ``;
});
// Launch PDF printer
const transcriptPopup = window.open('', '', 'toolbar=0, location=0, menubar=0, height=600, width=800');
transcriptPopup.document.write(transcript);
setTimeout(() => { transcriptPopup.print({ toPDF: true }); }, 100);
} else { // auto-save to file
if (format == 'md') { // remove extraneous HTML + fix file extension
const mdMatch = /<.*(?:.*?)<[^/]/.exec(transcript)[1];
transcript = mdMatch || transcript; filename = filename.replace('.html', '.md');
}
const blob = new Blob([transcript],
{ type: 'text/' + ( format == 'html' ? 'html' : format == 'md' ? 'markdown' : 'plain' )});
const link = document.createElement('a'), blobURL = URL.createObjectURL(blob);
link.href = blobURL; link.download = filename; document.body.append(link);
link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobURL);
}
},
extractCode: function() { chatgpt.code.extract(); },
generateRandomIP: function() {
const ip = Array.from({length: 4}, () => Math.floor(chatgpt.randomFloat() * 256)).join('.');
console.info('IP generated: ' + ip);
return ip;
},
get: function(targetType, targetName = '') {
// targetType = 'button'|'link'|'div'|'response'
// targetName = from get[targetName][targetType] methods, e.g. 'send'
// Validate argument types to be string only
if (typeof targetType !== 'string' || typeof targetName !== 'string') {
throw new TypeError('Invalid arguments. Both arguments must be strings.'); }
// Validate targetType
if (!targetTypes.includes(targetType.toLowerCase())) {
throw new Error('Invalid targetType: ' + targetType
+ '. Valid values are: ' + JSON.stringify(targetTypes)); }
// Validate targetName scoped to pre-validated targetType
const targetNames = [], reTargetName = new RegExp('^get(.*)' + targetType + '$', 'i');
for (const prop in chatgpt) {
if (typeof chatgpt[prop] == 'function' && reTargetName.test(prop)) {
targetNames.push( // add found targetName to valid array
prop.replace(reTargetName, '$1').toLowerCase());
}}
if (!targetNames.includes(targetName.toLowerCase())) {
throw new Error('Invalid targetName: ' + targetName + '. '
+ (targetNames.length > 0 ? 'Valid values are: ' + JSON.stringify(targetNames)
: 'targetType ' + targetType.toLowerCase() + ' does not require additional options.'));
}
// Call target function using pre-validated name components
const targetFuncNameLower = ('get' + targetName + targetType).toLowerCase();
const targetFuncName = Object.keys(this).find( // find originally cased target function name
(name) => { return name.toLowerCase() == targetFuncNameLower; }); // test for match
return this[targetFuncName](); // call found function
},
getAccessToken: function() {
return new Promise((resolve, reject) => {
if (Object.keys(chatgpt.openAIaccessToken).length > 0 && // populated
(Date.parse(chatgpt.openAIaccessToken.expireDate) - Date.parse(new Date()) >= 0)) // not expired
return resolve(chatgpt.openAIaccessToken.token);
const xhr = new XMLHttpRequest();
xhr.open('GET', endpoints.openAI.session, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve access token.');
console.info('Token expiration: ' + new Date(JSON.parse(xhr.responseText).expires).toLocaleString().replace(',', ' at'));
chatgpt.openAIaccessToken = {
token: JSON.parse(xhr.responseText).accessToken,
expireDate: JSON.parse(xhr.responseText).expires
};
return resolve(chatgpt.openAIaccessToken.token);
};
xhr.send();
});
},
getAccountDetails: function(...details) {
// details = [email|id|image|name|picture] = optional
// Build details array
const validDetails = [ 'email', 'id', 'image', 'name', 'picture' ];
details = ( !arguments[0] ? validDetails // no details passed, populate w/ all valid ones
: Array.isArray(arguments[0]) ? arguments[0] // details array passed, do nothing
: Array.from(arguments) ); // details arg(s) passed, convert to array
// Validate detail args
for (const detail of details) {
if (!validDetails.includes(detail)) { return console.error(
'Invalid detail arg \'' + detail + '\' supplied. Valid details are:\n'
+ ' [' + validDetails + ']'); }}
// Return account details
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', endpoints.openAI.session, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText).user, detailsToReturn = {};
for (const detail of details) detailsToReturn[detail] = data[detail];
return resolve(detailsToReturn);
} else return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve account details.');
};
xhr.send();
});
},
getChatBox: function() { return document.getElementById('prompt-textarea'); },
getChatData: function(chatToGet = 1, detailsToGet = 'all', sender = 'all', msgToGet = 'all') {
// chatToGet = 'active' | 'latest' | index|title|id of chat to get (defaults to active OpenAI chat > latest chat)
// detailsToGet = 'all' | [ 'id' | 'title' | 'create_time' | 'update_time' | 'msg' ] (defaults to 'all', excludes msg's)
// sender = ( 'all' | 'both' ) | 'user' | 'chatgpt' (defaults to 'all', requires 2nd param = 'msg')
// msgToGet = 'all' | 'latest' | index of msg to get (defaults to 'all', requires 2nd param = 'msg')
// Init args
const validDetails = [ 'all', 'id', 'title', 'create_time', 'update_time', 'msg' ];
const validSenders = [ 'all', 'both', 'user', 'chatgpt' ];
chatToGet = !chatToGet ? 'active' // if '' passed, set to active
: Number.isInteger(chatToGet) || /^\d+$/.test(chatToGet) ? // else if string/int num passed
( parseInt(chatToGet, 10) === 0 ? 0 : parseInt(chatToGet, 10) - 1 ) // ...offset -1 or keep as 0
: chatToGet; // else preserve non-num string as 'active', 'latest' or title/id of chat to get
detailsToGet = ['all', ''].includes(detailsToGet) ? // if '' or 'all' passed
validDetails.filter(detail => /^(?!all$|msg$).*/.test(detail)) // populate w/ [validDetails] except 'all' & 'msg'
: Array.isArray(detailsToGet) ? detailsToGet : [detailsToGet]; // else convert to array if needed
sender = !sender ? 'all' // if '' or unpassed, set to 'all'
: validSenders.includes(sender) ? sender : 'invalid'; // else set to validSenders or 'invalid'
msgToGet = Number.isInteger(msgToGet) || /^\d+$/.test(msgToGet) ? // if string/int num passed
( parseInt(msgToGet, 10) === 0 ? 0 : parseInt(msgToGet, 10) - 1 ) // ...offset -1 or keep as 0
: ['all', 'latest'].includes(msgToGet.toLowerCase()) ? // else if 'all' or 'latest' passed
msgToGet.toLowerCase() // ...preserve it
: !msgToGet ? 'all' // else if '', set to 'all'
: 'invalid'; // else set 'invalid' for validation step
// Validate args
for (const detail of detailsToGet) {
if (!validDetails.includes(detail)) { return console.error(
'Invalid detail arg \'' + detail + '\' passed. Valid details are:\n'
+ ' [' + validDetails + ']'); }}
if (sender == 'invalid') { return console.error(
'Invalid sender arg passed. Valid senders are:\n'
+ ' [' + validSenders + ']'); }
if (msgToGet == 'invalid') { return console.error(
'Invalid msgToGet arg passed. Valid msg\'s to get are:\n'
+ ' [ \'all\' | \'latest\' | index of msg to get ]'); }
const getChatDetails = (token, detailsToGet) => {
const re_chatID = /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', endpoints.openAI.chats, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve chat details.');
const data = JSON.parse(xhr.responseText).items;
if (data.length <= 0) return reject('🤖 chatgpt.js >> Chat list is empty.');
const detailsToReturn = {};
// Return by index if num, 'latest', or 'active' passed but not truly active
if (Number.isInteger(chatToGet) || chatToGet == 'latest' ||
(chatToGet == 'active' && !new RegExp('\/' + re_chatID.source + '$').test(window.location.href))) {
chatToGet = Number.isInteger(chatToGet) ? chatToGet : 0; // preserve index, otherwise get latest
if (chatToGet > data.length) { // reject if index out-of-bounds
return reject('🤖 chatgpt.js >> Chat with index ' + ( chatToGet + 1 )
+ ' is out of bounds. Only ' + data.length + ' chats exist!'); }
for (const detail of detailsToGet) detailsToReturn[detail] = data[chatToGet][detail];
return resolve(detailsToReturn);
}
// Return by title, ID or active chat
const chatIdentifier = ( // determine to check by ID or title
chatToGet == 'active' || new RegExp('^' + re_chatID.source + '$').test(chatToGet) ? 'id' : 'title' );
if (chatToGet == 'active') // replace chatToGet w/ actual ID
chatToGet = re_chatID.exec(window.location.href)[0];
let idx, chatFound; // index of potentially found chat, flag if found
for (idx = 0; idx < data.length; idx++) { // search for id/title to set chatFound flag
if (data[idx][chatIdentifier] == chatToGet) { chatFound = true; break; }}
if (!chatFound) // exit
return reject('🤖 chatgpt.js >> No chat with ' + chatIdentifier + ' = ' + chatToGet + ' found.');
for (const detail of detailsToGet) detailsToReturn[detail] = data[idx][detail];
return resolve(detailsToReturn);
};
xhr.send();
});};
const getChatMsgs = token => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
getChatDetails(token, ['id']).then(chat => {
xhr.open('GET', `${endpoints.openAI.chat}/${chat.id}`, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200) return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve chat messages.');
// Init const's
const data = JSON.parse(xhr.responseText).mapping; // Get chat messages
const userMessages = [], chatGPTMessages = [], msgsToReturn = [];
// Fill [userMessages]
for (const key in data)
if (data[key].message != null && data[key].message.author.role == 'user')
userMessages.push({ id: data[key].id, msg: data[key].message });
userMessages.sort((a, b) => a.msg.create_time - b.msg.create_time); // sort in chronological order
if (parseInt(msgToGet, 10) + 1 > userMessages.length) // reject if index out of bounds
return reject('🤖 chatgpt.js >> Message/response with index ' + ( msgToGet + 1)
+ ' is out of bounds. Only ' + userMessages.length + ' messages/responses exist!');
// Fill [chatGPTMessages]
for (const userMessage of userMessages) {
let sub = [];
for (const key in data) {
if (data[key].message != null && data[key].message.author.role == 'assistant' && data[key].parent == userMessage.id) {
sub.push(data[key].message);
}
}
sub.sort((a, b) => a.create_time - b.create_time); // sort in chronological order
sub = sub.map(x => { // pull out msgs after sorting
switch(x.content.content_type) {
case 'code': return x.content.text;
case 'text': return x.content.parts[0];
default: return;
}
});
sub = sub.length === 1 ? sub[0] : sub; // convert not regenerated responses to strings
chatGPTMessages.push(sub); // array of arrays (length > 1 = regenerated responses)
}
if (sender == 'user') // Fill [msgsToReturn] with user messages
for (const userMessage in userMessages)
msgsToReturn.push(userMessages[userMessage].msg.content.parts[0]);
else if (sender == 'chatgpt') // Fill [msgsToReturn] with ChatGPT responses
for (const chatGPTMessage of chatGPTMessages)
msgsToReturn.push(msgToGet == 'latest' ? chatGPTMessages[chatGPTMessages.length - 1] : chatGPTMessage );
else { // Fill [msgsToReturn] with objects of user messages and chatgpt response(s)
let i = 0;
for (const message in userMessages) {
msgsToReturn.push({
user: userMessages[message].msg.content.parts[0],
chatgpt: msgToGet == 'latest' ? chatGPTMessages[i][chatGPTMessages[i].length - 1] : chatGPTMessages[i]
});
i++;
}
}
return resolve(msgToGet == 'all' ? msgsToReturn // if 'all' passed, return array
: msgToGet == 'latest' ? msgsToReturn[msgsToReturn.length - 1] // else if 'latest' passed, return latest
: msgsToReturn[msgToGet] ); // else return element of array
};
xhr.send();
});});};
// Return chat data
return new Promise(resolve => { chatgpt.getAccessToken().then(token => {
if (!detailsToGet.includes('msg')) getChatDetails(token, detailsToGet).then(data => {
return resolve(data); // get just the chat details
});
else getChatMsgs(token).then(messages => { return resolve(messages); }); // otherwise get specific msg's
});});
},
getChatInput: function() { return chatgpt.getChatBox().value; },
getContinueGeneratingButton: function() {
for (const svg of document.querySelectorAll('form button svg')) {
if (svg.querySelector('polygon[points*="11 19 2 12 11 5 11 19"]'))
return svg.parentNode.parentNode;
}},
getFooterDiv: function() { return document.querySelector('main form').parentNode.parentNode.nextElementSibling; },
getHeaderDiv: function() { return document.querySelector('main .sticky'); },
getLastPrompt: function() { return chatgpt.getChatData('active', 'msg', 'user', 'latest'); },
getLastResponse: function() { return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'); },
getNewChatLink: function() {
for (const navLink of document.querySelectorAll('nav a')) {
if (/(new|clear) chat/i.test(navLink.text)) {
return navLink;
}}},
getRegenerateButton: function() {
for (const mainSVG of document.querySelectorAll('main svg')) {
if (mainSVG.querySelector('path[d*="M3.07 10.876C3.623"]')) // regen icon found
return mainSVG.parentNode;
}},
getResponse: function() {
// * Returns response via DOM by index arg if OpenAI chat page is active, otherwise uses API w/ following args:
// chatToGet = index|title|id of chat to get (defaults to latest if '' unpassed)
// responseToGet = index of response to get (defaults to latest if '' unpassed)
// regenResponseToGet = index of regenerated response to get (defaults to latest if '' unpassed)
if (window.location.href.startsWith('https://chat.openai.com/c/'))
return chatgpt.getResponseFromDOM.apply(null, arguments);
else return chatgpt.getResponseFromAPI.apply(null, arguments);
},
getResponseFromAPI: function(chatToGet, responseToGet) { return chatgpt.response.getFromAPI(chatToGet, responseToGet); },
getResponseFromDOM: function(pos) { return chatgpt.response.getFromDOM(pos); },
getScrollToBottomButton: function() { return document.querySelector('button[class*="cursor"][class*="bottom"]'); },
getSendButton: function() { return document.querySelector('form button[class*="bottom"]'); },
getStopGeneratingButton: function() {
for (const svg of document.querySelectorAll('form button svg')) {
if (svg.querySelector('path[d*="2 0 0 1 2"]'))
return svg.parentNode;
}},
getUserLanguage: function() {
return navigator.languages[0] || navigator.language || navigator.browserLanguage ||
navigator.systemLanguage || navigator.userLanguage || ''; },
hideFooter: function() { chatgpt.getFooterDiv().style.display = 'none'; },
hideHeader: function() { chatgpt.getHeaderDiv().style.display = 'none'; },
history: {
isLoaded: function() {
return new Promise(resolve => {
const checkChatHistory = () => {
if (document.querySelector('nav')) resolve(true);
else setTimeout(checkChatHistory, 100);
};
checkChatHistory();
});}
},
instructions: {
// NOTE: DOM is not updated to reflect new instructions added/removed or toggle state (until session refresh)
add: function(instruction, target) {
if (!instruction) return console.error('Please provide an instruction');
if (typeof instruction !== 'string') return console.error('Instruction must be a string');
const validTargets = ['user', 'chatgpt']; // valid targets
if (!target) return console.error('Please provide a valid target!');
if (typeof target !== 'string') return console.error('Target must be a string');
target = target.toLowerCase(); // lowercase target
if (!validTargets.includes(target))
return console.error(`Invalid target ${target}. Valid targets are [${validTargets}]`);
instruction = `\n\n${instruction}`; // add 2 newlines to the new instruction
return new Promise(resolve => {
chatgpt.getAccessToken().then(async token => {
const instructionsData = await this.fetchData();
// Concatenate old instructions with new instruction
if (target == 'user') instructionsData.about_user_message += instruction;
else if (target == 'chatgpt') instructionsData.about_model_message += instruction;
await this.sendRequest('POST', token, instructionsData);
return resolve();
});
});
},
clear: function(target) {
const validTargets = ['user', 'chatgpt']; // valid targets
if (!target) return console.error('Please provide a valid target!');
if (typeof target !== 'string') return console.error('Target must be a string');
target = target.toLowerCase(); // lowercase target
if (!validTargets.includes(target))
return console.error(`Invalid target ${target}. Valid targets are [${validTargets}]`);
return new Promise(resolve => {
chatgpt.getAccessToken().then(async token => {
const instructionsData = await this.fetchData();
// Clear target's instructions
if (target == 'user') instructionsData.about_user_message = '';
else if (target == 'chatgpt') instructionsData.about_model_message = '';
await this.sendRequest('POST', token, instructionsData);
return resolve();
});});
},
fetchData: function() {
// INTERNAL METHOD
return new Promise(resolve => {
chatgpt.getAccessToken().then(async token => {
return resolve(await this.sendRequest('GET', token)); // Return API data
});});
},
sendRequest: function(method, token, body) {
// INTERNAL METHOD
// Validate args
for (let i = 0; i < arguments.length - 1; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string`);
const validMethods = ['POST', 'GET'];
method = (method || '').trim().toUpperCase();
if (!method || !validMethods.includes(method)) // reject if not valid method
return console.error(`Valid methods are ${ validMethods }`);
if (!token) return console.error('Please provide a valid access token!');
if (body && typeof body !== 'object') // reject if body is passed but not an object
return console.error(`Invalid body data type. Got ${ typeof body }, expected object`);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, endpoints.openAI.instructions, true);
// Set headers
xhr.setRequestHeader('Accept-Language', 'en-US');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
if (method == 'POST') xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
const responseData = JSON.parse(xhr.responseText);
if (xhr.status === 422)
return reject('🤖 chatgpt.js >> Character limit exceeded. Custom instructions can have a maximum length of 1500 characters.');
else if (xhr.status === 403 && responseData.detail.reason == 'content_policy')
return reject('🤖 chatgpt.js >> ' + responseData.detail.description);
else if (xhr.status !== 200)
return reject('🤖 chatgpt.js >> Request failed. Cannot contact custom instructions endpoint.');
console.info(`Custom instructions successfully contacted with method ${ method }`);
return resolve(responseData || {}); // return response data no matter what the method is
};
xhr.send(JSON.stringify(body) || ''); // if body is passed send it, else just send the request
});
},
turnOff: function() {
return new Promise(resolve => {
chatgpt.getAccessToken().then(async token => {
const instructionsData = await this.fetchData();
instructionsData.enabled = false;
await this.sendRequest('POST', token, instructionsData);
return resolve();
});
});
},
turnOn: function() {
return new Promise(resolve => {
chatgpt.getAccessToken().then(async token => {
const instructionsData = await this.fetchData();
instructionsData.enabled = true;
await this.sendRequest('POST', token, instructionsData);
return resolve();
});
});
},
toggle: function() {
return new Promise(resolve => {
this.fetchData().then(async instructionsData => {
await (instructionsData.enabled ? this.turnOff() : this.turnOn());
return resolve();
});});
}
},
isChromium: function() { return chatgpt.browser.isChromium(); },
isDarkMode: function() { return document.documentElement.classList.toString().includes('dark'); },
isFirefox: function() { return chatgpt.browser.isFirefox(); },
isFullScreen: function() { return chatgpt.browser.isFullScreen(); },
isIdle: function() {
return new Promise(resolve => {
const intervalId = setInterval(() => {
if (chatgpt.getRegenerateButton()) {
clearInterval(intervalId); resolve(true);
}}, 100);});},
isLoaded: function() {
return new Promise(resolve => {
const intervalId = setInterval(() => {
if (document.querySelector('nav button[id*="menu"]')) {
clearInterval(intervalId); setTimeout(() => { resolve(true); }, 500);
}}, 100);});},
isLightMode: function() { return document.documentElement.classList.toString().includes('light'); },
isMobileDevice: function() { return chatgpt.browser.isMobile(); },
logout: function() { window.location.href = 'https://chat.openai.com/auth/logout'; },
menu: {
elements: [],
addedEvent: false,
append: function(element, attrs = {}) {
// element = 'button' | 'dropdown' REQUIRED (no default value)
// attrs = { ... }
// attrs for 'button': 'icon' = src string, 'label' = string, 'onclick' = function
// attrs for 'dropdown': 'items' = [ { text: string, value: string }, ... ] array of objects
// where 'text' is the displayed text of the option and 'value' is the value of the option
const validElements = ['button', 'dropdown'];
if (!element || typeof element !== 'string') // element not passed or invalid type
return console.error('🤖 chatgpt.js >> Please supply a valid string element name!');
element = element.toLowerCase();
if (!validElements.includes(element)) // element not in list
return console.error(`🤖 chatgpt.js >> Invalid element! Valid elements are [${validElements}]`);
const newElement = document.createElement(
element == 'dropdown' ? 'select' :
element == 'button' ? 'a' : element
);
newElement.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now(); // add random id to the element
if (element == 'button') {
newElement.textContent = attrs?.label && typeof attrs.label == 'string'
? attrs.label
: 'chatgpt.js button';
const icon = document.createElement('img');
icon.src = attrs?.icon && typeof attrs.icon == 'string' // can also be base64 encoded image string
? attrs.icon // add icon to button element if given, else default one
: ( endpoints.assets + '/starters/chrome/extension/icons/icon128.png' );
icon.width = 18;
newElement.insertBefore(icon, newElement.firstChild);
newElement.onclick = attrs?.onclick && typeof attrs.onclick == 'function'
? attrs.onclick
: function() {};
}
else if (element == 'dropdown') {
if (!attrs?.items || // there no are options to add
!Array.isArray(attrs.items) || // it's not an array
!attrs.items.length) // the array is empty
attrs.items = [{ text: '🤖 chatgpt.js option', value: 'chatgpt.js option value' }]; // set default dropdown entry
if (!attrs.items.every(el => typeof el == 'object')) // the entries of the array are not objects
return console.error('\'items\' must be an array of objects!');
newElement.style = 'background-color: #000; width: 100%; border: none;';
attrs.items.forEach(item => {
const optionElement = document.createElement('option');
optionElement.textContent = item?.text;
optionElement.value = item?.value;
newElement.add(optionElement);
});
}
const addElementsToMenu = () => {
const optionButtons = document.querySelectorAll('a[role="menuitem"]');
let cssClasses;
for (let navLink of optionButtons)
if (navLink.textContent == 'Settings') {
cssClasses = navLink.classList;
break; }
const headlessNav = optionButtons[0].parentNode;
chatgpt.menu.elements.forEach(element => {
element.setAttribute('class', cssClasses);
if (!headlessNav.contains(element))
try { headlessNav.insertBefore(element, headlessNav.firstChild); }
catch (err) { console.error(err); }
});
};
this.elements.push(newElement);
const menuBtn = document.querySelector('nav button[id*="headless"]');
if (!this.addedEvent) { // to prevent adding more than one event
menuBtn.addEventListener('click', () => { setTimeout(addElementsToMenu, 25); });
this.addedEvent = true; }
return newElement.id; // Return the element id
},
close: function() {
const menuBtn = document.querySelector('nav [id*="menu-button"][aria-expanded="true"]');
if (menuBtn) try { menuBtn.click(); } catch (err) { return console.error(err.message); }
else { console.info('Menu already hidden!'); }
},
open: function() {
const menuBtn = document.querySelector('nav [id*="menu-button"][aria-expanded="false"]');
if (menuBtn) try { menuBtn.click(); } catch (err) { return console.error(err.message); }
else { console.info('Menu already open!'); }
}
},
minify: function() { chatgpt.code.minify(); },
notify: async function(msg, position, notifDuration, shadow) {
notifDuration = notifDuration ? +notifDuration : 1.75; // sec duration to maintain notification visibility
const fadeDuration = 0.3, // sec duration of fade-out
vpYoffset = 23, vpXoffset = 27; // px offset from viewport border
// Create/append notification div
const notificationDiv = document.createElement('div'); // make div
notificationDiv.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now();
notificationDiv.classList.add('chatgpt-notif');
notificationDiv.innerText = msg; // insert msg
document.body.append(notificationDiv); // insert into DOM
// Create/append close button
const closeBtn = document.createElement('div');
closeBtn.title = 'Dismiss'; closeBtn.classList.add('notif-close-btn');
const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
closeSVG.setAttribute('height', '8px');
closeSVG.setAttribute('viewBox', '0 0 14 14');
closeSVG.setAttribute('fill', 'none');
closeSVG.style.height = closeSVG.style.width = '8px'; // override SVG styles on non-OpenAI sites
const closeSVGpath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
closeSVGpath.setAttribute('fill-rule', 'evenodd');
closeSVGpath.setAttribute('clip-rule', 'evenodd');
closeSVGpath.setAttribute('fill', 'white');
closeSVGpath.setAttribute('d', 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z');
closeSVG.append(closeSVGpath); closeBtn.append(closeSVG); notificationDiv.append(closeBtn);
// Determine div position/quadrant
notificationDiv.isTop = !position || !/low|bottom/i.test(position);
notificationDiv.isRight = !position || !/left/i.test(position);
notificationDiv.quadrant = (notificationDiv.isTop ? 'top' : 'bottom')
+ (notificationDiv.isRight ? 'Right' : 'Left');
// Create/append/update notification style (if missing or outdated)
const thisUpdated = 20231110; // datestamp of last edit for this file's `notifStyle`
let notifStyle = document.querySelector('#chatgpt-notif-style'); // try to select existing style
if (!notifStyle || parseInt(notifStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
if (!notifStyle) { // outright missing, create/id/attr/append it first
notifStyle = document.createElement('style'); notifStyle.id = 'chatgpt-notif-style';
notifStyle.setAttribute('last-updated', thisUpdated.toString());
document.head.append(notifStyle);
}
notifStyle.innerText = ( // update prev/new style contents
'.chatgpt-notif {'
+ 'background-color: black ; padding: 10px 13px 10px 18px ; border-radius: 11px ; border: 1px solid #f5f5f7 ;' // bubble style
+ 'opacity: 0 ; position: fixed ; z-index: 9999 ; font-size: 1.8rem ; color: white ;' // visibility
+ '-webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none ;'
+ `transform: translateX(${ !notificationDiv.isRight ? '-' : '' }35px) ;` // init off-screen for transition fx
+ ( shadow ? ( 'box-shadow: -8px 13px 25px 0 ' + ( /\b(shadow|on)\b/gi.test(shadow) ? 'gray' : shadow )) : '' ) + '}'
+ '.notif-close-btn { cursor: pointer ; float: right ; position: relative ; right: -4px ; margin-left: -3px ;'
+ 'display: grid }' // top-align for non-OpenAI sites
+ '@keyframes notif-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }' // transition out keyframes
+ '15% { opacity: 0.35 ; transform: rotateX(-27deg) scale(1.05) }'
+ '45% { opacity: 0.05 ; transform: rotateX(-81deg) }'
+ '100% { opacity: 0 ; transform: rotateX(-180deg) scale(1.15) }}'
);
}
// Enqueue notification
let notifyProps = JSON.parse(localStorage.notifyProps);
notifyProps.queue[notificationDiv.quadrant].push(notificationDiv.id);
localStorage.notifyProps = JSON.stringify(notifyProps);
// Position notification (defaults to top-right)
notificationDiv.style.top = notificationDiv.isTop ? vpYoffset.toString() + 'px' : '';
notificationDiv.style.bottom = !notificationDiv.isTop ? vpYoffset.toString() + 'px' : '';
notificationDiv.style.right = notificationDiv.isRight ? vpXoffset.toString() + 'px' : '';
notificationDiv.style.left = !notificationDiv.isRight ? vpXoffset.toString() + 'px' : '';
// Reposition old notifications
const thisQuadrantQueue = notifyProps.queue[notificationDiv.quadrant];
if (thisQuadrantQueue.length > 1) {
try { // to move old notifications
for (const divId of thisQuadrantQueue.slice(0, -1)) { // exclude new div
const oldDiv = document.getElementById(divId),
offsetProp = oldDiv.style.top ? 'top' : 'bottom', // pick property to change
vOffset = +/\d+/.exec(oldDiv.style[offsetProp])[0] + 5 + oldDiv.getBoundingClientRect().height;
oldDiv.style[offsetProp] = `${ vOffset }px`; // change prop
}
} catch (err) {}
}
// Show notification
setTimeout(() => {
notificationDiv.style.opacity = chatgpt.isDarkMode() ? 0.8 : 0.67; // show msg
notificationDiv.style.transform = 'translateX(0)'; // bring from off-screen
notificationDiv.style.transition = 'transform 0.15s ease, opacity 0.15s ease';
}, 10);
// Init delay before hiding
const hideDelay = fadeDuration > notifDuration ? 0 // don't delay if fade exceeds notification duration
: notifDuration - fadeDuration; // otherwise delay for difference
// Add notification dismissal to timeout schedule + button clicks
const dismissNotif = () => {
notificationDiv.style.animation = `notif-zoom-fade-out ${ fadeDuration }s ease-out`;
clearTimeout(dismissFuncTID);
};
const dismissFuncTID = setTimeout(dismissNotif, hideDelay * 1000); // maintain visibility for `hideDelay` secs, then dismiss
closeSVG.addEventListener('click', dismissNotif, { once: true }); // add to close button clicks
// Destroy notification
notificationDiv.addEventListener('animationend', () => {
notificationDiv.remove(); // remove from DOM
notifyProps = JSON.parse(localStorage.notifyProps);
notifyProps.queue[notificationDiv.quadrant].shift(); // + memory
localStorage.notifyProps = JSON.stringify(notifyProps); // + storage
}, { once: true });
},
obfuscate: function() { chatgpt.code.obfuscate(); },
printAllFunctions: function() {
// Define colors
const colors = { // element: [light, dark]
cmdPrompt: ['#ff00ff', '#00ff00'], // pink, green
objName: ['#0611e9', '#f9ee16'], // blue, yellow
methodName: ['#005aff', '#ffa500'], // blue, orange
entryType: ['#467e06', '#b981f9'], // green, purple
srcMethod: ['#ff0000', '#00ffff'] // red, cyan
};
Object.keys(colors).forEach(element => { // populate dark scheme colors if missing
colors[element][1] = colors[element][1] ||
'#' + (Number(`0x1${ colors[element][0].replace(/^#/, '') }`) ^ 0xFFFFFF)
.toString(16).substring(1).toUpperCase(); // convert to hex
});
// Create [functionNames]
const functionNames = [];
for (const prop in this) {
if (typeof this[prop] == 'function') {
const chatgptIsParent = !Object.keys(this).find(obj => Object.keys(this[obj]).includes(this[prop].name)),
functionParent = chatgptIsParent ? 'chatgpt' : 'other';
functionNames.push([functionParent, prop]);
} else if (typeof this[prop] == 'object') {
for (const nestedProp in this[prop]) {
if (typeof this[prop][nestedProp] == 'function') {
functionNames.push([prop, nestedProp]);
}}}}
functionNames.sort((a, b) => { return a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]); });
// Print methods
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches,
baseFontStyles = 'font-family: monospace ; font-size: larger ; ';
console.log('\n%c🤖 chatgpt.js methods\n', 'font-family: sans-serif ; font-size: xxx-large ; font-weight: bold');
for (const functionName of functionNames) {
const isChatGptObjParent = /chatgpt|other/.test(functionName[0]),
rootFunction = ( functionName[0] == 'chatgpt' ? this[functionName[1]].name
: functionName[0] !== 'other' ? functionName[0] + '.' + functionName[1]
: (( Object.keys(this).find(obj => Object.keys(this[obj]).includes(this[functionName[1]].name)) + '.' )
+ this[functionName[1]].name )),
isAsync = this[functionName[1]]?.constructor.name == 'AsyncFunction';
console.log('%c>> %c' + ( isChatGptObjParent ? '' : `${ functionName[0] }.%c`) + functionName[1]
+ ' - https://chatgptjs.org/userguide/' + /(?:.*\.)?(.*)/.exec(rootFunction)[1].toLowerCase() + ( isAsync ? '-async' : '' ) + '\n%c[%c'
+ ((( functionName[0] == 'chatgpt' && functionName[1] == this[functionName[1]].name ) || // parent is chatgpt + names match or
!isChatGptObjParent) // parent is chatgpt.obj
? 'Function' : 'Alias of' ) + '%c: %c'
+ rootFunction + '%c]',
// Styles
baseFontStyles + 'font-weight: bold ; color:' + colors.cmdPrompt[+isDarkMode],
baseFontStyles + 'font-weight: bold ;'
+ 'color:' + colors[isChatGptObjParent ? 'methodName' : 'objName'][+isDarkMode],
baseFontStyles + 'font-weight: ' + ( isChatGptObjParent ? 'initial' : 'bold' ) + ';'
+ 'color:' + ( isChatGptObjParent ? 'initial' : colors.methodName[+isDarkMode] ),
baseFontStyles + 'font-weight: ' + ( isChatGptObjParent ? 'bold' : 'initial' ) + ';'
+ 'color:' + ( isChatGptObjParent ? colors.entryType[+isDarkMode] : 'initial' ),
baseFontStyles + 'font-weight: ' + ( isChatGptObjParent ? 'initial' : 'bold' ) + ';'
+ 'color:' + ( isChatGptObjParent ? 'initial' : colors.entryType[+isDarkMode] ),
baseFontStyles + ( isChatGptObjParent ? 'font-style: italic' : 'font-weight: initial' ) + ';'
+ 'color:' + ( isChatGptObjParent ? colors.srcMethod[+isDarkMode] : 'initial' ),
baseFontStyles + ( isChatGptObjParent ? 'font-weight: initial' : 'font-style: italic' ) + ';'
+ 'color:' + ( isChatGptObjParent ? 'initial' : colors.srcMethod[+isDarkMode] ),
isChatGptObjParent ? '' : ( baseFontStyles + 'color: initial ; font-weight: initial' ));
}
},
randomFloat: function() {
// * Generates a random, cryptographically secure value between 0 (inclusive) & 1 (exclusive)
const crypto = window.crypto || window.msCrypto;
return crypto?.getRandomValues(new Uint32Array(1))[0] / 0xFFFFFFFF || Math.random();
},
refactor: function() { chatgpt.code.refactor(); },
regenerate: function() { chatgpt.response.regenerate(); },
renderHTML: function(node) {
const reTags = /<([a-z\d]+)\b([^>]*)>([\s\S]*?)<\/\1>/g,
reAttributes = /(\S+)=['"]?((?:.(?!['"]?\s+\S+=|[>']))+.)['"]?/g,
nodeContent = node.childNodes;
// Preserve consecutive spaces + line breaks
if (!this.renderHTML.preWrapSet) {
node.style.whiteSpace = 'pre-wrap'; this.renderHTML.preWrapSet = true;
setTimeout(() => { this.renderHTML.preWrapSet = false; }, 100);
}
// Process child nodes
for (const childNode of nodeContent) {
// Process text node
if (childNode.nodeType == Node.TEXT_NODE) {
const text = childNode.nodeValue,
elems = Array.from(text.matchAll(reTags));
// Process 1st element to render
if (elems.length > 0) {
const elem = elems[0],
[tagContent, tagName, tagAttributes, tagText] = elem.slice(0, 4),
tagNode = document.createElement(tagName); tagNode.textContent = tagText;
// Extract/set attributes
const attributes = Array.from(tagAttributes.matchAll(reAttributes));
attributes.forEach(attribute => {
const name = attribute[1], value = attribute[2].replace(/['"]/g, '');
tagNode.setAttribute(name, value);
});
const renderedNode = this.renderHTML(tagNode); // render child elements of newly created node
// Insert newly rendered node
const beforeTextNode = document.createTextNode(text.substring(0, elem.index)),
afterTextNode = document.createTextNode(text.substring(elem.index + tagContent.length));
// Replace text node with processed nodes
node.replaceChild(beforeTextNode, childNode);
node.insertBefore(renderedNode, beforeTextNode.nextSibling);
node.insertBefore(afterTextNode, renderedNode.nextSibling);
}
// Process element nodes recursively
} else if (childNode.nodeType == Node.ELEMENT_NODE) this.renderHTML(childNode);
}
return node; // if assignment used
},
resend: async function() { chatgpt.send(await chatgpt.getChatData('latest', 'msg', 'user', 'latest')); },
response: {
get: function() {
// * Returns response via DOM by index arg if OpenAI chat page is active, otherwise uses API w/ following args:
// chatToGet = index|title|id of chat to get (defaults to latest if '' unpassed)
// responseToGet = index of response to get (defaults to latest if '' unpassed)
// regenResponseToGet = index of regenerated response to get (defaults to latest if '' unpassed)
if (window.location.href.startsWith('https://chat.openai.com/c/'))
return this.getFromDOM.apply(null, arguments);
else return this.getFromAPI.apply(null, arguments);
},
getFromAPI: function(chatToGet, responseToGet) {
// chatToGet = index|title|id of chat to get (defaults to latest if '' or unpassed)
// responseToGet = index of response to get (defaults to latest if '' or unpassed)
chatToGet = chatToGet || 'latest'; responseToGet = responseToGet || 'latest';
return chatgpt.getChatData(chatToGet, 'msg', 'chatgpt', responseToGet);
},
getFromDOM: function(pos) {
const responseDivs = document.querySelectorAll('div[data-testid*="conversation-turn"]:nth-child(odd)'),
strPos = pos.toString().toLowerCase();
let response = '';
if (responseDivs.length) {
if (/last|final/.test(strPos)) // get last response
response = responseDivs[responseDivs.length - 1].textContent;
else { // get nth response
const nthOfResponse = (
// Calculate base number
Number.isInteger(pos) ? pos : // do nothing for integers
/^\d+/.test(strPos) ? /^\d+/.exec(strPos)[0] : // extract first digits for strings w/ them
( // convert words to integers for digitless strings
/^(?:1|one|fir)(?:st)?$/.test(strPos) ? 1
: /^(?:2|tw(?:o|en|el(?:ve|f))|seco)(?:nd|t[yi])?(?:e?th)?$/.test(strPos) ? 2
: /^(?:3|th(?:ree|ir?))(?:rd|teen|t[yi])?(?:e?th)?$/.test(strPos) ? 3
: /^(?:4|fou?r)(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 4
: /^(?:5|fi(?:ve|f))(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 5
: /^(?:6|six)(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 6
: /^(?:7|seven)(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 7
: /^(?:8|eight?)(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 8
: /^(?:9|nine?)(?:teen|t[yi])?(?:e?th)?$/.test(strPos) ? 9
: /^(?:10|ten)(?:th)?$/.test(strPos) ? 10 : 1 )
// Transform base number if suffixed
* ( /(ty|ieth)$/.test(strPos) ? 10 : 1 ) // x 10 if -ty/ieth
+ ( /teen(th)?$/.test(strPos) ? 10 : 0 ) // + 10 if -teen/teenth
);
response = responseDivs[nthOfResponse - 1].textContent;
}
response = response.replace(/^ChatGPTChatGPT/, ''); // strip sender name
}
return response;
},
getLast: function() { return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest'); },
regenerate: function() {
const regenBtn = chatgpt.getRegenerateButton();
if (regenBtn) regenBtn.click();
else console.error('Regenerate button not found!');
},
stopGenerating: function() {
for (const svg of document.querySelectorAll('form button svg')) {
if (svg.querySelector('path[d*="2 0 0 1 2"]')) {
svg.parentNode.click(); return;
}}}
},
reviewCode: function() { chatgpt.code.review(); },
scrollToBottom: function() {
const scrollBtn = chatgpt.getScrollToBottomButton();
if (scrollBtn) scrollBtn.click();
else console.error('Scroll button not found!');
},
send: function(msg, method='') {
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string!`);
const textArea = document.querySelector('form textarea'),
sendButton = document.querySelector('form button[class*="bottom"]');
textArea.value = msg;
textArea.dispatchEvent(new Event('input', { bubbles: true })); // enable send button
const delaySend = setInterval(() => {
if (!sendButton?.hasAttribute('disabled')) { // send msg
method.toLowerCase() == 'click' || chatgpt.browser.isMobile() ? sendButton.click()
: textArea.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 13, bubbles: true }));
clearInterval(delaySend);
}
}, 25);
},
sendInNewChat: function(msg) {
if (typeof msg !== 'string') return console.error('Message must be a string!');
for (const navLink of document.querySelectorAll('nav a')) {
if (/(new|clear) chat/i.test(navLink.text)) {
navLink.click(); break;
}} setTimeout(() => { chatgpt.send(msg); }, 500);
},
settings: {
scheme: {
isDark: function() { return document.documentElement.classList.contains('dark'); },
isLight: function() { return document.documentElement.classList.contains('light'); },
set: function(value) {
// Validate value
const validValues = ['dark', 'light', 'system'];
if (!value) return console.error('Please specify a scheme value!');
if (!validValues.includes(value)) return console.error(`Invalid scheme value. Valid values are [${ validValues }]`);
// Determine scheme to set
let schemeToSet = value;
if (value == 'system') schemeToSet = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
localStorage.setItem('theme', value);
console.info(`Scheme set to ${ value.toUpperCase() }.`);
// Toggle scheme if necessary
if (!document.documentElement.classList.contains(schemeToSet)) this.toggle();
},
toggle: function() {
const [schemeToRemove, schemeToAdd] = this.isDark() ? ['dark', 'light'] : ['light', 'dark'];
document.documentElement.classList.replace(schemeToRemove, schemeToAdd);
document.documentElement.style.colorScheme = schemeToAdd;
localStorage.setItem('theme', schemeToAdd);
}
}
},
sentiment: async function(text, entity) {
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string.`);
chatgpt.send('What is the sentiment of the following text'
+ ( entity ? ` towards the entity ${ entity },` : '')
+ ' from strongly negative to strongly positive?\n\n' + text );
console.info('Analyzing sentiment...');
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
setScheme: function(value) { chatgpt.settings.scheme.set(value); },
shareChat: function(chatToGet, method = 'clipboard') {
// chatToGet = index|title|id of chat to get (defaults to latest if '' or unpassed)
// method = [ 'alert'|'clipboard' ] (defaults to 'clipboard' if '' or unpassed)
const validMethods = ['alert', 'notify', 'notification', 'clipboard', 'copy'];
if (!validMethods.includes(method)) return console.error(
'Invalid method \'' + method + '\' passed. Valid methods are [' + validMethods + '].');
const getChatNode = token => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
chatgpt.getChatData(chatToGet).then(chat => {
xhr.open('GET', `${ endpoints.openAI.chat }/${ chat.id }`, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200)
return reject('🤖 chatgpt.js >> Request failed. Cannot retrieve chat node.');
return resolve(JSON.parse(xhr.responseText).current_node); // chat messages until now
};
xhr.send();
});});};
const makeChatToShare = (token, node) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
chatgpt.getChatData(chatToGet).then(chat => {
xhr.open('POST', endpoints.openAI.share_create, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200)
return reject('🤖 chatgpt.js >> Request failed. Cannot initialize share chat.');
return resolve(JSON.parse(xhr.responseText)); // return untouched data
};
xhr.send(JSON.stringify({ // request body
current_node_id: node, // by getChatNode
conversation_id: chat.id, // current chat id
is_anonymous: true // show user name in the conversation or not
}));
});});};
const confirmShareChat = (token, data) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PATCH', `${ endpoints.openAI.share }/${ data.share_id }`, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = () => {
if (xhr.status !== 200)
return reject('🤖 chatgpt.js >> Request failed. Cannot share chat.');
console.info(`Chat shared at '${ data.share_url }'`);
return resolve(); // the response has nothing useful
};
xhr.send(JSON.stringify({ // request body
share_id: data.share_id,
highlighted_message_id: data.highlighted_message_id,
title: data.title,
is_public: true, // must be true or it'll cause a 404 error
is_visible: data.is_visible,
is_anonymous: data.is_anonymous
}));
});};
return new Promise(resolve => {
chatgpt.getAccessToken().then(token => { // get access token
getChatNode(token).then(node => { // get chat node
makeChatToShare(token, node).then(data => {
confirmShareChat(token, data).then(() => {
if (['copy', 'clipboard'].includes(method)) navigator.clipboard.writeText(data.share_url);
else chatgpt.alert('🚀 Share link created!',
'"' + data.title + '" is available at: ' + data.share_url + '',
[ function openLink() { window.open(data.share_url, '_blank', 'noopener'); },
function copyLink() { navigator.clipboard.writeText(data.share_url); }]);
resolve(data.share_url);
});});});});});
},
showFooter: function() { chatgpt.getFooterDiv().style.display = 'revert'; },
showHeader: function() { chatgpt.getHeaderDiv().style.display = 'flex'; },
sidebar: {
elements: [], observer: {},
activateObserver: function() {
const chatHistoryNav = document.querySelector('nav'),
firstButton = chatHistoryNav.querySelector('a');
if (chatgpt.history.isOff()) // Hide enable history spam div
try { firstButton.parentNode.nextElementSibling.style.display = 'none'; } catch (err) {}
// Stop the previous observer to preserve resources
if (this.observer instanceof MutationObserver)
try { this.observer.disconnect(); } catch (e) {}
if (!this.elements.length) return console.error('🤖 chatgpt.js >> No elements to append!');
let cssClasses;
// Grab CSS from original website elements
for (let navLink of document.querySelectorAll('nav a')) {
if (/.*chat/.exec(navLink.text)[0]) {
cssClasses = navLink.classList;
navLink.parentNode.style.margin = '2px 0'; // add v-margins to ensure consistency across all inserted buttons
break;
}
}
// Apply CSS to make the added elements look like they belong to the website
this.elements.forEach(element => {
element.setAttribute('class', cssClasses);
element.style.maxHeight = element.style.minHeight = '44px'; // Fix the height of the element
element.style.margin = '2px 0';
});
const navBar = document.querySelector('nav');
// Create MutationObserver instance
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if ((mutation.type == 'childList' && mutation.addedNodes.length) ||
(mutation.type == 'attributes' && mutation.attributeName == 'data-chatgptjs')) // check for trigger
// Try to insert each element...
this.elements.forEach(element => {
// ...if it's not already present...
if (!navBar.contains(element))
try {
// ...at the top of the sidebar
navBar.insertBefore(element, navBar.querySelector('a').parentNode);
} catch (err) { console.error(err); }
});
});
});
this.observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
},
append: function(element, attrs = {}) {
// element = 'button' | 'dropdown' REQUIRED (no default value)
// attrs = { ... }
// attrs for 'button': 'icon' = src string, 'label' = string, 'onclick' = function
// attrs for 'dropdown': 'items' = [ { text: string, value: string }, ... ] array of objects
// where 'text' is the displayed text of the option and 'value' is the value of the option
const validElements = ['button', 'dropdown'];
if (!element || typeof element !== 'string') // Element not passed or invalid type
return console.error('🤖 chatgpt.js >> Please supply a valid string element name!');
element = element.toLowerCase();
if (!validElements.includes(element)) // Element not in list
return console.error(`🤖 chatgpt.js >> Invalid element! Valid elements are [${validElements}]`);
const newElement = document.createElement(element == 'dropdown' ? 'select' : element);
newElement.id = Math.floor(chatgpt.randomFloat() * 1000000) + Date.now(); // Add random id to the element
if (element == 'button') {
newElement.textContent = attrs?.label && typeof attrs.label == 'string'
? attrs.label
: 'chatgpt.js button';
const icon = document.createElement('img');
icon.src = attrs?.icon && typeof attrs.icon == 'string' // Can also be base64 encoded image string
? attrs.icon // Add icon to button element if given, else default one
: ( endpoints.assets + '/starters/chrome/extension/icons/icon128.png' );
icon.width = 18;
newElement.insertBefore(icon, newElement.firstChild);
newElement.onclick = attrs?.onclick && typeof attrs.onclick == 'function'
? attrs.onclick
: function() {};
}
else if (element == 'dropdown') {
if (!attrs?.items || // There no are options to add
!Array.isArray(attrs.items) || // It's not an array
!attrs.items.length) // The array is empty
attrs.items = [{ text: '🤖 chatgpt.js option', value: 'chatgpt.js option value' }]; // Set default dropdown entry
if (!attrs.items.every(el => typeof el == 'object')) // The entries of the array are not objects
return console.error('\'items\' must be an array of objects!');
attrs.items.forEach(item => {
const optionElement = document.createElement('option');
optionElement.textContent = item?.text;
optionElement.value = item?.value;
newElement.add(optionElement);
});
}
// Fix for blank background on dropdown elements
if (element == 'dropdown') newElement.style.backgroundColor = 'var(--gray-900, rgb(32, 33, 35))';
this.elements.push(newElement);
this.activateObserver();
document.body.setAttribute('data-chatgptjs', 'observer-trigger'); // add attribute to trigger the observer
return newElement.id; // Return the element id
},
hide: function() { this.isOn() ? this.toggle() : console.info('Sidebar already hidden!'); },
show: function() { this.isOff() ? this.toggle() : console.info('Sidebar already shown!'); },
isOff: function() { return !this.isOn(); },
isOn: function() {
return chatgpt.browser.isMobile() ?
document.documentElement.style.overflow == 'hidden'
: document.querySelector('#__next > div > div').style.visibility != 'hidden';
},
toggle: function() {
const isMobileDevice = chatgpt.browser.isMobile(),
navBtnSelector = isMobileDevice ? '#__next button' : 'main button' ,
isToggleBtn = isMobileDevice ? () => true // since 1st one is toggle
: btn => Array.from(btn.querySelectorAll('*'))
.some(child => child.style.transform.includes('translateY'));
for (const btn of document.querySelectorAll(navBtnSelector))
if (isToggleBtn(btn)) { btn.click(); return; }
}
},
startNewChat: function() {
for (const navLink of document.querySelectorAll('nav a')) {
if (/(new|clear) chat/i.test(navLink.text)) {
navLink.click(); return;
}}},
stop: function() { this.response.stopGenerating(); },
suggest: async function(ideaType, details) {
if (!ideaType) return console.error('ideaType (1st argument) not supplied'
+ '(e.g. \'gifts\', \'names\', \'recipes\', etc.)');
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string.`);
chatgpt.send('Suggest some names. ' + ( details || '' ));
console.info(`Creating ${ ideaType }...`);
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
speak: function(msg, options = {}) {
// Usage example: chatgpt.speak(await chatgpt.getLastResponse(), { voice: 1, pitch: 2, speed: 3 })
// options.voice = index of voices available on user device
// options.pitch = float for pitch of speech from 0 to 2
// options.speed = float for rate of speech from 0.1 to 10
const { voice = 2, pitch = 2, speed = 1.1 } = options;
// Validate args
if (typeof msg !== 'string') return console.error('Message must be a string!');
for (let key in options) {
const value = options[key];
if (typeof value !== 'number' && !/^\d+$/.test(value))
return console.error(`Invalid ${ key } index '${ value }'. Must be a number!`);
}
try { // to speak msg using {options}
const voices = speechSynthesis.getVoices(),
utterance = new SpeechSynthesisUtterance();
utterance.text = msg;
utterance.voice = voices[voice];
utterance.pitch = pitch;
utterance.rate = speed;
speechSynthesis.speak(utterance);
} catch (err) { console.error( err); }
},
summarize: async function(text) {
if (!text) return console.error('Text (1st) argument not supplied. Pass some text!');
if (typeof text !== 'string') return console.error('Text argument must be a string!');
chatgpt.send('Summarize the following text:\n\n' + text);
console.info('Summarizing text...');
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
toggleScheme: function() { chatgpt.settings.scheme.toggle(); },
translate: async function(text, outputLang) {
if (!text) return console.error('Text (1st) argument not supplied. Pass some text!');
if (!outputLang) return console.error('outputLang (2nd) argument not supplied. Pass a language!');
for (let i = 0; i < arguments.length; i++) if (typeof arguments[i] !== 'string')
return console.error(`Argument ${ i + 1 } must be a string!`);
chatgpt.send('Translate the following text to ' + outputLang
+ '. Only reply with the translation.\n\n' + text);
console.info('Translating text...');
await chatgpt.isIdle();
return chatgpt.getChatData('active', 'msg', 'chatgpt', 'latest');
},
unminify: function() { chatgpt.code.unminify(); },
uuidv4: function() {
let d = new Date().getTime(); // get current timestamp in ms (to ensure UUID uniqueness)
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = ( // generate random nibble
( d + (window.crypto.getRandomValues(new Uint32Array(1))[0] / (Math.pow(2, 32) - 1))*16)%16 | 0 );
d = Math.floor(d/16); // correspond each UUID digit to unique 4-bit chunks of timestamp
return ( c == 'x' ? r : (r&0x3|0x8) ).toString(16); // generate random hexadecimal digit
});
return uuid;
},
writeCode: function() { chatgpt.code.write(); }
};
chatgpt.scheme = { ...chatgpt.settings.scheme }; // copy `chatgpt.settings.scheme` methods into `chatgpt.scheme`
// Create chatgpt.[actions]Button(identifier) functions
const buttonActions = ['click', 'get'], targetTypes = [ 'button', 'link', 'div', 'response' ];
for (const buttonAction of buttonActions) {
chatgpt[buttonAction + 'Button'] = function handleButton(buttonIdentifier) {
const button = /^[.#]/.test(buttonIdentifier) ? document.querySelector(buttonIdentifier)
: /send/i.test(buttonIdentifier) ? document.querySelector('form button[class*="bottom"]')
: /scroll/i.test(buttonIdentifier) ? document.querySelector('button[class*="cursor"]')
: (function() { // get via text content
for (const button of document.querySelectorAll('button')) { // try buttons
if (button.textContent.toLowerCase().includes(buttonIdentifier.toLowerCase())) {
return button; }}
for (const navLink of document.querySelectorAll('nav a')) { // try nav links if no button
if (navLink.textContent.toLowerCase().includes(buttonIdentifier.toLowerCase())) {
return navLink; }}})();
if (buttonAction == 'click') { button.click(); } else { return button; }
};
}
// Create alias functions
const funcAliases = [
['actAs', 'actas', 'act', 'become', 'persona', 'premadePrompt', 'preMadePrompt', 'prePrompt', 'preprompt', 'roleplay', 'rolePlay', 'rp'],
['activateAutoRefresh', 'activateAutoRefresher', 'activateRefresher', 'activateSessionRefresher',
'autoRefresh', 'autoRefresher', 'autoRefreshSession', 'refresher', 'sessionRefresher'],
['deactivateAutoRefresh', 'deactivateAutoRefresher', 'deactivateRefresher', 'deactivateSessionRefresher'],
['detectLanguage', 'getLanguage'],
['executeCode', 'codeExecute'],
['exportChat', 'chatExport', 'export'],
['getFooterDiv', 'getFooter'],
['getHeaderDiv', 'getHeader'],
['getLastPrompt', 'getLastQuery', 'getMyLastMsg', 'getMyLastQuery'],
['getScrollToBottomButton', 'getScrollButton'],
['getTextarea', 'getTextArea', 'getChatbox', 'getChatBox'],
['isFullScreen', 'isFullscreen'],
['logOut', 'logout', 'logOff', 'logoff', 'signOut', 'signout', 'signOff', 'signoff'],
['minify', 'codeMinify', 'minifyCode'],
['new', 'newChat', 'startNewChat'],
['obfuscate', 'codeObfuscate', 'obfuscateCode'],
['printAllFunctions', 'showAllFunctions'],
['refactor', 'codeRefactor', 'refactorCode'],
['refreshReply', 'regenerate', 'regenerateReply'],
['refreshSession', 'sessionRefresh'],
['renderHTML', 'renderHtml', 'renderLinks', 'renderTags'],
['reviewCode', 'codeReview'],
['send', 'sendChat', 'sendMsg'],
['sendInNewChat', 'sendNewChat'],
['sentiment', 'analyzeSentiment', 'sentimentAnalysis'],
['stop', 'stopGenerating'],
['suggest', 'suggestion', 'recommend'],
['toggleAutoRefresh', 'toggleAutoRefresher', 'toggleRefresher', 'toggleSessionRefresher'],
['toggleScheme', 'toggleMode'],
['translate', 'translation', 'translator'],
['unminify', 'unminifyCode', 'codeUnminify'],
['writeCode', 'codeWrite']
];
const synonyms = [
['account', 'acct'],
['activate', 'turnOn'],
['analyze', 'check', 'evaluate', 'review'],
['ask', 'send', 'submit'],
['button', 'btn'],
['chat', 'conversation', 'convo'],
['data', 'details'],
['deactivate', 'deActivate', 'turnOff'],
['execute', 'interpret', 'interpreter', 'run'],
['generating', 'generation'],
['minify', 'uglify'],
['refactor', 'rewrite'],
['regenerate', 'regen'],
['render', 'parse'],
['reply', 'response'],
['sentiment', 'attitude', 'emotion', 'feeling', 'opinion', 'perception'],
['speak', 'say', 'speech', 'talk', 'tts'],
['summarize', 'tldr'],
['unminify', 'beautify', 'prettify', 'prettyPrint']
];
const camelCaser = (words) => {
return words.map((word, index) => index === 0 || word == 's' ? word : word.charAt(0).toUpperCase() + word.slice(1)).join(''); };
for (const prop in chatgpt) {
// Create new function for each alias
for (const subAliases of funcAliases) {
if (subAliases.includes(prop)) {
if (subAliases.some(element => element.includes('.'))) {
const nestedFunction = subAliases.find(element => element.includes('.')).split('.')[1];
for (const nestAlias of subAliases) {
if (/^(\w+)/.exec(nestAlias)[1] !== prop) { // don't alias og function
chatgpt[nestAlias] = chatgpt[prop][nestedFunction]; // make new function, reference og one
}}} else { // alias direct functions
for (const dirAlias of subAliases) {
if (dirAlias !== prop) { // don't alias og function
chatgpt[dirAlias] = chatgpt[prop]; // make new function, reference og one
}}}
}}
do { // create new function per synonym per word per function
var newFunctionsCreated = false;
for (const funcName in chatgpt) {
if (typeof chatgpt[funcName] == 'function') {
const funcWords = funcName.split(/(?=[A-Zs])/); // split function name into constituent words
for (const funcWord of funcWords) {
const synonymValues = [].concat(...synonyms // flatten into single array w/ word's synonyms
.filter(arr => arr.includes(funcWord.toLowerCase())) // filter in relevant synonym sub-arrays
.map(arr => arr.filter(synonym => synonym !== funcWord.toLowerCase()))); // filter out matching word
for (const synonym of synonymValues) { // create function per synonym
const newFuncName = camelCaser(funcWords.map(word => (word == funcWord ? synonym : word)));
if (!chatgpt[newFuncName]) { // don't alias existing functions
chatgpt[newFuncName] = chatgpt[funcName]; // make new function, reference og one
newFunctionsCreated = true;
}}}}}} while (newFunctionsCreated); // loop over new functions to encompass all variations
}
// Prefix console logs w/ '🤖 chatgpt.js >> '
const consolePrefix = '🤖 chatgpt.js >> ', ogError = console.error, ogInfo = console.info;
console.error = (...args) => {
if (!args[0].startsWith(consolePrefix)) ogError(consolePrefix + args[0], ...args.slice(1));
else ogError(...args);
};
console.info = (msg) => {
if (!msg.startsWith(consolePrefix)) ogInfo(consolePrefix + msg);
else ogInfo(msg);
};
// Export chatgpt object
try { window.chatgpt = chatgpt; } catch (err) {} // for Greasemonkey
try { module.exports = chatgpt; } catch (err) {} // for CommonJS