// ==UserScript==
// @name ChatGPT Conversation Lister
// @namespace https://moukaeritai.work/chatgpt-conversation-lister
// @version 0.7.3.20230810
// @description Retrieves the titles and unique identifiers of conversations in ChatGPT's web interface. Intended for listing and organization purposes only.
// @author Takashi SASAKI https://twitter.com/TakashiSasaki
// @match https://chat.openai.com/
// @match https://chat.openai.com/c/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @license MIT
// ==/UserScript==
/*
## Detailed Description
ChatGPT Conversation Lister is a UserScript designed to get and list the conversations within the ChatGPT's web interface.
Specifically, this UserScript retrieves only the titles and unique identifiers of the chat histories, and it does not obtain or interact with the content of the conversations.
By obtaining the titles and unique identifiers, users can easily keep track of their conversations, arrange them in a systematic manner, and retrieve them as needed,
including pasting them into tools such as Google Sheets or Excel for further organization and analysis.
This tool is intended solely for listing and organization, and it does not alter the conversations in any way.
Importantly, the UserScript gathers information directly from the ChatGPT web pages that the user is viewing, and its execution does not initiate any new communications or network requests.
### How to Use ChatGPT Conversation Lister
1. **Access ChatGPT's web page** with ChatGPT_ConversationLister.user.js installed on your browser.
2. **Open the Tampermonkey or Greasemonkey menu** and you should see the item created by ChatGPT_ConversationLister.user.js.
3. **Choose an option like "List conversations in TSV"** from the menu, and a dialog with the results will be displayed.
4. **Double-click the result** to copy it to the clipboard. Pressing the ESC key will close the dialog.
These steps provide a simple and efficient way to organize and manage your conversations within the ChatGPT interface using the ChatGPT_ConversationLister.user.js script. It ensures the integrity of the conversation content while allowing easy access and retrieval as needed.
### This script is available at
- **GitHub Gist**: [ChatGPT_ConversationLister.user.js](https://gist.github.com/TakashiSasaki/e9d01e569ec339d01bd7d43188b15673)
- **Greasy Fork镜像**: [ChatGPT_ConversationLister.user.js](https://gf.qytechs.cn/ja/scripts/472814)
*/
(function() {
'use strict';
const container = document.createElement('div');
container.id = "tampermonkeyDialogDiv";
document.body.appendChild(container);
const shadowRoot = container.attachShadow({mode: 'open'});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
while(shadowRoot.firstChild){
shadowRoot.removeChild(shadowRoot.firstChild);
}//while
}//if
});
function createDialog(){
// ダイアログ要素を作成
var dialog = document.createElement('div');
dialog.style.cssText = `
position: fixed;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
max-height:80%;
background: white;
padding: 10px;
border: 1px solid black;
z-index: 10000;
border-radius: 15px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
overflow: auto;
`;
var closeButton = document.createElement('button');
closeButton.innerHTML = 'Close';
closeButton.style.cssText = 'position: absolute; top: 0; right: 0; user-select: none';
closeButton.addEventListener('click', function() {
while(shadowRoot.firstChild){
shadowRoot.removeChild(shadowRoot.firstChild);
}//while
});
dialog.appendChild(closeButton);
shadowRoot.appendChild(dialog);
return dialog;
}//createDialog
function createTextarea(){
var textarea = document.createElement('textarea');
textarea.setAttribute("readonly", "readonly");
textarea.style.cssText = `
position: fixed;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
max-height:80%;
background: white;
padding: 10px;
border: 1px solid black;
z-index: 10000;
border-radius: 15px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
overflow: auto;
`;
textarea.addEventListener("dblclick", (event)=>{
event.target.select();
document.execCommand('copy');
});
shadowRoot.appendChild(textarea);
return textarea;
}//createTextarea
var lastScrollTop = 0;
var originalScrollHeight = 0;
var observer = null;
GM_registerMenuCommand("Continuous scrolling on conversation list", ()=>{
const div = document.querySelector("#__next > div.overflow-hidden.w-full.h-full.relative.flex > div.dark.flex-shrink-0.overflow-x-hidden > div > div > div > nav > div.overflow-y-auto");
console.log(div);
const style = window.getComputedStyle(div);
console.log(style);
if(style.overflowY === 'auto') {
console.log(style.overflowY);
if(!observer) {
observer = new MutationObserver((mutationList, observer)=>{
mutationList.forEach(mutation=>{
//if(mutation.target !== div) return;
//console.log(mutation.target);
if(lastScrollTop != div.scrollTop) {
console.log("lastScrollTop", lastScrollTop, "scrollTop", div.scrollTop);
lastScrollTop = div.scrollTop;
setTimeout(()=> {div.scrollTop = div.scrollHeight * 2}, 200);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 3}, 300);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 4}, 400);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 5}, 500);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 6}, 600);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 7}, 700);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 8}, 800);
}//if
});//forEach
});//MutationObserver
}//if
observer.observe(div, {
childList : true,
attributes: true,
subtree: true
});
originalScrollHeight = div.scrollHeight;
console.log("originalScrollHeight", originalScrollHeight);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 2}, 200);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 3}, 300);
setTimeout(()=> {div.scrollTop = div.scrollHeight * 4}, 400);
}//if
});//GM_registerMenuCommand
GM_registerMenuCommand("List conversations in TSV", ()=>{
updateConversationList();
const textarea = createTextarea();
const idArray = GM_listValues();
const tsv = idArray.map( id =>{
const conversation = GM_getValue(id);
return [conversation.id, conversation.title, conversation.projectionId];
});
const tsvSorted = tsv.sort( (a,b) => {
return parseInt(a[2] - parseInt(b[2]));
}//sort
);
const tsvJoined = tsvSorted.map((x)=> x.join("\t"));
textarea.value = tsvJoined.join("\n");
});
function updateConversationList(){
//const div = document.querySelector("#__next div nav div div");
//const div = document.querySelector("#__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.flex-shrink-0.overflow-x-hidden > div > div > div > nav > div.flex-col > div > div");
const div = document.querySelector("#__next > div.overflow-hidden.w-full.h-full.relative.flex > div.dark.flex-shrink-0.overflow-x-hidden > div > div > div > nav > div.overflow-y-auto");
console.log(div);
const liNodes = div.querySelectorAll("li");
console.log(liNodes);
liNodes.forEach((li)=>{
console.log(li);
for (var key in li) {
if (key.startsWith('__reactProps')) {
const id = li[key].children.props.id;
const title = li[key].children.props.title;
const projectionId = li.dataset.projectionId;
console.log(id, title,projectionId);
if(!id) continue;
if(!title) continue;
GM_setValue(id, {id:id, title:title, projectionId:projectionId});
}//if
}//for
});//forEach
}//updateConversationList
// Your code here...
})();