ChatGPT Conversation Lister

Retrieves the titles and unique identifiers of conversations in ChatGPT's web interface. Intended for listing and organization purposes only.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         ChatGPT Conversation Lister
// @namespace    https://moukaeritai.work/chatgpt-conversation-lister
// @version      0.8.3.20231006
// @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==


(function() {
    'use strict';

    const hostDiv = document.createElement('div');
    //container.id = "tampermonkeyDialogDiv";
    document.body.appendChild(hostDiv);
    const shadowRoot = hostDiv.attachShadow({mode: 'open'});
    const containerDiv = document.createElement("div");
    containerDiv.id = "containerDiv";
    shadowRoot.appendChild(containerDiv);

    document.addEventListener('keydown', function(e) {
        console.log(e);
        if (e.key === 'Escape') {
            while(containerDiv.firstChild){
                containerDiv.removeChild(containerDiv.firstChild);
            }//while
        }//if
    });

    function createDialogDiv(){
        // ダイアログ要素を作成
        const dialogDiv = document.createElement('div');
        dialogDiv.id = "dialogDiv";
        dialogDiv.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;
  line-height: 1.7em;
`;
        dialogDiv.innerHTML = `<style>
a:link, a:visited a{
  color: inherit;
  text-decoration: none;
  background-color: #d9ffaf;
  margin-left : 0.5em;
  margin-right: 0.5em;
  margin-top: 0.5em;
  margin-bottom: 0.5em;
  border-radius: 10px;
  padding: 1px;
  padding-left: 6px;
  padding-right: 6px;
  box-shadow: 2px 2px 3px rgba(0,0,0,0.4);
</style>`;

        containerDiv.appendChild(dialogDiv);
        return dialogDiv;
    }//createDialogDiv

    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');
        });
        containerDiv.appendChild(textarea);
        return textarea;
    }//createTextarea

    const selector20230827 = "#__next > div > div > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div > div > div > div > nav > div.flex-col > div > div";
    const selector20230828 = "#__next > div > div > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div > div > div > div > nav > div.flex-col.overflow-y-auto";
    const selector20230829 = "#__next > div.overflow-hidden.w-full.h-full > div > div > div > div > nav > div.overflow-y-auto";
    const conversationListDivErrorMessage = "Unable to retrieve the conversation list. This may be due to changes in the DOM structure of ChatGPT. Please await updates to the ChatGPT Conversation Lister.";
    const conversationListDiv = selector20230829;

    var lastScrollTop = 0;
    var originalScrollHeight = 0;
    var observer = null;
    GM_registerMenuCommand("Continuous scrolling on conversation list", ()=>{
        const div = document.querySelector(conversationListDiv);
        if(!div){
            alert(conversationListDivErrorMessage);
            return;
        }//if
        const style = window.getComputedStyle(div);
        console.log(style);
        if(style.overflowY === 'auto'|| style.overflowY === 'visible') {
            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}, 500);
                            setTimeout(()=> {div.scrollTop = div.scrollHeight}, 1000);
                            setTimeout(()=> {div.scrollTop = div.scrollHeight}, 2000);
                            setTimeout(()=> {div.scrollTop = div.scrollHeight}, 4000);
                            setTimeout(()=> {div.scrollTop = div.scrollHeight}, 8000);
                            updateConversationList();
                        }//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("Search in titles of conversations", searchForTitle);

    const searchForTitleButton = document.createElement("img");
    searchForTitleButton.setAttribute("src", "https://moukaeritai-static.glitch.me/svg/search-in-title-icon.svg");
    searchForTitleButton.style.background="lightyellow";
    searchForTitleButton.addEventListener("click", searchForTitle);
    document.querySelector("nav a.flex").insertAdjacentElement("afterend", searchForTitleButton);

    function searchForTitle (){
        updateConversationList();
        const dialogDiv = createDialogDiv();
        const input = document.createElement("input");
        //input.style.position = "absolute";
        input.addEventListener("keyup", event => {
            setTimeout(()=>{
                const nodeList = dialogDiv.querySelectorAll("a");
                for(var i=0; i<nodeList.length; ++i){
                    if(nodeList[i].textContent.toLowerCase().indexOf(input.value.toLowerCase()) == -1){
                        nodeList[i].style.display = "none";
                    } else {
                        nodeList[i].style.display = "";
                    }//if
                }//for
            }, 0); //setTimeout
        }); //addEventListener
        dialogDiv.appendChild(input);

        const idArray = GM_listValues();
        const objects = idArray.map( id=>GM_getValue(id));
        objects.sort( (a,b) => a.projectionId - b.projectionId);
        objects.forEach( object =>{
            const a = document.createElement("a");
            a.setAttribute("href", "https://chat.openai.com/c/" + object.id);
            a.innerText = object.title;
            dialogDiv.appendChild(a);
        });//forEach
    }//searchForTitle

    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];
        });//map
        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");
    });//GM_registerMenuCommand

    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 selector20230810 = "#__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";
        const div = document.querySelector(conversationListDiv);
        if(!div){
            alert(conversationListDivErrorMessage);
            return;
        }
        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...
})();