Adds a button to copy ChatGPT responses with em dashes replaced by regular dashes
Size
6.5 KB
Version
1.0.1
Created
Oct 21, 2025
Updated
1 day ago
1// ==UserScript==
2// @name ChatGPT Copy Without Em Dash
3// @description Adds a button to copy ChatGPT responses with em dashes replaced by regular dashes
4// @version 1.0.1
5// @match https://*.chatgpt.com/*
6// @icon https://cdn.oaistatic.com/assets/favicon-l4nq08hd.svg
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('ChatGPT Copy Without Em Dash extension loaded');
12
13 // Function to create the custom copy button
14 function createCopyButton() {
15 const button = document.createElement('button');
16 button.className = 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg';
17 button.setAttribute('aria-label', 'Copy without em dash');
18 button.setAttribute('data-state', 'closed');
19 button.title = 'Copy without em dash';
20
21 button.innerHTML = `
22 <span class="flex items-center justify-center touch:w-10 h-8 w-8">
23 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">
24 <path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z" fill="currentColor"></path>
25 <line x1="6" y1="12" x2="13" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
26 </svg>
27 </span>
28 `;
29
30 return button;
31 }
32
33 // Function to copy text without em dashes
34 async function copyWithoutEmDash(messageElement) {
35 try {
36 // Get the text content from the message
37 const textContent = messageElement.textContent || messageElement.innerText;
38
39 // Replace em dashes (—) with regular hyphens (-)
40 const cleanedText = textContent.replace(/—/g, '-');
41
42 // Copy to clipboard
43 await navigator.clipboard.writeText(cleanedText);
44
45 console.log('Text copied without em dashes:', cleanedText);
46
47 // Visual feedback
48 return true;
49 } catch (error) {
50 console.error('Failed to copy text:', error);
51 return false;
52 }
53 }
54
55 // Function to add button to a message
56 function addButtonToMessage(article) {
57 // Check if button already exists
58 if (article.querySelector('[aria-label="Copy without em dash"]')) {
59 return;
60 }
61
62 // Find the action buttons container
63 const actionsContainer = article.querySelector('[data-testid="copy-turn-action-button"]')?.parentElement;
64
65 if (!actionsContainer) {
66 console.log('Actions container not found for message');
67 return;
68 }
69
70 // Find the message content
71 const messageContent = article.querySelector('[data-message-author-role="assistant"] .markdown');
72
73 if (!messageContent) {
74 console.log('Message content not found');
75 return;
76 }
77
78 // Create and add the button
79 const copyButton = createCopyButton();
80
81 // Add click handler
82 copyButton.addEventListener('click', async (e) => {
83 e.preventDefault();
84 e.stopPropagation();
85
86 const success = await copyWithoutEmDash(messageContent);
87
88 if (success) {
89 // Visual feedback - change button appearance briefly
90 const originalHTML = copyButton.innerHTML;
91 copyButton.innerHTML = `
92 <span class="flex items-center justify-center touch:w-10 h-8 w-8">
93 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">
94 <path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
95 </svg>
96 </span>
97 `;
98
99 setTimeout(() => {
100 copyButton.innerHTML = originalHTML;
101 }, 1500);
102 }
103 });
104
105 // Insert the button after the regular copy button
106 const regularCopyButton = actionsContainer.querySelector('[data-testid="copy-turn-action-button"]');
107 if (regularCopyButton) {
108 regularCopyButton.parentNode.insertBefore(copyButton, regularCopyButton.nextSibling);
109 console.log('Copy without em dash button added to message');
110 }
111 }
112
113 // Function to process all messages
114 function processMessages() {
115 const messages = document.querySelectorAll('article[data-testid^="conversation-turn-"]');
116 console.log(`Found ${messages.length} messages to process`);
117
118 messages.forEach(article => {
119 // Only process assistant messages
120 if (article.querySelector('[data-message-author-role="assistant"]')) {
121 addButtonToMessage(article);
122 }
123 });
124 }
125
126 // Debounce function to avoid excessive processing
127 function debounce(func, wait) {
128 let timeout;
129 return function executedFunction(...args) {
130 const later = () => {
131 clearTimeout(timeout);
132 func(...args);
133 };
134 clearTimeout(timeout);
135 timeout = setTimeout(later, wait);
136 };
137 }
138
139 // Observe DOM changes to add buttons to new messages
140 function observeMessages() {
141 const observer = new MutationObserver(debounce(() => {
142 processMessages();
143 }, 300));
144
145 observer.observe(document.body, {
146 childList: true,
147 subtree: true
148 });
149
150 console.log('MutationObserver started');
151 }
152
153 // Initialize the extension
154 function init() {
155 console.log('Initializing ChatGPT Copy Without Em Dash extension');
156
157 // Process existing messages
158 processMessages();
159
160 // Start observing for new messages
161 observeMessages();
162 }
163
164 // Wait for the page to be ready
165 if (document.readyState === 'loading') {
166 document.addEventListener('DOMContentLoaded', init);
167 } else {
168 init();
169 }
170})();