ChatGPT bring back date grouping

Brings back the date grouping on chatgpt.com

  1. // ==UserScript==
  2. // @name ChatGPT bring back date grouping
  3. // @version 2
  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-07-27
  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. .__chat-date-label {
  37. position: absolute;
  38. top: 0;
  39. right: 12px;
  40. font-size: 0.7rem;
  41. color: #888;
  42. pointer-events: none;
  43. background-color: transparent;
  44. line-height: 1;
  45. text-shadow: 0 0 2px rgba(0,0,0,0.5);
  46. }
  47.  
  48. .__chat-timestamp {
  49. position: absolute;
  50. right: 8px;
  51. top: -1px;
  52. font-size: 0.7rem;
  53. color: #999;
  54. pointer-events: none;
  55. }
  56. .__hide-timestamps .__chat-timestamp {
  57. display: none;
  58. }
  59.  
  60. .__timestamp-icon {
  61. position: relative;
  62. display: inline-block;
  63. opacity: 1;
  64. transition: opacity 0.2s;
  65. }
  66.  
  67. .__timestamp-icon.__disabled {
  68. opacity: 0.5;
  69. }
  70.  
  71. .__timestamp-icon.__disabled::after {
  72. content: "";
  73. position: absolute;
  74. top: 50%;
  75. left: 50%;
  76. width: 110%;
  77. height: 0;
  78. border-top: 2px solid #fff;
  79. transform: translate(-50%, -50%) rotate(-45deg);
  80. transform-origin: center;
  81. pointer-events: none;
  82. }
  83. `)
  84.  
  85. function getDateGroupLabel(isoString) {
  86. const date = new Date(isoString);
  87. const now = new Date();
  88. const msInDay = 24 * 60 * 60 * 1000;
  89. const daysAgo = Math.floor((now - date) / msInDay);
  90. const monthsAgo = (now.getFullYear() - date.getFullYear()) * 12 + (now.getMonth() - date.getMonth());
  91.  
  92. if (daysAgo <= 0) return 'Today';
  93. if (daysAgo === 1) return 'Yesterday';
  94. if (daysAgo <= 6) return `${daysAgo} days ago`;
  95. if (daysAgo <= 13) return 'Last week';
  96. if (daysAgo <= 20) return '2 weeks ago';
  97. if (daysAgo <= 31) return 'Last month';
  98. if (monthsAgo <= 11) return `${monthsAgo} months ago`;
  99. return 'Last year';
  100. }
  101.  
  102. function getReactFiber(dom) {
  103. for (const key in dom) {
  104. if (key.startsWith('__reactFiber$')) return dom[key];
  105. }
  106. return null;
  107. }
  108.  
  109. function extractChatInfo(fiber) {
  110. const c = fiber.memoizedProps?.conversation;
  111. return c
  112. ? {
  113. id: c.id,
  114. title: c.title,
  115. created: c.create_time,
  116. updated: c.update_time,
  117. node: fiber.stateNode
  118. }
  119. : null;
  120. }
  121.  
  122. const seenIds = new Set();
  123. const chatList = [];
  124.  
  125. function processNewChatNode(node) {
  126. const fiber = getReactFiber(node);
  127. if (!fiber) return;
  128.  
  129. let current = fiber;
  130. while (current && !current.memoizedProps?.conversation) {
  131. current = current.return;
  132. }
  133.  
  134. if (!current || !current.memoizedProps?.conversation) return;
  135.  
  136. const chat = extractChatInfo(current);
  137. if (chat && !seenIds.has(chat.id)) {
  138. seenIds.add(chat.id);
  139. const dateKey = chat[groupBy];
  140. chat.node = node;
  141. chatList.push(chat);
  142.  
  143. queueRender();
  144. }
  145. }
  146.  
  147. function groupChatsByGroupName() {
  148. const groups = new Map();
  149.  
  150. for (const chat of chatList) {
  151. chat.group = getDateGroupLabel(chat[groupBy]);
  152. if (!groups.has(chat.group)) groups.set(chat.group, []);
  153. groups.get(chat.group).push(chat);
  154. }
  155.  
  156. return [...groups.entries()].sort((a, b) => {
  157. const aTime = new Date(a[1][0][groupBy]).getTime();
  158. const bTime = new Date(b[1][0][groupBy]).getTime();
  159. return bTime - aTime;
  160. });
  161. }
  162.  
  163. function clearGroupedChats(aside) {
  164. aside.querySelectorAll('a[href^="/c/"], .__chat-group-header').forEach(el => el.remove());
  165. }
  166.  
  167. function renderGroupedChats(aside) {
  168. const observer = aside.__chatObserver;
  169. if (observer) observer.disconnect();
  170.  
  171. clearGroupedChats(aside);
  172. const groups = groupChatsByGroupName();
  173.  
  174. for (const [label, chats] of groups) {
  175. const currentGroupBy = groupBy;
  176.  
  177. const header = document.createElement('div');
  178. header.className = '__chat-group-header';
  179. header.textContent = label;
  180. aside.appendChild(header);
  181.  
  182. chats
  183. .sort((a, b) => new Date(b[currentGroupBy]) - new Date(a[currentGroupBy]))
  184. .forEach(chat => {
  185. const existingLabel = chat.node.querySelector('.__chat-timestamp');
  186. if (existingLabel) existingLabel.remove();
  187.  
  188. const timestamp = document.createElement('div');
  189. timestamp.className = '__chat-timestamp';
  190. timestamp.textContent = new Date(chat[currentGroupBy]).toLocaleDateString(undefined, {
  191. year: 'numeric', month: 'short', day: 'numeric'
  192. });
  193.  
  194. chat.node.style.position = 'relative';
  195. chat.node.appendChild(timestamp);
  196.  
  197. aside.appendChild(chat.node);
  198. });
  199.  
  200. }
  201.  
  202.  
  203. if (observer) observer.observe(aside, { childList: true, subtree: true });
  204. }
  205.  
  206.  
  207. function sortChats(a, b) {
  208. return new Date(b[groupBy]) - new Date(a[groupBy]);
  209. }
  210.  
  211. let renderTimer = null;
  212.  
  213. function queueRender() {
  214. if (renderTimer) clearTimeout(renderTimer);
  215. renderTimer = setTimeout(() => {
  216. const aside = document.querySelector('#history aside');
  217. if (aside) renderGroupedChats(aside);
  218. }, 200);
  219. }
  220.  
  221. function observeChatList(aside) {
  222. const observer = new MutationObserver(mutations => {
  223. for (const mutation of mutations) {
  224. for (const node of mutation.addedNodes) {
  225. if (node.nodeType === 1 && node.matches('a[href^="/c/"]')) {
  226. processNewChatNode(node);
  227. }
  228. }
  229. for (const node of mutation.removedNodes) {
  230. if (node.nodeType === 1 && node.matches('a[href^="/c/"]')) {
  231. const index = chatList.findIndex(c => c.node === node);
  232. if (index !== -1) {
  233. const removed = chatList.splice(index, 1)[0];
  234. seenIds.delete(removed.id);
  235. queueRender();
  236. }
  237. }
  238. }
  239. }
  240. });
  241.  
  242. observer.observe(aside, { childList: true, subtree: true });
  243. aside.__chatObserver = observer;
  244. aside.querySelectorAll('a[href^="/c/"]').forEach(processNewChatNode);
  245. }
  246.  
  247. function insertToggleButton(aside) {
  248. const header = aside.querySelector('h2');
  249. if (!header || header.querySelector('.__group-toggle')) return;
  250.  
  251. // Wrap h2 content to align flexibly
  252. const wrapper = document.createElement('div');
  253. wrapper.style.cssText = `
  254. display: flex;
  255. justify-content: space-between;
  256. align-items: center;
  257. width: 100%;
  258. `;
  259.  
  260. // Move existing h2 text/content into the wrapper
  261. while (header.firstChild) {
  262. wrapper.appendChild(header.firstChild);
  263. }
  264. header.appendChild(wrapper);
  265.  
  266. const btn = document.createElement('button');
  267. btn.className = '__group-toggle';
  268. const icon = '⇅';
  269. btn.textContent = `${icon} By ${groupBy}`;
  270. btn.title = 'Click to toggle sorting mode';
  271. btn.style.cssText = `
  272. font-size: 0.75rem;
  273. background-color: #2a2b32;
  274. border: 1px solid #444;
  275. border-radius: 999px;
  276. padding: 3px 10px;
  277. color: #ccc;
  278. cursor: pointer;
  279. transition: background-color 0.2s;
  280. `;
  281.  
  282. btn.addEventListener('mouseenter', () => {
  283. btn.style.backgroundColor = '#3a3b42';
  284. });
  285.  
  286. btn.addEventListener('mouseleave', () => {
  287. btn.style.backgroundColor = '#2a2b32';
  288. });
  289.  
  290. btn.addEventListener('click', () => {
  291. groupBy = groupBy === 'updated' ? 'created' : 'updated';
  292. GM_setValue('groupBy', groupBy);
  293. btn.textContent = `${icon} By ${groupBy}`;
  294. queueRender();
  295. });
  296.  
  297. const timestampBtn = document.createElement('button');
  298. timestampBtn.className = '__toggle-timestamps';
  299. timestampBtn.style.cssText = `
  300. display: flex;
  301. align-items: center;
  302. font-size: 0.75rem;
  303. background-color: #2a2b32;
  304. border: 1px solid #444;
  305. border-radius: 999px;
  306. padding: 3px 10px;
  307. color: #ccc;
  308. cursor: pointer;
  309. transition: background-color 0.2s;
  310. `;
  311.  
  312. const timestampIcon = document.createElement('span');
  313. timestampIcon.className = '__timestamp-icon';
  314. timestampIcon.textContent = '🕒';
  315.  
  316. function updateTimestampIcon(state) {
  317. timestampIcon.classList.toggle('__disabled', !state);
  318. }
  319. updateTimestampIcon(GM_getValue('showTimestamps', true));
  320.  
  321.  
  322. timestampBtn.appendChild(timestampIcon);
  323. timestampBtn.title = 'Toggle timestamps';
  324.  
  325. timestampBtn.addEventListener('mouseenter', () => {
  326. timestampBtn.style.backgroundColor = '#3a3b42';
  327. });
  328. timestampBtn.addEventListener('mouseleave', () => {
  329. timestampBtn.style.backgroundColor = '#2a2b32';
  330. });
  331.  
  332. timestampBtn.addEventListener('click', () => {
  333. const current = GM_getValue('showTimestamps', true);
  334. const next = !current;
  335. GM_setValue('showTimestamps', next);
  336. updateTimestampIcon(next);
  337. const aside = document.querySelector('#history aside');
  338. if (aside) aside.classList.toggle('__hide-timestamps', !next);
  339. });
  340.  
  341. const buttonGroup = document.createElement('div');
  342. buttonGroup.style.cssText = `
  343. display: flex;
  344. gap: 6px;
  345. margin-left: auto;
  346. `;
  347.  
  348. buttonGroup.appendChild(btn);
  349. buttonGroup.appendChild(timestampBtn);
  350. wrapper.appendChild(buttonGroup);
  351.  
  352.  
  353. }
  354.  
  355. (function watchSidebar() {
  356. let lastAside = null;
  357.  
  358. function setup(aside) {
  359. if (!aside || aside === lastAside) return;
  360. lastAside = aside;
  361.  
  362. aside.classList.toggle('__hide-timestamps', !GM_getValue('showTimestamps', true));
  363.  
  364. insertToggleButton(aside);
  365. observeChatList(aside);
  366. renderGroupedChats(aside);
  367. console.log("ChatGPT grouping: sidebar attached.");
  368. }
  369.  
  370. const rootObserver = new MutationObserver(() => {
  371. const history = document.querySelector('#history');
  372. if (!history) return;
  373.  
  374. const aside = history.querySelector('aside');
  375. if (aside && aside !== lastAside) {
  376. setup(aside);
  377. }
  378. });
  379.  
  380. rootObserver.observe(document.body, { childList: true, subtree: true });
  381.  
  382. const asideNow = document.querySelector('#history aside');
  383. if (asideNow) setup(asideNow);
  384. })();
  385. })();

QingJ © 2025

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