chatgpt-export

Add a copy button next to LaTeX formulas

  1. // ==UserScript==
  2. // @name chatgpt-export
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Add a copy button next to LaTeX formulas
  6. // @author You
  7. // @match https://chat.openai.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13. let debounceTimer;
  14.  
  15. const addCopyButtons = () => {
  16. const mathDivs = document.querySelectorAll('.math, .math-inline, .math-display');
  17. const copiedButtonHtml = `<button
  18. class="flex ml-auto gap-2 rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"><svg
  19. stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round"
  20. stroke-linejoin="round" class="icon-sm" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
  21. <polyline points="20 6 9 17 4 12"></polyline>
  22. </svg></button>`
  23. const copyButtonHtml = `<button
  24. class="flex ml-auto gap-2 rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"><svg
  25. stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round"
  26. stroke-linejoin="round" class="icon-sm" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
  27. <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
  28. <rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
  29. </svg></button>`
  30.  
  31. mathDivs.forEach(div => {
  32. if (div.getAttribute('data-copy-button-added') === 'true') return;
  33. div.setAttribute('data-copy-button-added', 'true');
  34. const annotation = div.querySelector('annotation[encoding="application/x-tex"]');
  35. if (annotation) {
  36. let latexText = annotation.textContent;
  37. const copyButton = document.createElement('span');
  38. copyButton.className = 'copy-button text-gray-400 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400';
  39. copyButton.innerHTML = copyButtonHtml;
  40. copyButton.style.verticalAlign = 'middle'; // 确保与行内文本对齐
  41. copyButton.style.display = 'inline-flex'; // 使用 flex 布局保持行内
  42. if (div.classList.contains('math-display')) {
  43. latexText = `$$ ${latexText} $$`
  44. copyButton.style.position = 'absolute';
  45. copyButton.style.top = '0';
  46. copyButton.style.right = '0';
  47. copyButton.style.zIndex = '1';
  48. div.style.position = 'relative';
  49. div.appendChild(copyButton);
  50. } else if (div.classList.contains('math-inline')) {
  51. latexText = `$ ${latexText} $`
  52. copyButton.style.marginLeft = '5px';
  53. div.parentNode.insertBefore(copyButton, div.nextSibling);
  54. }
  55. copyButton.addEventListener('click', () => {
  56. navigator.clipboard.writeText(latexText).then(() => {
  57. copyButton.innerHTML = copiedButtonHtml;
  58. setTimeout(() => {
  59. copyButton.innerHTML = copyButtonHtml;
  60. }, 2000);
  61. }).catch(err => {
  62. console.error('复制失败:', err);
  63. });
  64. });
  65. }
  66. });
  67. };
  68. const debounceAddCopyButtons = () => {
  69. clearTimeout(debounceTimer);
  70. debounceTimer = setTimeout(addCopyButtons, 300);
  71. };
  72.  
  73. addCopyButtons(); // Run once initially
  74.  
  75. const observer = new MutationObserver(() => {
  76. console.log("DOM has changed, rechecking...");
  77. debounceAddCopyButtons();
  78. });
  79.  
  80. observer.observe(document.body, { childList: true, subtree: true });
  81. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址