Pin Chats in ChatGPT

Allows pinning chats in the ChatGPT sidebar

目前為 2024-05-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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");
})();