// ==UserScript==
// @name ChatGPT Sidebar GPT Reorder
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Reorder GPTs in ChatGPT sidebar with a custom sort list.
// @author @MartianInGreen
// @match https://*.chatgpt.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CUSTOM_SORT_KEY = 'customGPTSort';
const REORDER_BUTTON_ID = 'reorder-gpts-button';
const MODAL_ID = 'gpt-sort-modal-overlay';
// Utility function to wait for an element based on a predicate function
function waitForElement(predicate, timeout = 20000) {
return new Promise((resolve, reject) => {
if (predicate()) {
return resolve(predicate());
}
const observer = new MutationObserver(() => {
if (predicate()) {
resolve(predicate());
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
reject(new Error('Element not found within timeout.'));
}, timeout);
});
}
// Function to identify the target button (adjust if necessary)
function identifyTargetButton() {
// Example: Find a button with specific aria-label or class
const buttons = Array.from(document.querySelectorAll('button'));
for (let btn of buttons) {
if (btn.textContent.toLowerCase().includes('more')) {
return btn;
}
// Add other identification logic if needed
}
// Fallback
return null;
}
// Function to get all GPT elements (Update selector based on actual DOM)
function getAllGPTs() {
// Updated selector based on provided HTML
const gpts = Array.from(document.querySelectorAll('div[tabindex="0"] > a.group.flex.h-10'));
console.log('Found GPTs:', gpts);
return gpts;
}
// Function to get the GPT container (Update selector based on actual DOM)
function getGPTContainer() {
const gpts = getAllGPTs();
if (gpts.length === 0) return null;
const container = gpts[0].parentElement;
console.log('GPT Container:', container);
return container;
}
// Function to save custom sort to localStorage
function saveCustomSort(sortList) {
localStorage.setItem(CUSTOM_SORT_KEY, JSON.stringify(sortList));
}
// Function to load custom sort from localStorage
function loadCustomSort() {
const data = localStorage.getItem(CUSTOM_SORT_KEY);
return data ? JSON.parse(data) : null;
}
// Function to initialize custom sort
function initializeCustomSort() {
const gpts = getAllGPTs();
const sortList = gpts.map(gpt => {
const nameElement = gpt.querySelector('div.text-sm.text-token-text-primary'); // Update selector if necessary
return {
name: nameElement ? nameElement.textContent.trim() : '',
url: gpt.getAttribute('href'),
icon: gpt.querySelector('img') ? gpt.querySelector('img').src : ''
};
});
saveCustomSort(sortList);
return sortList;
}
// Function to reorder GPTs based on sort list
function reorderGPTs(sortList) {
const gptContainer = getGPTContainer();
if (!gptContainer) {
console.warn('GPT container not found. Cannot reorder GPTs.');
return;
}
const gpts = getAllGPTs();
const gptMap = {};
gpts.forEach(gpt => {
const nameElement = gpt.querySelector('div.text-sm.text-token-text-primary'); // Update selector if necessary
const name = nameElement ? nameElement.textContent.trim() : '';
if (name) {
gptMap[name] = gpt;
}
});
// Clear current GPTs
gpts.forEach(gpt => {
if (gptContainer.contains(gpt)) {
gptContainer.removeChild(gpt);
} else {
console.warn('GPT is not a child of the container:', gpt);
}
});
// Append GPTs based on sortList
sortList.forEach(item => {
if (gptMap[item.name]) {
gptContainer.appendChild(gptMap[item.name]);
delete gptMap[item.name];
}
});
// Append any remaining GPTs (new ones) to the end
Object.values(gptMap).forEach(gpt => {
gptContainer.appendChild(gpt);
});
}
// Function to create the Sort UI Modal
function createSortModal(sortList) {
// If modal already exists, remove it
const existingModal = document.getElementById(MODAL_ID);
if (existingModal) {
existingModal.remove();
}
// Create modal overlay
const modalOverlay = document.createElement('div');
modalOverlay.id = MODAL_ID;
Object.assign(modalOverlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '10000'
});
// Create modal content
const modalContent = document.createElement('div');
Object.assign(modalContent.style, {
backgroundColor: '#fff',
padding: '20px',
borderRadius: '8px',
width: '400px',
maxHeight: '80%',
overflowY: 'auto',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
color: '#000' // Set text color to black
});
// Modal header
const header = document.createElement('h2');
header.textContent = 'Reorder GPTs';
Object.assign(header.style, {
marginTop: '0',
marginBottom: '10px',
textAlign: 'center',
color: '#000' // Ensure header text is black
});
modalContent.appendChild(header);
// Instructions
const instructions = document.createElement('p');
instructions.textContent = 'Drag and drop the GPTs to reorder them. Click "Save" to apply changes or "Refresh" to revert.';
Object.assign(instructions.style, {
fontSize: '14px',
marginBottom: '10px',
textAlign: 'center',
color: '#000' // Set text color to black
});
modalContent.appendChild(instructions);
// Create list container
const list = document.createElement('ul');
list.id = 'gpt-sort-list';
Object.assign(list.style, {
listStyleType: 'none',
padding: '0',
marginBottom: '20px'
});
sortList.forEach(item => {
const listItem = document.createElement('li');
listItem.textContent = item.name;
listItem.setAttribute('data-name', item.name);
Object.assign(listItem.style, {
padding: '8px',
margin: '4px 0',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
cursor: 'grab',
color: '#000' // Set list item text color to black
});
list.appendChild(listItem);
});
modalContent.appendChild(list);
// Buttons container
const buttonsContainer = document.createElement('div');
Object.assign(buttonsContainer.style, {
display: 'flex',
justifyContent: 'space-between'
});
// Export button
const exportButton = document.createElement('button');
exportButton.textContent = 'Export';
Object.assign(exportButton.style, {
padding: '8px 16px',
backgroundColor: '#555555',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: 'auto'
});
buttonsContainer.appendChild(exportButton);
// Import button
const importButton = document.createElement('button');
importButton.textContent = 'Import';
Object.assign(importButton.style, {
padding: '8px 16px',
backgroundColor: '#555555',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px'
});
buttonsContainer.appendChild(importButton);
// Refresh button
const refreshButton = document.createElement('button');
refreshButton.textContent = 'Refresh';
Object.assign(refreshButton.style, {
padding: '8px 16px',
backgroundColor: '#ffa500', // Orange color for distinction
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px'
});
buttonsContainer.appendChild(refreshButton);
// Save button
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
Object.assign(saveButton.style, {
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px'
});
buttonsContainer.appendChild(saveButton);
// Cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
Object.assign(cancelButton.style, {
padding: '8px 16px',
backgroundColor: '#f44336',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: '10px'
});
buttonsContainer.appendChild(cancelButton);
modalContent.appendChild(buttonsContainer);
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
// Make the list sortable using HTML5 Drag and Drop
makeListSortable(list);
// Event listeners
cancelButton.addEventListener('click', () => {
modalOverlay.remove();
});
saveButton.addEventListener('click', () => {
const newSortList = [];
const items = list.querySelectorAll('li');
items.forEach(li => {
const name = li.getAttribute('data-name');
const found = sortList.find(item => item.name === name);
if (found) newSortList.push(found);
});
// Update the sort list with any new GPTs
const allGPTs = getAllGPTs();
allGPTs.forEach(gpt => {
const nameElement = gpt.querySelector('div.text-sm.text-token-text-primary'); // Update selector if necessary
const name = nameElement ? nameElement.textContent.trim() : '';
if (!newSortList.find(item => item.name === name)) {
newSortList.push({
name: name,
url: gpt.getAttribute('href'),
icon: gpt.querySelector('img') ? gpt.querySelector('img').src : ''
});
}
});
saveCustomSort(newSortList);
reorderGPTs(newSortList);
modalOverlay.remove();
});
// Export functionality
exportButton.addEventListener('click', () => {
const dataStr = JSON.stringify(sortList, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gpt_sort_order.json';
a.click();
URL.revokeObjectURL(url);
});
// Import functionality
importButton.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = event => {
try {
const importedSortList = JSON.parse(event.target.result);
if (Array.isArray(importedSortList)) {
saveCustomSort(importedSortList);
reorderGPTs(importedSortList);
modalOverlay.remove();
alert('Sort order imported successfully.');
} else {
throw new Error('Invalid sort list format.');
}
} catch (err) {
alert('Failed to import sort order: ' + err.message);
}
};
reader.readAsText(file);
};
input.click();
});
// Refresh functionality
refreshButton.addEventListener('click', () => {
const currentSort = loadCustomSort();
if (currentSort) {
reorderGPTs(currentSort);
alert('GPT list refreshed based on the current sort order.');
} else {
alert('No custom sort list found.');
}
});
}
// Function to make a list sortable using Drag and Drop
function makeListSortable(list) {
let draggedItem = null;
list.addEventListener('dragstart', (e) => {
if (e.target.tagName.toLowerCase() === 'li') {
draggedItem = e.target;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.innerHTML);
e.target.style.opacity = '0.5';
}
});
list.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target;
if (target && target !== draggedItem && target.nodeName === 'LI') {
const rect = target.getBoundingClientRect();
const next = (e.clientY - rect.top) > (rect.height / 2);
list.insertBefore(draggedItem, next ? target.nextSibling : target);
}
});
list.addEventListener('dragend', (e) => {
if (draggedItem) {
draggedItem.style.opacity = '1';
draggedItem = null;
}
});
// Make list items draggable
const items = list.querySelectorAll('li');
items.forEach(item => {
item.setAttribute('draggable', 'true');
});
}
// Function to create and inject the "Reorder GPTs" button
function injectSortButton() {
const existingButton = document.getElementById(REORDER_BUTTON_ID);
if (existingButton) return; // Prevent duplicate buttons
const button = document.createElement('button');
button.id = REORDER_BUTTON_ID;
button.textContent = 'Reorder GPTs';
Object.assign(button.style, {
padding: '8px 16px',
margin: '10px',
backgroundColor: '#008CBA',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
button.addEventListener('click', () => {
const sortList = loadCustomSort();
if (sortList) {
createSortModal(sortList);
} else {
alert('No custom sort list found.');
}
});
// Insert the button above the GPT list
const gptContainer = getGPTContainer();
if (gptContainer) {
// Insert the button at the top of the container
gptContainer.insertBefore(button, gptContainer.firstChild);
console.log('"Reorder GPTs" button injected.');
// Attach event delegation for GPT clicks
//attachGPTClickListener(gptContainer);
} else {
console.warn('GPT container not found. Cannot inject the "Reorder GPTs" button.');
}
}
// Function to attach a click listener to GPT items to reapply sort when a GPT is clicked
async function attachGPTClickListener(gptContainer) {
gptContainer.addEventListener('click', function(event) {
reloadAll(event);
});
}
async function reloadAll(event) {
setTimeout(function() {
const gptItem = event.target.closest('a.group.flex.h-10');
if (gptItem) {
console.log('GPT item clicked:', gptItem);
// Wait for the sidebar button to load
const buttons = Array.from(document.querySelectorAll('button'));
let sidebarButton = null;
for (let btn of buttons) {
if (btn.textContent.toLowerCase().includes('more')) {
sidebarButton = btn;
}
// Add other identification logic if needed
}
if (!sidebarButton) {
throw new Error('Sidebar button could not be identified.');
}
console.log('Sidebar button found:', sidebarButton);
// Click the button to load all GPTs
sidebarButton.click();
console.log('Sidebar button clicked to load all GPTs.');
// Wait for GPTs to load
waitForElement(() => getAllGPTs().length > 0, 20000);
console.log('GPTs loaded.');
// Initialize or load custom sort
let sortList = loadCustomSort();
if (!sortList) {
sortList = initializeCustomSort();
console.log('Custom sort initialized with default order.');
} else {
console.log('Custom sort loaded from localStorage.');
}
// Reorder GPTs
reorderGPTs(sortList);
console.log('GPTs reordered based on custom sort.');
// Inject the "Reorder GPTs" button
injectSortButton();
}
}, 2000);
}
// Attach event listener for 'popstate' to handle URL changes caused by browser navigation
window.addEventListener('popstate', function(event) {
// Your code here
console.log('URL changed:', window.location.href);
main();
});
let url = window.location.href;
// Optional: Use a MutationObserver to detect URL changes not caused by popstate (e.g., SPA routing)
const observer = new MutationObserver(() => {
if (window.location.href != url){
url = window.location.href;
main();
}
//main();
});
// Observe changes to the document's title (as an example, adjust if necessary)
observer.observe(document, { subtree: true, childList: true });
// Main function to orchestrate the script
async function main() {
try {
console.log('ChatGPT Sidebar GPT Reorder script started.');
// Wait for the sidebar button to load
const sidebarButton = await waitForElement(identifyTargetButton, 20000);
if (!sidebarButton) {
throw new Error('Sidebar button could not be identified.');
}
console.log('Sidebar button found:', sidebarButton);
// Click the button to load all GPTs
sidebarButton.click();
console.log('Sidebar button clicked to load all GPTs.');
// Wait for GPTs to load
await waitForElement(() => getAllGPTs().length > 0, 20000);
console.log('GPTs loaded.');
// Initialize or load custom sort
let sortList = loadCustomSort();
if (!sortList) {
sortList = initializeCustomSort();
console.log('Custom sort initialized with default order.');
} else {
console.log('Custom sort loaded from localStorage.');
}
// Reorder GPTs
reorderGPTs(sortList);
console.log('GPTs reordered based on custom sort.');
// Inject the "Reorder GPTs" button
injectSortButton();
// Optional: Use a MutationObserver to detect URL changes not caused by popstate (e.g., SPA routing)
//const observer = new MutationObserver(() => {
// onURLChange();
//});
// Observe changes to the document's title (as an example, adjust if necessary)
//observer.observe(document, { subtree: true, childList: true });
} catch (error) {
console.error('ChatGPT Sidebar GPT Reorder script error:', error);
}
}
// Run the main function after DOM is fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();