Pin Chats in ChatGPT

Allows pinning chats in the ChatGPT sidebar

// ==UserScript==
// @name         Pin Chats in ChatGPT
// @version      20240522
// @description  Allows pinning chats in the ChatGPT sidebar
// @author       jamesdeluk
// @match        https://chatgpt.com/*
// @grant        none
// @namespace https://greasyfork.org/users/242246
// ==/UserScript==

(function () {
  "use strict";

  async function waitForElement(selector) {
    while (!document.querySelector(selector)) {
      await new Promise((resolve) => requestAnimationFrame(resolve));
    }

    // Load pinned chats from local storage
    var pinnedChats = JSON.parse(localStorage.getItem("pinnedChats") || "[]");

    // Add toggle pin button to each chat item
    function addPinButton(chatItem, chatId, chatTitle) {
      var pinButton = document.createElement("button");
      pinButton.innerHTML = "📌";
      pinButton.className = "pin-button";
      pinButton.title = "Toggle pin";
      pinButton.onclick = function (event) {
        event.preventDefault();
        event.stopPropagation();
        togglePinChat(chatId, chatTitle, pinButton);
      };
      var chatTextDiv = chatItem.querySelector(
        ".relative.grow.overflow-hidden.whitespace-nowrap"
      );
      chatTextDiv.parentElement.insertBefore(pinButton, chatTextDiv);
    }

    // Toggle pin/unpin chat function
    function togglePinChat(chatId, chatTitle, button) {
      var chatIndex = pinnedChats.findIndex((chat) => chat.id === chatId);
      if (chatIndex === -1) {
        pinnedChats.push({ id: chatId, title: chatTitle });
      } else {
        pinnedChats.splice(chatIndex, 1);
      }
      localStorage.setItem("pinnedChats", JSON.stringify(pinnedChats));
      renderPinnedChats();
    }

    // Render pinned chats
    function renderPinnedChats() {
      var todaySection = ''
      var headers = document.querySelectorAll("h3");
      for (var i = 0; i < headers.length; i++) {
        if (headers[i].textContent.trim() === "Today") {
          todaySection = headers[i].closest("div");
        }
      }
      var pinnedSection = document.getElementById("pinned-chats-section");

      if (!pinnedSection) {
        pinnedSection = document.createElement("div");
        pinnedSection.id = "pinned-chats-section";
        todaySection.parentElement.insertBefore(pinnedSection, todaySection);
      }

      pinnedSection.innerHTML = "";
      var header = document.createElement("h3");
      header.className =
        "pb-2 pt-3 px-2 text-xs font-medium text-ellipsis overflow-hidden break-all text-token-text-secondary";
      header.innerText = "Pinned";
      pinnedSection.appendChild(header);

      pinnedChats.forEach((chat) => {
        var chatItem = document.createElement("li");
        chatItem.className =
          "group relative rounded-lg active:opacity-90 hover:bg-token-sidebar-surface-secondary";
        chatItem.innerHTML = `
                <a href="/c/${chat.id}" class="flex items-center gap-2 p-2">
                    <div class="relative grow overflow-hidden whitespace-nowrap">${chat.title}
                        <div class="absolute bottom-0 right-0 top-0 bg-gradient-to-l to-transparent from-token-sidebar-surface-primary group-hover:from-token-sidebar-surface-secondary w-8 from-0% group-hover:w-20"></div>
                    </div>
                </a>
            `;
        pinnedSection.appendChild(chatItem);
      });
    }

    // Initial rendering of pinned chats
    renderPinnedChats();

    // Observe sidebar for new chat items
    var sidebarObserver = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.addedNodes) {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === 1 && node.matches("li")) {
              var chatId = node
                .querySelector("a")
                .getAttribute("href")
                .split("/")
                .pop();
              var chatTitle = node.querySelector(
                ".relative.grow.overflow-hidden.whitespace-nowrap"
              ).innerText;
              addPinButton(node, chatId, chatTitle);
            }
          });
        }
      });
    });

    var sidebar = document.querySelector("nav");
    if (sidebar) {
      sidebarObserver.observe(sidebar, { childList: true, subtree: true });

      // Add pin buttons to existing chat items
      sidebar.querySelectorAll("li").forEach((chatItem) => {
        var chatId = chatItem
          .querySelector("a")
          .getAttribute("href")
          .split("/")
          .pop();
        var chatTitle = chatItem.querySelector(
          ".relative.grow.overflow-hidden.whitespace-nowrap"
        ).innerText;
        addPinButton(chatItem, chatId, chatTitle);
      });
    }
  }

  waitForElement(".relative.grow.overflow-hidden.whitespace-nowrap");
})();