Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ My user scripts to add functionality to various sites around the web (that were
- [Freshdesk Make 'Redactor Editor' Resizeable](./userscripts/freshdesk-make-redactor-editor-resizeable/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/freshdesk-make-redactor-editor-resizeable/freshdesk-make-redactor-editor-resizeable.user.js))
- [GitHub Gist - CodeMirror Resizer](./userscripts/github-gist-codemirror-resizer/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/github-gist-codemirror-resizer/github-gist-codemirror-resizer.user.js))
- [GitHub Notifications - Arrow key navigation](./userscripts/github-notifications-arrow-key-navigation/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/github-notifications-arrow-key-navigation/github-notifications-arrow-key-navigation.user.js))
- [ChatGPT Memories Copy Button](./userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js))
- [Google developer sites - Ensure ?authuser= by default](./userscripts/google-developer-sites-ensure-authuser/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/google-developer-sites-ensure-authuser/google-developer-sites-ensure-authuser.user.js))
- [Nourishd Meal Highlighter](./userscripts/nourishd-meal-highlighter/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/nourishd-meal-highlighter/nourishd-meal-highlighter.user.js))
- [YouTube Speed Override](./userscripts/youtube-speed-override/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/youtube-speed-override/youtube-speed-override.user.js))
Expand Down
16 changes: 16 additions & 0 deletions userscripts/chatgpt-memories-copy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ChatGPT Memories Copy Button

```javascript
// ==UserScript==
// @name ChatGPT Memories Copy Button
// @namespace https://www.devalias.net/
// @version 0.4
// @description Add a "copy" button next to the trash icon for ChatGPT user memories to copy memory text easily.
// @author Glenn 'devalias' Grant
// @match https://chatgpt.com/c/*#settings/Personalization*
// @match https://chat.openai.com/c/*#settings/Personalization*
// @icon https://cdn.oaistatic.com/assets/favicon-eex17e9e.ico
// ==/UserScript==
```

- [Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js)
100 changes: 100 additions & 0 deletions userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// ==UserScript==
// @name ChatGPT Memories Copy Button
// @namespace https://www.devalias.net/
// @version 0.4
// @description Add a "copy" button next to the trash icon for ChatGPT user memories to copy memory text easily.
// @author Glenn 'devalias' Grant
// @match https://chatgpt.com/c/*#settings/Personalization*
// @match https://chat.openai.com/c/*#settings/Personalization*
// @icon https://cdn.oaistatic.com/assets/favicon-eex17e9e.ico
// ==/UserScript==

(function() {
'use strict';

function handleCopyClick(textCell, btn) {
const text = textCell.textContent.trim();
navigator.clipboard.writeText(text).then(() => {
// Visual feedback by temporarily changing color
btn.classList.add('text-token-text-secondary');
setTimeout(() => btn.classList.remove('text-token-text-secondary'), 800);
});
}

function createCopyButton(textCell) {
const btn = document.createElement('button');
btn.type = 'button';
btn.ariaLabel = 'Copy';
btn.className = 'text-token-text-tertiary hover:text-token-text-secondary';
btn.innerHTML = `
<span class="leading-none">
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" class="icon-sm">
<path d="M6 2a2 2 0 0 0-2 2v10h2V4h8V2H6zm2 4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0 2h8v10H8V8z"/>
</svg>
</span>`;
btn.addEventListener('click', () => handleCopyClick(textCell, btn)); // explicit btn arg
return btn;
}

function injectCopyButtons() {
document.querySelectorAll('[data-testid="modal-memories"] table tbody tr').forEach(row => {
const textCell = row.querySelector('.text-left');
const actionCell = row.querySelector('.text-right');
if (!textCell || !actionCell) return;
if (actionCell.querySelector('button[aria-label="Copy"]')) return; // already added

const copyBtn = createCopyButton(textCell);

const trashBtn = actionCell.querySelector('button[aria-label="Remove"]');
if (trashBtn) {
actionCell.insertBefore(copyBtn, trashBtn);
} else {
// fallback: put it first so it precedes any other future actions
actionCell.prepend(copyBtn);
}
});
}

const memoriesModalTbody = document.querySelector('[data-testid="modal-memories"] table tbody');

if (memoriesModalTbody) {
const observer = new MutationObserver(mutations => {
// MutationRecord structure reference:
// {
// type: "childList" | "attributes" | "characterData",
// target: Node, // node that changed
// addedNodes: NodeList, // newly added children
// removedNodes: NodeList, // removed children
// previousSibling: Node | null,
// nextSibling: Node | null,
// attributeName: string | null,
// attributeNamespace: string | null,
// oldValue: string | null
// }

for (const m of mutations) {
// NOTE: could also use mutations.find(...) for conciseness, but loop is clearer
if (
m.type === "childList" &&
([...m.addedNodes, ...m.removedNodes].some(n => n.nodeName === "TR"))
) {
// Debug logging to help refine filtering:
// console.debug("Relevant mutation:", m);

injectCopyButtons();
break; // stop after first relevant
}
}
});

observer.observe(memoriesModalTbody, { childList: true });
// ⚠️ If the modal isn't yet present when this script runs,
// we may need to observe a higher-level container until it appears.
} else {
// Fallback: tbody not found at script init.
// Might need to retry later or observe the modal root itself.
// console.warn("Memories modal tbody not found; observer not started");
}

injectCopyButtons();
})();