Tools: Archiving ChatGPT Conversations with a Bookmarklet

Tools: Archiving ChatGPT Conversations with a Bookmarklet

Source: Dev.to

Overview ## Authentication Handling ## Archiving Logic ## Collecting Conversation Data ## Overlay User Interface ## Logging and Feedback ## Cleanup This script is a JavaScript bookmarklet designed to help users archive multiple ChatGPT conversations directly from the history page. It runs entirely in the browser and adds a temporary overlay interface on top of the existing page. The bookmarklet scans the conversation history list, extracts conversation IDs and titles, and presents them in a selectable UI. Users can choose multiple conversations and archive them in one action. Throughout the process, logs are written both to the browser console and to a log panel in the overlay. The script retrieves an access token from the page’s embedded client-bootstrap element. This token is required to authenticate requests to the ChatGPT backend API. If the token cannot be found, the script stops and logs an error. A dedicated archiveConversation function sends a PATCH request to the ChatGPT backend API for a given conversation ID. The request marks the conversation as archived and checks the response for success. Each request is processed sequentially, and the result is logged for visibility. The script queries the DOM for conversation links in the history sidebar. Each link’s URL is parsed to extract the conversation ID, while the visible text is used as the conversation title. These values are stored and later displayed in the UI. An overlay covers the current page and displays a centered panel. This panel contains: All buttons are explicitly styled to resemble default browser buttons, ensuring they remain clearly visible even if the page’s CSS overrides standard styles. The script includes a logging utility that writes messages to both the console and the on-screen log panel. This makes it easy to follow what the script is doing, track progress, and identify errors during execution. Once the archiving process finishes, or if the user closes the interface manually, the overlay is removed from the page. No permanent changes are made to the page structure beyond the archived conversations themselves. This bookmarklet provides a lightweight, practical way to manage and archive ChatGPT conversations efficiently using only client-side JavaScript. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: javascript:(async () => { /* ============================= * Logging utility * ============================= */ let logContainer = null; function log(...args) { console.log("[Archive Bookmarklet]", ...args); if (logContainer) { const line = document.createElement("div"); line.innerText = args.map(String).join(" "); logContainer.appendChild(line); logContainer.scrollTop = logContainer.scrollHeight; } } log("start"); /* ============================= * Load accessToken * ============================= */ const bootstrapEl = document.getElementById("client-bootstrap"); if (!bootstrapEl) { log("client-bootstrap not found"); return; } const accessToken = JSON.parse(bootstrapEl.innerHTML).session.accessToken; log("accessToken loaded"); /* ============================= * archiveConversation * ============================= */ async function archiveConversation(conversationId) { log("archive start:", conversationId); const response = await fetch( "https://chatgpt.com/backend-api/conversation/" + conversationId, { headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, body: JSON.stringify({ is_archived: true }), method: "PATCH", mode: "cors", credentials: "include" } ); if (!response.ok) { log("HTTP error:", response.status); return false; } const result = await response.json(); log("result:", JSON.stringify(result)); return result.success === true; } /* ============================= * Collect history links * ============================= */ const links = Array.from( document.querySelectorAll("#history > a") ); log("links found:", links.length); const conversations = links .map((a) => { const match = a .getAttribute("href") ?.match(/^\/c\/(.+)$/); if (!match) return null; return { conversationId: match[1], title: a.innerText.trim() }; }) .filter(Boolean); log("conversations parsed:", conversations.length); /* ============================= * Normalize button style * (override page CSS) * ============================= */ function normalizeButtonStyle(btn) { btn.style.all = "revert"; btn.style.font = "inherit"; btn.style.padding = "6px 12px"; btn.style.border = "1px solid #888"; btn.style.borderRadius = "4px"; btn.style.background = "#eee"; btn.style.color = "#000"; btn.style.cursor = "pointer"; } /* ============================= * Overlay UI * ============================= */ const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(0,0,0,0.6)"; overlay.style.zIndex = "99999"; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; const panel = document.createElement("div"); panel.style.background = "#fff"; panel.style.padding = "16px"; panel.style.width = "640px"; panel.style.maxHeight = "80%"; panel.style.overflow = "auto"; panel.style.borderRadius = "8px"; panel.style.fontSize = "14px"; panel.style.position = "relative"; /* ---- Close button ---- */ const closeButton = document.createElement("button"); closeButton.innerText = "×"; normalizeButtonStyle(closeButton); closeButton.style.position = "absolute"; closeButton.style.top = "8px"; closeButton.style.right = "8px"; closeButton.onclick = () => { log("UI closed"); overlay.remove(); }; panel.appendChild(closeButton); /* ---- Title ---- */ const title = document.createElement("h2"); title.innerText = "Archive Conversations"; panel.appendChild(title); /* ---- Conversation list ---- */ const list = document.createElement("div"); conversations.forEach((c) => { const label = document.createElement("label"); label.style.display = "block"; label.style.marginBottom = "6px"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.conversationId = c.conversationId; label.appendChild(checkbox); label.appendChild( document.createTextNode(" " + c.title) ); list.appendChild(label); }); panel.appendChild(list); /* ---- Archive button ---- */ const archiveButton = document.createElement("button"); archiveButton.innerText = "Archive selected"; normalizeButtonStyle(archiveButton); archiveButton.style.marginTop = "12px"; archiveButton.onclick = async () => { const checked = Array.from( panel.querySelectorAll("input[type=checkbox]:checked") ); log("selected:", checked.length); for (const cb of checked) { const id = cb.dataset.conversationId; const ok = await archiveConversation(id); log(id, "->", ok); } log("done"); overlay.remove(); }; panel.appendChild(archiveButton); /* ---- Log output ---- */ const logTitle = document.createElement("h3"); logTitle.innerText = "Log"; logTitle.style.marginTop = "16px"; panel.appendChild(logTitle); logContainer = document.createElement("div"); logContainer.style.background = "#f5f5f5"; logContainer.style.padding = "8px"; logContainer.style.height = "120px"; logContainer.style.overflow = "auto"; logContainer.style.fontFamily = "monospace"; logContainer.style.fontSize = "12px"; panel.appendChild(logContainer); overlay.appendChild(panel); document.body.appendChild(overlay); log("UI rendered"); })(); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: javascript:(async () => { /* ============================= * Logging utility * ============================= */ let logContainer = null; function log(...args) { console.log("[Archive Bookmarklet]", ...args); if (logContainer) { const line = document.createElement("div"); line.innerText = args.map(String).join(" "); logContainer.appendChild(line); logContainer.scrollTop = logContainer.scrollHeight; } } log("start"); /* ============================= * Load accessToken * ============================= */ const bootstrapEl = document.getElementById("client-bootstrap"); if (!bootstrapEl) { log("client-bootstrap not found"); return; } const accessToken = JSON.parse(bootstrapEl.innerHTML).session.accessToken; log("accessToken loaded"); /* ============================= * archiveConversation * ============================= */ async function archiveConversation(conversationId) { log("archive start:", conversationId); const response = await fetch( "https://chatgpt.com/backend-api/conversation/" + conversationId, { headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, body: JSON.stringify({ is_archived: true }), method: "PATCH", mode: "cors", credentials: "include" } ); if (!response.ok) { log("HTTP error:", response.status); return false; } const result = await response.json(); log("result:", JSON.stringify(result)); return result.success === true; } /* ============================= * Collect history links * ============================= */ const links = Array.from( document.querySelectorAll("#history > a") ); log("links found:", links.length); const conversations = links .map((a) => { const match = a .getAttribute("href") ?.match(/^\/c\/(.+)$/); if (!match) return null; return { conversationId: match[1], title: a.innerText.trim() }; }) .filter(Boolean); log("conversations parsed:", conversations.length); /* ============================= * Normalize button style * (override page CSS) * ============================= */ function normalizeButtonStyle(btn) { btn.style.all = "revert"; btn.style.font = "inherit"; btn.style.padding = "6px 12px"; btn.style.border = "1px solid #888"; btn.style.borderRadius = "4px"; btn.style.background = "#eee"; btn.style.color = "#000"; btn.style.cursor = "pointer"; } /* ============================= * Overlay UI * ============================= */ const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(0,0,0,0.6)"; overlay.style.zIndex = "99999"; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; const panel = document.createElement("div"); panel.style.background = "#fff"; panel.style.padding = "16px"; panel.style.width = "640px"; panel.style.maxHeight = "80%"; panel.style.overflow = "auto"; panel.style.borderRadius = "8px"; panel.style.fontSize = "14px"; panel.style.position = "relative"; /* ---- Close button ---- */ const closeButton = document.createElement("button"); closeButton.innerText = "×"; normalizeButtonStyle(closeButton); closeButton.style.position = "absolute"; closeButton.style.top = "8px"; closeButton.style.right = "8px"; closeButton.onclick = () => { log("UI closed"); overlay.remove(); }; panel.appendChild(closeButton); /* ---- Title ---- */ const title = document.createElement("h2"); title.innerText = "Archive Conversations"; panel.appendChild(title); /* ---- Conversation list ---- */ const list = document.createElement("div"); conversations.forEach((c) => { const label = document.createElement("label"); label.style.display = "block"; label.style.marginBottom = "6px"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.conversationId = c.conversationId; label.appendChild(checkbox); label.appendChild( document.createTextNode(" " + c.title) ); list.appendChild(label); }); panel.appendChild(list); /* ---- Archive button ---- */ const archiveButton = document.createElement("button"); archiveButton.innerText = "Archive selected"; normalizeButtonStyle(archiveButton); archiveButton.style.marginTop = "12px"; archiveButton.onclick = async () => { const checked = Array.from( panel.querySelectorAll("input[type=checkbox]:checked") ); log("selected:", checked.length); for (const cb of checked) { const id = cb.dataset.conversationId; const ok = await archiveConversation(id); log(id, "->", ok); } log("done"); overlay.remove(); }; panel.appendChild(archiveButton); /* ---- Log output ---- */ const logTitle = document.createElement("h3"); logTitle.innerText = "Log"; logTitle.style.marginTop = "16px"; panel.appendChild(logTitle); logContainer = document.createElement("div"); logContainer.style.background = "#f5f5f5"; logContainer.style.padding = "8px"; logContainer.style.height = "120px"; logContainer.style.overflow = "auto"; logContainer.style.fontFamily = "monospace"; logContainer.style.fontSize = "12px"; panel.appendChild(logContainer); overlay.appendChild(panel); document.body.appendChild(overlay); log("UI rendered"); })(); COMMAND_BLOCK: javascript:(async () => { /* ============================= * Logging utility * ============================= */ let logContainer = null; function log(...args) { console.log("[Archive Bookmarklet]", ...args); if (logContainer) { const line = document.createElement("div"); line.innerText = args.map(String).join(" "); logContainer.appendChild(line); logContainer.scrollTop = logContainer.scrollHeight; } } log("start"); /* ============================= * Load accessToken * ============================= */ const bootstrapEl = document.getElementById("client-bootstrap"); if (!bootstrapEl) { log("client-bootstrap not found"); return; } const accessToken = JSON.parse(bootstrapEl.innerHTML).session.accessToken; log("accessToken loaded"); /* ============================= * archiveConversation * ============================= */ async function archiveConversation(conversationId) { log("archive start:", conversationId); const response = await fetch( "https://chatgpt.com/backend-api/conversation/" + conversationId, { headers: { authorization: "Bearer " + accessToken, "content-type": "application/json" }, body: JSON.stringify({ is_archived: true }), method: "PATCH", mode: "cors", credentials: "include" } ); if (!response.ok) { log("HTTP error:", response.status); return false; } const result = await response.json(); log("result:", JSON.stringify(result)); return result.success === true; } /* ============================= * Collect history links * ============================= */ const links = Array.from( document.querySelectorAll("#history > a") ); log("links found:", links.length); const conversations = links .map((a) => { const match = a .getAttribute("href") ?.match(/^\/c\/(.+)$/); if (!match) return null; return { conversationId: match[1], title: a.innerText.trim() }; }) .filter(Boolean); log("conversations parsed:", conversations.length); /* ============================= * Normalize button style * (override page CSS) * ============================= */ function normalizeButtonStyle(btn) { btn.style.all = "revert"; btn.style.font = "inherit"; btn.style.padding = "6px 12px"; btn.style.border = "1px solid #888"; btn.style.borderRadius = "4px"; btn.style.background = "#eee"; btn.style.color = "#000"; btn.style.cursor = "pointer"; } /* ============================= * Overlay UI * ============================= */ const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(0,0,0,0.6)"; overlay.style.zIndex = "99999"; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; const panel = document.createElement("div"); panel.style.background = "#fff"; panel.style.padding = "16px"; panel.style.width = "640px"; panel.style.maxHeight = "80%"; panel.style.overflow = "auto"; panel.style.borderRadius = "8px"; panel.style.fontSize = "14px"; panel.style.position = "relative"; /* ---- Close button ---- */ const closeButton = document.createElement("button"); closeButton.innerText = "×"; normalizeButtonStyle(closeButton); closeButton.style.position = "absolute"; closeButton.style.top = "8px"; closeButton.style.right = "8px"; closeButton.onclick = () => { log("UI closed"); overlay.remove(); }; panel.appendChild(closeButton); /* ---- Title ---- */ const title = document.createElement("h2"); title.innerText = "Archive Conversations"; panel.appendChild(title); /* ---- Conversation list ---- */ const list = document.createElement("div"); conversations.forEach((c) => { const label = document.createElement("label"); label.style.display = "block"; label.style.marginBottom = "6px"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.conversationId = c.conversationId; label.appendChild(checkbox); label.appendChild( document.createTextNode(" " + c.title) ); list.appendChild(label); }); panel.appendChild(list); /* ---- Archive button ---- */ const archiveButton = document.createElement("button"); archiveButton.innerText = "Archive selected"; normalizeButtonStyle(archiveButton); archiveButton.style.marginTop = "12px"; archiveButton.onclick = async () => { const checked = Array.from( panel.querySelectorAll("input[type=checkbox]:checked") ); log("selected:", checked.length); for (const cb of checked) { const id = cb.dataset.conversationId; const ok = await archiveConversation(id); log(id, "->", ok); } log("done"); overlay.remove(); }; panel.appendChild(archiveButton); /* ---- Log output ---- */ const logTitle = document.createElement("h3"); logTitle.innerText = "Log"; logTitle.style.marginTop = "16px"; panel.appendChild(logTitle); logContainer = document.createElement("div"); logContainer.style.background = "#f5f5f5"; logContainer.style.padding = "8px"; logContainer.style.height = "120px"; logContainer.style.overflow = "auto"; logContainer.style.fontFamily = "monospace"; logContainer.style.fontSize = "12px"; panel.appendChild(logContainer); overlay.appendChild(panel); document.body.appendChild(overlay); log("UI rendered"); })(); - A close button to dismiss the UI without taking action - A list of conversations with checkboxes for selection - An “Archive selected” button to start the archiving process - A log area showing real-time status messages