ChatGPT Pin Chats

Add a pin button to each chat in ChatGPT's sidebar, making it easy to save important conversations. Pinned chats are displayed in the 'Pinned Chats' section at the bottom of the sidebar. Click pinned chats to reopen them, and you can also remove individual pins or clear all pinned chats.

目前為 2025-03-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT Pin Chats
  3. // @namespace https://gf.qytechs.cn/en/users/1444872-tlbstation
  4. // @version 1.8.0
  5. // @description Add a pin button to each chat in ChatGPT's sidebar, making it easy to save important conversations. Pinned chats are displayed in the 'Pinned Chats' section at the bottom of the sidebar. Click pinned chats to reopen them, and you can also remove individual pins or clear all pinned chats.
  6. // @icon https://i.ibb.co/jZ3HpwPk/pngwing-com.png
  7. // @author TLBSTATION
  8. // @match https://chatgpt.com/*
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const STORAGE_KEY = 'pinnedChatsGPT';
  18.  
  19. function getPinnedChats() {
  20. return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
  21. }
  22.  
  23. function savePinnedChats(chats) {
  24. localStorage.setItem(STORAGE_KEY, JSON.stringify(chats));
  25. }
  26.  
  27. function createPinnedSection() {
  28. let pinnedContainer = document.querySelector('#pinned-chats');
  29. if (!pinnedContainer) {
  30. pinnedContainer = document.createElement('div');
  31. pinnedContainer.id = 'pinned-chats';
  32. pinnedContainer.style.padding = '10px';
  33. pinnedContainer.style.marginBottom = '0';
  34. pinnedContainer.style.borderBottom = '2px solid rgba(255, 255, 255, 0.2)';
  35. //pinnedContainer.style.borderTop = '2px solid rgba(255, 255, 255, 0.2)';
  36. pinnedContainer.style.color = 'white';
  37. pinnedContainer.style.paddingLeft = '10px';
  38. pinnedContainer.style.marginLeft = '-11px';
  39.  
  40. function insertPinnedSection() {
  41. const targetDiv = document.querySelector('.flex-col.flex-1.transition-opacity.duration-500.relative.pr-3.overflow-y-auto');
  42.  
  43. if (targetDiv && targetDiv.parentElement) {
  44. targetDiv.parentElement.insertBefore(pinnedContainer, targetDiv.nextSibling); // Insert after target div
  45. return true;
  46. }
  47. return false;
  48. }
  49.  
  50. // Try inserting immediately
  51. if (!insertPinnedSection()) {
  52. // If not found, keep checking until target div appears
  53. const observer = new MutationObserver(() => {
  54. if (insertPinnedSection()) observer.disconnect();
  55. });
  56. observer.observe(document.body, { childList: true, subtree: true });
  57. }
  58. }
  59. return pinnedContainer;
  60. }
  61.  
  62.  
  63. function updatePinnedSection() {
  64. const pinnedContainer = createPinnedSection();
  65. const pinnedChats = getPinnedChats();
  66. pinnedContainer.innerHTML = '';
  67.  
  68. // Header (Title + Clear Button)
  69. const header = document.createElement('div');
  70. header.style.display = 'flex';
  71. header.style.alignItems = 'center';
  72. header.style.justifyContent = 'space-between';
  73. header.style.marginBottom = '10px';
  74.  
  75. const title = document.createElement('h3');
  76. title.innerText = '📌 Pinned Chats';
  77. title.style.fontSize = '16px';
  78. title.style.fontWeight = 'bold';
  79. title.style.margin = '0';
  80.  
  81. const clearButton = document.createElement('button');
  82. clearButton.innerText = '❌';
  83. clearButton.style.padding = '4px 4px';
  84. clearButton.style.border = 'none';
  85. clearButton.style.cursor = 'pointer';
  86. clearButton.style.background = 'white';
  87. clearButton.style.color = 'black';
  88. clearButton.style.fontSize = '11px';
  89. clearButton.style.borderRadius = '4px';
  90. clearButton.style.transition = 'background 0.3s';
  91.  
  92. clearButton.addEventListener('mouseenter', () => {
  93. clearButton.style.background = '#682828';
  94. clearButton.style.color = 'white';
  95. });
  96. clearButton.addEventListener('mouseleave', () => {
  97. clearButton.style.background = 'white';
  98. clearButton.style.color = 'black';
  99. });
  100.  
  101. clearButton.addEventListener('click', () => {
  102. localStorage.removeItem(STORAGE_KEY);
  103. updatePinnedSection();
  104. addPinToChats(); // Ensure pins are re-checked
  105.  
  106. // Reset all pin button opacities in chat history
  107. document.querySelectorAll('.pin-button').forEach(button => {
  108. button.style.opacity = '0.5';
  109. });
  110. });
  111.  
  112.  
  113. header.appendChild(title);
  114. header.appendChild(clearButton);
  115. pinnedContainer.appendChild(header);
  116.  
  117. // Pinned chats list
  118. if (pinnedChats.length > 0) {
  119. clearButton.style.display = 'block';
  120. const list = document.createElement('ul');
  121. list.style.listStyle = 'none';
  122. list.style.padding = '0';
  123. list.style.margin = '0';
  124. // Set fixed height and make it scrollable
  125. list.style.maxHeight = '200px'; // Adjust height as needed
  126. list.style.overflowY = 'auto';
  127. list.style.paddingRight = '5px'; // Prevents scrollbar from overlapping content
  128.  
  129. pinnedChats.forEach(chat => {
  130. const chatItem = document.createElement('li');
  131. chatItem.style.padding = '8px';
  132. chatItem.style.marginBottom = '5px';
  133. chatItem.style.cursor = 'pointer';
  134. chatItem.style.borderRadius = '6px';
  135. chatItem.style.background = 'rgba(255, 255, 255, 0.1)';
  136. chatItem.style.transition = 'background 0.3s';
  137. chatItem.style.display = 'flex';
  138. chatItem.style.alignItems = 'center';
  139.  
  140. chatItem.addEventListener('mouseenter', () => {
  141. chatItem.style.background = 'rgba(255, 255, 255, 0.2)';
  142. });
  143. chatItem.addEventListener('mouseleave', () => {
  144. chatItem.style.background = 'rgba(255, 255, 255, 0.1)';
  145. });
  146.  
  147. // Create clickable link
  148. const chatLink = document.createElement('a');
  149. chatLink.href = `https://chatgpt.com/c/${chat.id}`;
  150. chatLink.innerText = chat.title;
  151. chatLink.style.color = 'white';
  152. chatLink.style.textDecoration = 'none';
  153. chatLink.style.flexGrow = '1';
  154. chatLink.style.whiteSpace = 'nowrap';
  155. chatLink.style.overflow = 'hidden';
  156. chatLink.style.textOverflow = 'ellipsis';
  157.  
  158.  
  159.  
  160. // Remove button
  161. const removeButton = document.createElement('span');
  162. removeButton.innerText = '❌';
  163. removeButton.style.cursor = 'pointer';
  164. removeButton.style.marginLeft = '10px';
  165. removeButton.style.fontSize = '14px';
  166. removeButton.style.opacity = '0';
  167. removeButton.style.transition = 'opacity 0.3s';
  168.  
  169. chatItem.addEventListener('mouseenter', () => {
  170. removeButton.style.opacity = '1';
  171. });
  172. chatItem.addEventListener('mouseleave', () => {
  173. removeButton.style.opacity = '0';
  174. });
  175.  
  176. removeButton.addEventListener('click', (event) => {
  177. event.stopPropagation();
  178.  
  179. let updatedChats = getPinnedChats().filter(c => c.id !== chat.id);
  180. savePinnedChats(updatedChats);
  181. updatePinnedSection();
  182.  
  183. // Update pin button opacity in chat history
  184. document.querySelectorAll('.pin-button').forEach(button => {
  185. const parentChatItem = button.closest('li[data-testid^="history-item-"]');
  186. if (parentChatItem) {
  187. const chatID = parentChatItem.querySelector('a')?.getAttribute('href')?.split('/c/')[1];
  188. if (chatID === chat.id) {
  189. button.style.opacity = '0.5'; // Set opacity to indicate unpinned
  190. }
  191. }
  192. });
  193. });
  194.  
  195.  
  196. chatItem.appendChild(chatLink);
  197. chatItem.appendChild(removeButton);
  198. list.appendChild(chatItem);
  199. });
  200.  
  201. pinnedContainer.appendChild(list);
  202. } else {
  203. clearButton.style.display = 'none';
  204. const emptyMessage = document.createElement('p');
  205. emptyMessage.innerText = 'No pinned chats';
  206. emptyMessage.style.color = 'rgba(255, 255, 255, 0.6)';
  207. emptyMessage.style.fontSize = '14px';
  208. pinnedContainer.appendChild(emptyMessage);
  209. }
  210. }
  211.  
  212. function addPinToChats() {
  213. document.querySelectorAll('li[data-testid^="history-item-"]').forEach(chatItem => {
  214. if (chatItem.querySelector('.pin-button')) return;
  215.  
  216. const chatTitleElement = chatItem.querySelector('a div');
  217. if (!chatTitleElement) return;
  218.  
  219. const chatTitle = chatTitleElement.innerText.trim();
  220. const chatID = chatItem.querySelector('a')?.getAttribute('href')?.split('/c/')[1];
  221.  
  222. const pinButton = document.createElement('span');
  223. pinButton.className = 'pin-button';
  224. pinButton.innerText = '📌';
  225. pinButton.style.cursor = 'pointer';
  226. pinButton.style.marginRight = '8px';
  227. pinButton.style.fontSize = '16px';
  228. pinButton.style.opacity = '0.5';
  229.  
  230. let pinnedChats = getPinnedChats();
  231. if (pinnedChats.some(c => c.id === chatID)) pinButton.style.opacity = '1';
  232.  
  233. pinButton.addEventListener('click', (event) => {
  234. event.stopPropagation(); // Prevents bubbling up
  235. event.preventDefault(); // Ensures no accidental refresh or navigation
  236.  
  237. let pinnedChats = getPinnedChats();
  238.  
  239. if (pinnedChats.some(c => c.id === chatID)) {
  240. pinnedChats = pinnedChats.filter(c => c.id !== chatID);
  241. pinButton.style.opacity = '0.5';
  242. } else {
  243. pinnedChats.unshift({ title: chatTitle, id: chatID });
  244. pinButton.style.opacity = '1';
  245. }
  246.  
  247. savePinnedChats(pinnedChats);
  248. updatePinnedSection();
  249. });
  250.  
  251. chatTitleElement.parentElement.prepend(pinButton);
  252. });
  253. }
  254.  
  255. function observeSidebar() {
  256. const observer = new MutationObserver(() => addPinToChats());
  257. const sidebar = document.querySelector('nav');
  258. if (sidebar) observer.observe(sidebar, { childList: true, subtree: true });
  259. }
  260.  
  261. function init() {
  262. addPinToChats();
  263. updatePinnedSection();
  264. observeSidebar();
  265. }
  266. function waitForElement(selector, callback) {
  267. const observer = new MutationObserver((mutations, obs) => {
  268. if (document.querySelector(selector)) {
  269. callback();
  270. obs.disconnect(); // Stop observing once the element is found
  271. }
  272. });
  273.  
  274. observer.observe(document.body, { childList: true, subtree: true });
  275. }
  276.  
  277. // Start script after the required element appears
  278. waitForElement(".relative.grow.overflow-hidden.whitespace-nowrap", init);
  279.  
  280.  
  281.  
  282. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址