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

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

当前为 2023-03-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT LaTeX Auto Render (OpenAI, you, new bing, etc.)
  3. // @version 0.5.3
  4. // @author Scruel Tao
  5. // @homepage https://github.com/scruel/tampermonkey-scripts
  6. // @description Auto typeset LaTeX math formulas on ChatGPT pages (OpenAI, new bing, you, etc.).
  7. // @description:zh-CN 自动渲染 ChatGPT 页面 (OpenAI, new bing, you 等) 上的 LaTeX 数学公式。
  8. // @match https://chat.openai.com/*
  9. // @match https://you.com/search?*&tbm=youchat*
  10. // @match https://www.bing.com/search?*
  11. // @namespace http://tampermonkey.net/
  12. // @icon https://chat.openai.com/favicon.ico
  13. // @grant none
  14. // @noframes
  15. // ==/UserScript==
  16.  
  17. 'use strict';
  18.  
  19. const _parsed_mark = '_sc_parsed';
  20. function queryAddNoParsed(query) {
  21. return query + ":not([" + _parsed_mark + "])";
  22. }
  23.  
  24. async function prepareScript() {
  25. window._sc_beforeTypesetMsg = (msg) => {};
  26. window._sc_typesetAfter = (element) => {};
  27. window._sc_typeset = () => {
  28. try {
  29. const messages = window._sc_getMsgEles();
  30. messages.forEach(msg => {
  31. msg.setAttribute(_parsed_mark,'');
  32. window._sc_beforeTypesetMsg(msg);
  33. MathJax.typesetPromise([msg]);
  34. window._sc_typesetAfter(msg);
  35. });
  36. } catch (e) {
  37. console.warn(e);
  38. }
  39. }
  40. window._sc_mutationHandler = (mutation) => {
  41. if (mutation.oldValue === '') {
  42. window._sc_typeset();
  43. }
  44. };
  45. window._sc_chatLoaded = () => { return true; };
  46.  
  47. var afterMainOvservationStart = () => {window._sc_typeset();};
  48.  
  49. if (window.location.host == "www.bing.com") {
  50. window._sc_getObserveElement = () => {
  51. const ele = document.querySelector("#b_sydConvCont > cib-serp");
  52. if (!ele) {return null;}
  53. return ele.shadowRoot.querySelector("#cib-action-bar-main");
  54. }
  55. const getContMsgEles = (cont, isInChat=true) => {
  56. const allChatTurn = cont.shadowRoot.querySelector("#cib-conversation-main").shadowRoot.querySelectorAll("cib-chat-turn");
  57. var lastChatTurnSR = allChatTurn[allChatTurn.length - 1];
  58. if (isInChat) { lastChatTurnSR = lastChatTurnSR.shadowRoot; }
  59. const allCibMsgGroup = lastChatTurnSR.querySelectorAll("cib-message-group");
  60. const allCibMsg = Array.from(allCibMsgGroup).map(e => Array.from(e.shadowRoot.querySelectorAll("cib-message"))).flatMap(e => e);
  61. return Array.from(allCibMsg).map(cibMsg => cibMsg.shadowRoot.querySelector("cib-shared")).filter(e => e);
  62. }
  63. window._sc_getMsgEles = () => {
  64. try {
  65. const convCont = document.querySelector("#b_sydConvCont > cib-serp");
  66. const tigerCont = document.querySelector("#b_sydTigerCont > cib-serp");
  67. return getContMsgEles(convCont).concat(getContMsgEles(tigerCont, false));
  68. } catch (ignore) {
  69. return [];
  70. }
  71. }
  72. }
  73. else if (window.location.host == "chat.openai.com") {
  74. window._sc_getMsgEles = () => {
  75. return document.querySelectorAll(queryAddNoParsed("div.w-full div.text-base div.items-start"));
  76. }
  77. window._sc_beforeTypesetMsg = (msg) => {
  78. // Prevent latex typeset conflict
  79. const displayEles = msg.querySelectorAll('.math-display');
  80. displayEles.forEach(e => {
  81. const texEle = e.querySelector(".katex-mathml annotation");
  82. e.removeAttribute("class");
  83. e.textContent = texEle.textContent;
  84. });
  85. };
  86. window._sc_getObserveElement = () => {
  87. return document.querySelector("main form textarea+button");
  88. }
  89. window._sc_typesetAfter = (element) => { element.style.display = 'unset';}
  90. window._sc_chatLoaded = () => { return document.querySelector('main div.text-sm>svg.animate-spin') === null; };
  91.  
  92. afterMainOvservationStart = () => {
  93. window._sc_typeset();
  94. // Handle conversation switch
  95. new MutationObserver((mutationList) => {
  96. mutationList.forEach(async (mutation) => {
  97. if (mutation.addedNodes){
  98. window._sc_typeset();
  99. startMainOvservation(await getMainObserveElement(true));
  100. }
  101. });
  102. }).observe(document.querySelector('#__next'), {childList: true});
  103. };
  104. }
  105. else if (window.location.host == "you.com") {
  106. window._sc_getMsgEles = () => {
  107. return document.querySelectorAll(queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]'));
  108. }
  109. window._sc_getObserveElement = () => {
  110. return document.querySelector('main div[data-testid="youchat-input"] textarea+button');
  111. }
  112. }
  113. console.log('Waiting for chat loading...')
  114. const mainElement = await getMainObserveElement();
  115. console.log('Chat loaded.')
  116. startMainOvservation(mainElement);
  117. afterMainOvservationStart();
  118. }
  119.  
  120. function getMainObserveElement(chatLoaded=false) {
  121. return new Promise(async (resolve, reject) => {
  122. const resolver = () => {
  123. const ele = window._sc_getObserveElement();
  124. if (ele && (chatLoaded || window._sc_chatLoaded())) {
  125. return resolve(ele);
  126. }
  127. window.setTimeout(resolver, 500);
  128. }
  129. resolver();
  130. });
  131. }
  132.  
  133. async function startMainOvservation(mainElement) {
  134. const observerOptions = {
  135. attributeOldValue : true,
  136. attributeFilter: ['cancelable', 'disabled'],
  137. };
  138. const callback = (mutationList, observer) => {
  139. mutationList.forEach(mutation => {
  140. window._sc_mutationHandler(mutation);
  141. });
  142. };
  143. if (window._sc_mainObserver) {
  144. window._sc_mainObserver.disconnect();
  145. }
  146. window._sc_mainObserver = new MutationObserver(callback);
  147. window._sc_mainObserver.observe(mainElement, observerOptions);
  148. }
  149.  
  150. async function addScript(url) {
  151. const scriptElement = document.createElement('script');
  152. const headElement = document.getElementsByTagName('head')[0] || document.documentElement;
  153. if (!headElement.appendChild(scriptElement)) {
  154. // Prevent appendChild overwritten problem.
  155. headElement.append(scriptElement);
  156. }
  157. scriptElement.src = url;
  158. }
  159.  
  160. async function waitMathJaxLoaded() {
  161. while (!MathJax.hasOwnProperty('typeset')) {
  162. if (window._sc_ChatLatex.loadCount > 20000 / 200) {
  163. setTipsElementText("Failed to load MathJax, try refresh.", true);
  164. }
  165. await new Promise((x) => setTimeout(x, 500));
  166. window._sc_ChatLatex.loadCount += 1;
  167. }
  168. }
  169.  
  170. function showTipsElement() {
  171. const tipsElement = window._sc_ChatLatex.tipsElement;
  172. tipsElement.style.position = "fixed";
  173. tipsElement.style.left = "10px";
  174. tipsElement.style.bottom = "10px";
  175. tipsElement.style.background = '#333';
  176. tipsElement.style.color = '#fff';
  177. document.body.appendChild(tipsElement);
  178. }
  179.  
  180. function setTipsElementText(text, errorRaise=false) {
  181. window._sc_ChatLatex.tipsElement.innerHTML = text;
  182. if (errorRaise) {
  183. throw text;
  184. }
  185. console.log(text);
  186. }
  187.  
  188. function hideTipsElement(timeout=3) {
  189. window.setTimeout(() => {window._sc_ChatLatex.tipsElement.hidden=true; }, 3000);
  190. }
  191.  
  192. async function loadMathJax() {
  193. showTipsElement();
  194. setTipsElementText("Loading MathJax...");
  195. addScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js');
  196. await waitMathJaxLoaded();
  197. setTipsElementText("MathJax Loaded.");
  198. hideTipsElement();
  199. }
  200.  
  201. (async function() {
  202. window._sc_ChatLatex = {
  203. tipsElement: document.createElement("div"),
  204. loadCount: 0
  205. };
  206. window.MathJax = {
  207. tex: {
  208. inlineMath: [['$', '$'], ['\\(', '\\)']],
  209. displayMath : [['$$', '$$', ['\\[', '\\]']]]
  210. },
  211. startup: {
  212. typeset: false
  213. }
  214. };
  215.  
  216. await loadMathJax();
  217. await prepareScript();
  218. })();