ChatGPT LaTeX Auto Render (OpenAI, you, bing, etc.)

自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。

当前为 2023-02-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT LaTeX Auto Render (OpenAI, you, bing, etc.)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4.4
  5. // @author Scruel
  6. // @homepage https://github.com/scruel/tampermonkey-scripts
  7. // @description Auto typeset LaTeX math formulas on ChatGPT pages (OpenAI, new bing, you, etc.).
  8. // @description:zh-CN 自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。
  9. // @match https://chat.openai.com/*
  10. // @match https://you.com/search?*&tbm=youchat*
  11. // @match https://www.bing.com/search?*
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. 'use strict';
  16.  
  17. const _parsed_mark = '_sc_parsed';
  18.  
  19. function queryAddNoParsed(query) {
  20. return query + ":not([" + _parsed_mark + "])";
  21. }
  22.  
  23. function prepareScript() {
  24. window._sc_afterTypesetElement = (element) => {};
  25. window._sc_typeset = () => {
  26. if (window._sc_typesetting) {
  27. return;
  28. }
  29. window._sc_typesetting = true;
  30. try {
  31. const messages = window._sc_getMessages();
  32. messages.forEach((element) => {
  33. element.setAttribute(_parsed_mark,'');
  34. MathJax.typesetPromise([element]);
  35. window._sc_afterTypesetElement(element);
  36. });
  37. } catch (ignore) {
  38. }
  39. window._sc_typesetting = false;
  40. }
  41.  
  42. const commonCheck = (query) => {
  43. const submitButton = document.querySelector(query);
  44. return submitButton && !submitButton.disabled;
  45. }
  46.  
  47. if (window.location.host == "www.bing.com") {
  48. window._sc_getMessages = () => {
  49. const elements = [];
  50. const allChatTurn = document.querySelector("#b_sydConvCont > cib-serp").shadowRoot.querySelector("#cib-conversation-main").shadowRoot.querySelectorAll("#cib-chat-main > cib-chat-turn");
  51. if (allChatTurn.length == 0) {
  52. return;
  53. }
  54. const allCibMeg = allChatTurn[allChatTurn.length - 1].shadowRoot.querySelector("cib-message-group.response-message-group").shadowRoot.querySelectorAll("cib-message");
  55. allCibMeg.forEach((cibMeg) => {
  56. const element = cibMeg.shadowRoot.querySelector(queryAddNoParsed("cib-shared"));
  57. if (element) {
  58. elements.append(element);
  59. }
  60. });
  61. return elements;
  62. }
  63. window._sc_isAnswerPrepared = () => {
  64. const actionBarParent = document.querySelector("#b_sydConvCont > cib-serp");
  65. if (!actionBarParent) {
  66. return false;
  67. }
  68. const actionBar = actionBarParent.shadowRoot.querySelector("#cib-action-bar-main");
  69. return actionBar && !actionBar.hasAttribute('cancelable');
  70. }
  71. }
  72. else if (window.location.host == "you.com") {
  73. window._sc_getMessages = () => {
  74. return document.querySelectorAll(queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]'));
  75. }
  76. window._sc_isAnswerPrepared = () => {
  77. return commonCheck('main div[data-testid="youchat-input"] textarea+button');
  78. }
  79. }
  80. else if (window.location.host == "chat.openai.com") {
  81. window._sc_getMessages = () => {
  82. return document.querySelectorAll(queryAddNoParsed("div.w-full div.text-base div.items-start"));
  83. }
  84. window._sc_afterTypesetElement = (element) => { element.style.display = 'unset';}
  85. window._sc_isAnswerPrepared = () => {
  86. return commonCheck('main form textarea+button');
  87. }
  88. }
  89. }
  90.  
  91. function renderTrigger() {
  92. setTimeout(renderLatex, window.renderDelay);
  93. }
  94.  
  95. function renderLatex() {
  96. if (window._sc_isAnswerPrepared()) {
  97. // console.log("Rendering...")
  98. window._sc_typeset();
  99. }
  100. renderTrigger();
  101. }
  102.  
  103. async function addScript(url) {
  104. const scriptElement = document.createElement('script');
  105. const headElement = document.getElementsByTagName('head')[0] || document.documentElement;
  106. if (!headElement.appendChild(scriptElement)) {
  107. // Prevent appendChild overwritten problem.
  108. headElement.append(scriptElement);
  109. }
  110. scriptElement.src = url;
  111.  
  112. await waitScriptLoaded();
  113. }
  114.  
  115. function waitScriptLoaded() {
  116. return new Promise(async (resolve, reject) => {
  117. const resolver = () => {
  118. if (MathJax.hasOwnProperty('typeset')) {
  119. resolve();
  120. return;
  121. }
  122. if (window._sc_ChatLatex.loadCount > 100) {
  123. setTipsElementText("Failed to load MathJax, try refresh.");
  124. reject("Failed to load MathJax, try refresh.");
  125. return;
  126. }
  127. window._sc_ChatLatex.loadCount += 1;
  128. window.setTimeout(resolver, 200);
  129. }
  130. resolver();
  131. });
  132. }
  133.  
  134. function setTipsElementText(text) {
  135. window._sc_ChatLatex.tipsElement.innerHTML = text;
  136. }
  137.  
  138. function showTipsElement() {
  139. const tipsElement = window._sc_ChatLatex.tipsElement;
  140. tipsElement.style.position = "fixed";
  141. tipsElement.style.left = "10px";
  142. tipsElement.style.bottom = "10px";
  143. tipsElement.style.background = '#333';
  144. tipsElement.style.color = '#fff';
  145. document.body.appendChild(tipsElement);
  146. }
  147.  
  148. function hideTipsElement(timeout=3) {
  149. window.setTimeout(() => {window._sc_ChatLatex.tipsElement.hidden=true}, 3000);
  150. }
  151.  
  152. (async function() {
  153. window._sc_ChatLatex = {
  154. tipsElement: document.createElement("div"),
  155. loadCount: 0
  156. }
  157. window.MathJax = {
  158. tex: {
  159. inlineMath: [['$', '$'], ['\\(', '\\)']],
  160. displayMath : [['$$', '$$']]
  161. }
  162. };
  163.  
  164. showTipsElement();
  165. setTipsElementText("Loading MathJax...");
  166. await addScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js');
  167. setTipsElementText("MathJax Loaded.");
  168. hideTipsElement();
  169.  
  170. prepareScript();
  171. window.renderDelay = 1000;
  172. renderTrigger();
  173. })();