// ==UserScript==
// @name ChatGPT Sidebar GPT Reorder
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Reorder GPTs in ChatGPT sidebar with a custom sort list and add "See less" functionality.
// @author @MartianInGreen
// @match https://*.chatgpt.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CUSTOM_SORT_KEY = 'customGPTSort';
const HIDDEN_GPTS_KEY = 'hiddenGPTs';
const REORDER_BUTTON_ID = 'reorder-gpts-button';
const SEE_LESS_BUTTON_ID = 'see-less-button';
const SEE_MORE_BUTTON_ID = 'see-more-button';
const MODAL_ID = 'gpt-sort-modal-overlay';
const MAX_VISIBLE_GPTS = 5; // Number of GPTs to show when "See less" is activated
// Add CSS for hidden GPTs
const style = document.createElement('style');
style.innerHTML = `
.hidden-gpt {
display: none !important;
}
`;
document.head.appendChild(style);
// 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 save hidden GPTs to localStorage
function saveHiddenGPTs(hiddenList) {
localStorage.setItem(HIDDEN_GPTS_KEY, JSON.stringify(hiddenList));
}
// Function to load hidden GPTs from localStorage
function loadHiddenGPTs() {
const data = localStorage.getItem(HIDDEN_GPTS_KEY);
return data ? JSON.parse(data) : [];
}
// 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 and hidden list
function reorderGPTs(sortList, hiddenList = []) {
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;
}
});
// Apply hidden class based on hiddenList
Object.keys(gptMap).forEach(name => {
if (hiddenList.includes(name)) {
gptMap[name].classList.add('hidden-gpt');
} else {
gptMap[name].classList.remove('hidden-gpt');
}
});
// Optionally, reorder GPTs based on sortList
sortList.forEach(item => {
const gpt = gptMap[item.name];
if (gpt && !hiddenList.includes(item.name)) {
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: '6px 8px', // Reduced padding
margin: '4px 0',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
cursor: 'grab',
color: '#000', // Set list item text color to black
fontSize: '14px', // Reduced font size
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
});
list.appendChild(listItem);
});
modalContent.appendChild(list);
// Buttons container
const buttonsContainer = document.createElement('div');
Object.assign(buttonsContainer.style, {
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: '10px'
});
// Export button
const exportButton = document.createElement('button');
exportButton.textContent = 'Export';
Object.assign(exportButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#555555',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: '1 1 45%'
});
buttonsContainer.appendChild(exportButton);
// Import button
const importButton = document.createElement('button');
importButton.textContent = 'Import';
Object.assign(importButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#555555',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: '1 1 45%'
});
buttonsContainer.appendChild(importButton);
// Refresh button
const refreshButton = document.createElement('button');
refreshButton.textContent = 'Refresh';
Object.assign(refreshButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#ffa500', // Orange color for distinction
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: '1 1 45%'
});
buttonsContainer.appendChild(refreshButton);
// Save button
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
Object.assign(saveButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: '1 1 45%'
});
buttonsContainer.appendChild(saveButton);
// Cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
Object.assign(cancelButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#f44336',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: '1 1 45%'
});
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, loadHiddenGPTs());
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, loadHiddenGPTs());
modalOverlay.remove();
console.log('Sort order imported successfully.');
} else {
throw new Error('Invalid sort list format.');
}
} catch (err) {
console.error('Failed to import sort order:', err.message);
}
};
reader.readAsText(file);
};
input.click();
});
// Refresh functionality
refreshButton.addEventListener('click', () => {
const currentSort = loadCustomSort();
if (currentSort) {
reorderGPTs(currentSort, loadHiddenGPTs());
console.log('GPT list refreshed based on the current sort order.');
} else {
console.log('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" and "See Less/See More" buttons
function injectSortAndSeeButtons() {
const existingReorderButton = document.getElementById(REORDER_BUTTON_ID);
const existingSeeLessButton = document.getElementById(SEE_LESS_BUTTON_ID);
const existingSeeMoreButton = document.getElementById(SEE_MORE_BUTTON_ID);
if (existingReorderButton || existingSeeLessButton || existingSeeMoreButton) return; // Prevent duplicate buttons
const gptContainer = getGPTContainer();
if (!gptContainer) {
console.warn('GPT container not found. Cannot inject the "Reorder GPTs" and "See Less" buttons.');
return;
}
// Create container for the buttons
const buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
display: 'flex',
gap: '10px',
margin: '10px'
});
// Create "Reorder GPTs" button
const reorderButton = document.createElement('button');
reorderButton.id = REORDER_BUTTON_ID;
reorderButton.textContent = 'Reorder GPTs';
Object.assign(reorderButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#008CBA',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
reorderButton.addEventListener('click', () => {
const sortList = loadCustomSort();
if (sortList) {
createSortModal(sortList);
} else {
console.log('No custom sort list found.');
}
});
// Create "See Less" button
const seeLessButton = document.createElement('button');
seeLessButton.id = SEE_LESS_BUTTON_ID;
seeLessButton.textContent = 'See Less';
Object.assign(seeLessButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#e7e7e7',
color: '#000',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
// Create "See More" button
const seeMoreButton = document.createElement('button');
seeMoreButton.id = SEE_MORE_BUTTON_ID;
seeMoreButton.textContent = 'See More';
Object.assign(seeMoreButton.style, {
padding: '6px 12px', // Reduced padding
fontSize: '14px', // Reduced font size
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
display: 'none' // Initially hidden
});
// Append buttons to the container
buttonContainer.appendChild(reorderButton);
buttonContainer.appendChild(seeLessButton);
buttonContainer.appendChild(seeMoreButton);
// Insert the button container at the top of the GPT container
gptContainer.insertBefore(buttonContainer, gptContainer.firstChild);
console.log('"Reorder GPTs" and "See Less" buttons injected.');
// Event listener for "See Less"
seeLessButton.addEventListener('click', () => {
const allGPTs = getAllGPTs();
if (allGPTs.length <= MAX_VISIBLE_GPTS) {
console.log(`There are ${allGPTs.length} GPTs, which is within the visible limit.`);
return;
}
const hiddenGPTs = allGPTs.slice(MAX_VISIBLE_GPTS).map(gpt => {
const nameElement = gpt.querySelector('div.text-sm.text-token-text-primary'); // Update selector if necessary
return nameElement ? nameElement.textContent.trim() : '';
});
// Save hidden GPTs to localStorage
saveHiddenGPTs(hiddenGPTs);
// Reorder GPTs with hidden GPTs
const sortList = loadCustomSort();
reorderGPTs(sortList, hiddenGPTs);
// Toggle button visibility
seeLessButton.style.display = 'none';
seeMoreButton.style.display = 'inline-block';
});
// Event listener for "See More"
seeMoreButton.addEventListener('click', () => {
const hiddenGPTs = loadHiddenGPTs();
if (hiddenGPTs.length === 0) {
console.log('No GPTs are hidden.');
return;
}
// Reorder GPTs without hidden GPTs
const sortList = loadCustomSort();
reorderGPTs(sortList, []);
// Clear hidden GPTs from localStorage
saveHiddenGPTs([]);
// Toggle button visibility
seeLessButton.style.display = 'inline-block';
seeMoreButton.style.display = 'none';
});
// Automatically click the "See Less" button to set the default state
const allGPTs = getAllGPTs();
if (allGPTs.length > MAX_VISIBLE_GPTS) {
seeLessButton.click();
}
}
// 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(async function() {
const gptItem = event.target.closest('a.group.flex.h-10');
if (gptItem) {
console.log('GPT item clicked:', gptItem);
// Identify the sidebar button
const sidebarButton = identifyTargetButton();
if (!sidebarButton) {
console.error('Sidebar button could not be identified.');
return;
}
console.log('Sidebar button found:', sidebarButton);
// Click the button to load all GPTs
sidebarButton.click();
console.log('Sidebar button clicked to load all GPTs.');
try {
// 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 with hidden GPTs
const hiddenGPTs = loadHiddenGPTs();
reorderGPTs(sortList, hiddenGPTs);
console.log('GPTs reordered based on custom sort.');
// Inject the "Reorder GPTs" and "See Less/See More" buttons
injectSortAndSeeButtons();
} catch (err) {
console.error('Error loading GPTs:', err);
}
}
}, 2000);
}
// 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 with hidden GPTs
const hiddenGPTs = loadHiddenGPTs();
reorderGPTs(sortList, hiddenGPTs);
console.log('GPTs reordered based on custom sort.');
// Inject the "Reorder GPTs" and "See Less/See More" buttons
injectSortAndSeeButtons();
// Attach GPT click listener to handle dynamic changes
const gptContainer = getGPTContainer();
if (gptContainer) {
attachGPTClickListener(gptContainer);
}
} catch (error) {
console.error('ChatGPT Sidebar GPT Reorder script error:', error);
}
}
// Attach event listener for 'popstate' to handle URL changes caused by browser navigation
window.addEventListener('popstate', function(event) {
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();
}
});
// Observe changes to the document's title (as an example, adjust if necessary)
observer.observe(document, { subtree: true, childList: true });
// Run the main function after DOM is fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();