您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add persistent favorite/bookmark functionality to ChatGPT conversations with local storage support
// ==UserScript== // @name GPT Chat Pin // @namespace github.com/longkidkoolstar // @version 1.1.1 // @description Add persistent favorite/bookmark functionality to ChatGPT conversations with local storage support // @author longkidkoolstar // @license none // @match https://chatgpt.com/* // @grant GM.setValue // @grant GM.getValue // @grant GM.listValues // ==/UserScript== (function() { 'use strict'; console.log('GPT Chat Pin script loaded'); // Add CSS styles with dark mode support const styles = ` #favorite-chats-section { background-color: rgba(0, 0, 0, 0.05); border-radius: 8px; padding: 12px 16px; margin: 10px 0; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: background-color 0.3s, box-shadow 0.3s; } .dark #favorite-chats-section { background-color: rgba(255, 255, 255, 0.05); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } #favorite-chats-section h3 { margin: 0 0 10px 0; font-size: 16px; font-weight: 600; color: #202123; display: flex; align-items: center; gap: 6px; } .dark #favorite-chats-section h3 { color: #d1d5db; } #favorite-chats-section ul { list-style-type: none; margin: 0; padding: 0; } #favorite-chats-section li { margin: 6px 0; padding: 4px 0; display: flex; align-items: center; } #favorite-chats-section a { color: #202123; text-decoration: none; font-size: 14px; display: flex; align-items: center; width: 100%; padding: 6px 10px; border-radius: 6px; transition: all 0.2s ease; } #favorite-chats-section a span { display: inline-block; max-width: 17ch; /* Roughly 20 characters */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dark #favorite-chats-section a { color: #d1d5db; } #favorite-chats-section a:hover { background-color: rgba(0, 0, 0, 0.1); transform: translateX(2px); } .dark #favorite-chats-section a:hover { background-color: rgba(255, 255, 255, 0.1); } #favorite-chats-section a::before { content: '⭐'; margin-right: 8px; font-size: 14px; } .favorite-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: all 0.2s ease; position: relative; } .favorite-icon:hover { background-color: rgba(0, 0, 0, 0.1); transform: scale(1.2); } .dark .favorite-icon:hover { background-color: rgba(255, 255, 255, 0.1); } .favorite-icon.active { color: #ffb400; } .dark .favorite-icon.active { color: #ffd700; } `; // Add styles to document const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement); // Custom Alert Box Styles const customAlertStyles = ` .custom-alert-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .custom-alert-box { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); text-align: center; max-width: 400px; width: 90%; color: #202123; } .dark .custom-alert-box { background-color: #343541; color: #d1d5db; } .custom-alert-box h4 { margin-top: 0; font-size: 18px; margin-bottom: 15px; } .custom-alert-box p { margin-bottom: 20px; font-size: 14px; line-height: 1.5; } .custom-alert-buttons button { background-color: #10a37f; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 14px; margin: 0 10px; transition: background-color 0.2s ease; } .custom-alert-buttons button:hover { background-color: #0e8e6f; } .custom-alert-buttons button.cancel { background-color: #dc3545; } .custom-alert-buttons button.cancel:hover { background-color: #c82333; } `; const customAlertStyleElement = document.createElement('style'); customAlertStyleElement.textContent = customAlertStyles; document.head.appendChild(customAlertStyleElement); // Custom Alert Box Function function showCustomAlert(message, onConfirm) { const overlay = document.createElement('div'); overlay.classList.add('custom-alert-overlay'); const alertBox = document.createElement('div'); alertBox.classList.add('custom-alert-box'); const title = document.createElement('h4'); title.textContent = 'Confirm Action'; const msg = document.createElement('p'); msg.textContent = message; const buttonContainer = document.createElement('div'); buttonContainer.classList.add('custom-alert-buttons'); const confirmBtn = document.createElement('button'); confirmBtn.textContent = 'Confirm'; confirmBtn.addEventListener('click', () => { document.body.removeChild(overlay); onConfirm(true); }); const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; cancelBtn.classList.add('cancel'); cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); onConfirm(false); }); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(confirmBtn); alertBox.appendChild(title); alertBox.appendChild(msg); alertBox.appendChild(buttonContainer); overlay.appendChild(alertBox); document.body.appendChild(overlay); } // Function to add the favorite icon to individual chat links function addFavoriteIcon() { const chatLinks = document.querySelectorAll("a[href^='/c/']"); // Select all chat links chatLinks.forEach(targetElement => { if (targetElement && !targetElement.querySelector('.favorite-icon')) { const chatId = targetElement.href.split('/').pop(); // Assuming chat ID is the last part of the URL const favoriteIcon = document.createElement('span'); favoriteIcon.classList.add('favorite-icon'); favoriteIcon.style.cursor = 'pointer'; favoriteIcon.style.marginLeft = '8px'; favoriteIcon.style.fontSize = '16px'; // Check initial favorite state GM.getValue(`favorite_chat_${chatId}`, false).then(isFavorited => { favoriteIcon.textContent = isFavorited ? '⭐' : '☆'; // Star emoji for favorited, outline star for unfavorited if (isFavorited) { favoriteIcon.classList.add('active'); } else { favoriteIcon.classList.remove('active'); } }); favoriteIcon.addEventListener('click', (event) => { event.stopPropagation(); // Prevent event bubbling event.preventDefault(); // Prevent navigation when clicking the icon GM.getValue(`favorite_chat_${chatId}`, false).then(isFavorited => { const newState = !isFavorited; let chatTitle = ''; if (newState) { // Extract the chat title from the link text chatTitle = targetElement.textContent.trim(); // Remove any emoji or special characters that might be part of the favorite icon chatTitle = chatTitle.replace(/[⭐☆]/g, '').trim(); } if (newState) { // If favoriting, set the value GM.setValue(`favorite_chat_${chatId}`, { isFavorited: newState, title: chatTitle }).then(() => { favoriteIcon.textContent = '⭐'; favoriteIcon.classList.add('active'); console.log(`Chat ${chatId} favorited with title: ${chatTitle}`); displayFavoritedChats(); // Update favorite list }); } else { // If unfavoriting, remove the value showCustomAlert('Are you sure you want to unfavorite this chat?', (confirmed) => { if (confirmed) { GM.deleteValue(`favorite_chat_${chatId}`).then(() => { favoriteIcon.textContent = '☆'; favoriteIcon.classList.remove('active'); console.log(`Chat ${chatId} unfavorited and removed from storage`); displayFavoritedChats(); // Update favorite list }); } }); } }); }); targetElement.appendChild(favoriteIcon); } }); } // Observe changes in the DOM to add the icon when new chat links are added const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { addFavoriteIcon(); break; // Only need to run once per mutation batch } } }); // Start observing the element that contains the chat history for childList changes // This is more efficient than observing the entire document.body const historyAside = document.querySelector("#history > aside:nth-child(1)"); if (historyAside) { observer.observe(historyAside, { childList: true, subtree: true }); } else { // Fallback to observing body if the specific history element isn't immediately available observer.observe(document.body, { childList: true, subtree: true }); } // Function to display favorited chats async function displayFavoritedChats() { const historyElement = document.querySelector("#history"); if (!historyElement) return; let favoriteSection = document.querySelector("#favorite-chats-section"); if (!favoriteSection) { favoriteSection = document.createElement('div'); favoriteSection.id = 'favorite-chats-section'; historyElement.parentNode.insertBefore(favoriteSection, historyElement); } favoriteSection.innerHTML = '<h3><span style="color: #ffb400;">⭐</span> Favorite Chats</h3>'; // Clear and add title with star icon const allKeys = await GM.listValues(); const favoriteChatKeys = allKeys.filter(key => key.startsWith('favorite_chat_') && !key.startsWith('favorite_chat_title_')); if (favoriteChatKeys.length === 0) { favoriteSection.innerHTML += '<p style="color: #6e6e80; font-size: 14px; font-style: italic; margin: 8px 0;">No favorite chats yet.</p>'; return; } const ul = document.createElement('ul'); for (const key of favoriteChatKeys) { const favoriteData = await GM.getValue(key, null); if (favoriteData && favoriteData.isFavorited) { const chatId = key.replace('favorite_chat_', ''); let chatTitle = favoriteData.title; // If title is empty, try to get it from the sidebar if (!chatTitle) { const existingLink = document.querySelector(`a[href='/c/${chatId}']`); if (existingLink && existingLink.textContent.trim()) { const linkText = existingLink.textContent.trim(); chatTitle = linkText.replace(/[⭐☆]/g, '').trim(); // Update the stored data with the found title GM.setValue(key, { isFavorited: true, title: chatTitle }); } else { chatTitle = `Chat ${chatId}`; } } const li = document.createElement('li'); const link = document.createElement('a'); link.href = `/c/${chatId}`; // Create a span to hold the text for proper truncation const titleSpan = document.createElement('span'); titleSpan.textContent = chatTitle; link.appendChild(titleSpan); li.appendChild(link); ul.appendChild(li); } } favoriteSection.appendChild(ul); } // Initial calls and observation addFavoriteIcon(); displayFavoritedChats(); // Check for dark mode and apply appropriate class function checkDarkMode() { // Different ways to detect dark mode const isDarkMode = document.documentElement.classList.contains('dark') || document.body.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDarkMode) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } // Periodically check for new chat elements, update favorites, and check dark mode setInterval(() => { addFavoriteIcon(); displayFavoritedChats(); checkDarkMode(); }, 2000); // Check every 2 seconds // Initial dark mode check checkDarkMode(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址