ChatGPT bring back date grouping

Brings back the date grouping on chatgpt.com

  1. // ==UserScript==
  2. // @name ChatGPT bring back date grouping
  3. // @version 1.3
  4. // @author tiramifue
  5. // @description Brings back the date grouping on chatgpt.com
  6. // @match https://chatgpt.com/*
  7. // @run-at document-end
  8. // @namespace https://gf.qytechs.cn/users/570213
  9. // @license Apache-2.0
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @noframes
  14. // ==/UserScript==
  15.  
  16. // updated 2025-06-14
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. let groupBy = GM_getValue('groupBy', 'updated');
  22.  
  23. GM_addStyle(`
  24. .__chat-group-header {
  25. font-weight: normal;
  26. padding: 6px 10px;
  27. font-size: 0.85rem;
  28. color: #999;
  29. margin-top: 0;
  30. }
  31.  
  32. .__chat-group-header:not(:first-of-type) {
  33. margin-top: 12px;
  34. }
  35. `)
  36.  
  37. function getDateGroupLabel(isoString) {
  38. const date = new Date(isoString);
  39. const now = new Date();
  40. const msInDay = 24 * 60 * 60 * 1000;
  41. const daysAgo = Math.floor((now - date) / msInDay);
  42. const monthsAgo = (now.getFullYear() - date.getFullYear()) * 12 + (now.getMonth() - date.getMonth());
  43.  
  44. if (daysAgo <= 0) return 'Today';
  45. if (daysAgo === 1) return 'Yesterday';
  46. if (daysAgo <= 6) return `${daysAgo} days ago`;
  47. if (daysAgo <= 13) return 'Last week';
  48. if (daysAgo <= 20) return '2 weeks ago';
  49. if (daysAgo <= 31) return 'Last month';
  50. if (monthsAgo <= 11) return `${monthsAgo} months ago`;
  51. return 'Last year';
  52. }
  53.  
  54. function getReactFiber(dom) {
  55. for (const key in dom) {
  56. if (key.startsWith('__reactFiber$')) return dom[key];
  57. }
  58. return null;
  59. }
  60.  
  61. function extractChatInfo(fiber) {
  62. const c = fiber.memoizedProps?.conversation;
  63. return c
  64. ? {
  65. id: c.id,
  66. title: c.title,
  67. created: c.create_time,
  68. updated: c.update_time,
  69. node: fiber.stateNode
  70. }
  71. : null;
  72. }
  73.  
  74. const seenIds = new Set();
  75. const chatList = [];
  76.  
  77. function processNewChatNode(node) {
  78. const fiber = getReactFiber(node);
  79. if (!fiber) return;
  80.  
  81. let current = fiber;
  82. while (current && !current.memoizedProps?.conversation) {
  83. current = current.return;
  84. }
  85.  
  86. if (!current || !current.memoizedProps?.conversation) return;
  87.  
  88. const chat = extractChatInfo(current);
  89. if (chat && !seenIds.has(chat.id)) {
  90. seenIds.add(chat.id);
  91. const dateKey = chat[groupBy];
  92. chat.node = node;
  93. chatList.push(chat);
  94.  
  95. queueRender();
  96. }
  97. }
  98.  
  99. function groupChatsByGroupName() {
  100. const groups = new Map();
  101.  
  102. for (const chat of chatList) {
  103. chat.group = getDateGroupLabel(chat[groupBy]);
  104. if (!groups.has(chat.group)) groups.set(chat.group, []);
  105. groups.get(chat.group).push(chat);
  106. }
  107.  
  108. return [...groups.entries()].sort((a, b) => {
  109. const aTime = new Date(a[1][0][groupBy]).getTime();
  110. const bTime = new Date(b[1][0][groupBy]).getTime();
  111. return bTime - aTime;
  112. });
  113. }
  114.  
  115. function clearGroupedChats(aside) {
  116. aside.querySelectorAll('a[href^="/c/"], .__chat-group-header').forEach(el => el.remove());
  117. }
  118.  
  119. function renderGroupedChats(aside) {
  120. const observer = aside.__chatObserver;
  121. if (observer) observer.disconnect();
  122.  
  123. clearGroupedChats(aside);
  124. const groups = groupChatsByGroupName();
  125.  
  126. for (const [label, chats] of groups) {
  127. const header = document.createElement('div');
  128. header.className = '__chat-group-header';
  129. header.textContent = label;
  130. aside.appendChild(header);
  131.  
  132. chats
  133. .sort(sortChats)
  134. .forEach(chat => aside.appendChild(chat.node));
  135. }
  136.  
  137. if (observer) observer.observe(aside, { childList: true, subtree: true });
  138. }
  139.  
  140. function sortChats(a, b) {
  141. return new Date(b[groupBy]) - new Date(a[groupBy]);
  142. }
  143.  
  144. let renderTimer = null;
  145.  
  146. function queueRender() {
  147. if (renderTimer) clearTimeout(renderTimer);
  148. renderTimer = setTimeout(() => {
  149. const aside = document.querySelector('#history aside');
  150. if (aside) renderGroupedChats(aside);
  151. }, 200);
  152. }
  153.  
  154. function observeChatList(aside) {
  155. const observer = new MutationObserver(mutations => {
  156. for (const mutation of mutations) {
  157. for (const node of mutation.addedNodes) {
  158. if (node.nodeType === 1 && node.matches('a[href^="/c/"]')) {
  159. processNewChatNode(node);
  160. }
  161. }
  162. for (const node of mutation.removedNodes) {
  163. if (node.nodeType === 1 && node.matches('a[href^="/c/"]')) {
  164. const index = chatList.findIndex(c => c.node === node);
  165. if (index !== -1) {
  166. const removed = chatList.splice(index, 1)[0];
  167. seenIds.delete(removed.id);
  168. queueRender();
  169. }
  170. }
  171. }
  172. }
  173. });
  174.  
  175. observer.observe(aside, { childList: true, subtree: true });
  176. aside.__chatObserver = observer;
  177. aside.querySelectorAll('a[href^="/c/"]').forEach(processNewChatNode);
  178. }
  179.  
  180. function insertToggleButton(aside) {
  181. const header = aside.querySelector('h2');
  182. if (!header || header.querySelector('.__group-toggle')) return;
  183.  
  184. // Wrap h2 content to align flexibly
  185. const wrapper = document.createElement('div');
  186. wrapper.style.cssText = `
  187. display: flex;
  188. justify-content: space-between;
  189. align-items: center;
  190. padding-right: 10px;
  191. `;
  192.  
  193. // Move existing h2 text/content into the wrapper
  194. while (header.firstChild) {
  195. wrapper.appendChild(header.firstChild);
  196. }
  197. header.appendChild(wrapper);
  198.  
  199. const btn = document.createElement('button');
  200. btn.className = '__group-toggle';
  201. const icon = '⇅';
  202. btn.textContent = `${icon} By ${groupBy}`;
  203. btn.title = 'Click to toggle sorting mode';
  204. btn.style.cssText = `
  205. font-size: 0.75rem;
  206. background-color: #2a2b32;
  207. border: 1px solid #444;
  208. border-radius: 999px;
  209. padding: 3px 10px;
  210. color: #ccc;
  211. cursor: pointer;
  212. transition: background-color 0.2s;
  213. `;
  214.  
  215. btn.addEventListener('mouseenter', () => {
  216. btn.style.backgroundColor = '#3a3b42';
  217. });
  218.  
  219. btn.addEventListener('mouseleave', () => {
  220. btn.style.backgroundColor = '#2a2b32';
  221. });
  222.  
  223. btn.addEventListener('click', () => {
  224. groupBy = groupBy === 'updated' ? 'created' : 'updated';
  225. GM_setValue('groupBy', groupBy);
  226. btn.textContent = `${icon} By ${groupBy}`;
  227. queueRender();
  228. });
  229.  
  230. wrapper.appendChild(btn);
  231. }
  232.  
  233. (function watchSidebar() {
  234. let lastAside = null;
  235.  
  236. function setup(aside) {
  237. if (!aside || aside === lastAside) return;
  238. lastAside = aside;
  239.  
  240. insertToggleButton(aside);
  241. observeChatList(aside);
  242. renderGroupedChats(aside);
  243. console.log("ChatGPT grouping: sidebar attached.");
  244. }
  245.  
  246. const rootObserver = new MutationObserver(() => {
  247. const history = document.querySelector('#history');
  248. if (!history) return;
  249.  
  250. const aside = history.querySelector('aside');
  251. if (aside && aside !== lastAside) {
  252. setup(aside);
  253. }
  254. });
  255.  
  256. rootObserver.observe(document.body, { childList: true, subtree: true });
  257.  
  258. const asideNow = document.querySelector('#history aside');
  259. if (asideNow) setup(asideNow);
  260. })();
  261. })();

QingJ © 2025

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