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

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

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

  1. // ==UserScript==
  2. // @name ChatGPT LaTeX Auto Render (OpenAI, you, new bing, etc.)
  3. // @version 0.5.5
  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://www.bing.com/search?*
  10. // @match https://you.com/search?*&tbm=youchat*
  11. // @match https://www.you.com/search?*&tbm=youchat*
  12. // @namespace http://tampermonkey.net/
  13. // @icon https://chat.openai.com/favicon.ico
  14. // @grant none
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. 'use strict';
  19.  
  20. const _parsed_mark = '_sc_parsed';
  21. function queryAddNoParsed(query) {
  22. return query + ":not([" + _parsed_mark + "])";
  23. }
  24.  
  25. async function prepareScript() {
  26. window._sc_beforeTypesetMsg = (msg) => { msg.setAttribute(_parsed_mark,'');};
  27. window._sc_afterTypesetMsg = (element) => {};
  28. window._sc_typeset = () => {
  29. try {
  30. const messages = window._sc_getMsgEles();
  31. messages.forEach(msg => {
  32. window._sc_beforeTypesetMsg(msg);
  33. MathJax.typesetPromise([msg]);
  34. window._sc_afterTypesetMsg(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. window._sc_getObserveElement = () => { return null; };
  47. var observerOptions = {
  48. attributeOldValue : true,
  49. attributeFilter: ['cancelable', 'disabled'],
  50. };
  51. var afterMainOvservationStart = () => { window._sc_typeset(); };
  52.  
  53. // Handle special cases per site.
  54. if (window.location.host == "www.bing.com") {
  55. window._sc_getObserveElement = () => {
  56. const ele = document.querySelector("#b_sydConvCont > cib-serp");
  57. if (!ele) {return null;}
  58. return ele.shadowRoot.querySelector("#cib-action-bar-main");
  59. }
  60.  
  61. const getContMsgEles = (cont, isInChat=true) => {
  62. const allChatTurn = cont.shadowRoot.querySelector("#cib-conversation-main").shadowRoot.querySelectorAll("cib-chat-turn");
  63. var lastChatTurnSR = allChatTurn[allChatTurn.length - 1];
  64. if (isInChat) { lastChatTurnSR = lastChatTurnSR.shadowRoot; }
  65. const allCibMsgGroup = lastChatTurnSR.querySelectorAll("cib-message-group");
  66. const allCibMsg = Array.from(allCibMsgGroup).map(e => Array.from(e.shadowRoot.querySelectorAll("cib-message"))).flatMap(e => e);
  67. return Array.from(allCibMsg).map(cibMsg => cibMsg.shadowRoot.querySelector("cib-shared")).filter(e => e);
  68. }
  69. window._sc_getMsgEles = () => {
  70. try {
  71. const convCont = document.querySelector("#b_sydConvCont > cib-serp");
  72. const tigerCont = document.querySelector("#b_sydTigerCont > cib-serp");
  73. return getContMsgEles(convCont).concat(getContMsgEles(tigerCont, false));
  74. } catch (ignore) {
  75. return [];
  76. }
  77. }
  78. }
  79. else if (window.location.host == "chat.openai.com") {
  80. window._sc_getObserveElement = () => {
  81. return document.querySelector("main form textarea+button");
  82. }
  83. window._sc_chatLoaded = () => { return document.querySelector('main div.text-sm>svg.animate-spin') === null; };
  84.  
  85. afterMainOvservationStart = () => {
  86. window._sc_typeset();
  87. // Handle conversation switch
  88. new MutationObserver((mutationList) => {
  89. mutationList.forEach(async (mutation) => {
  90. if (mutation.addedNodes){
  91. window._sc_typeset();
  92. startMainOvservation(await getMainObserveElement(true), observerOptions);
  93. }
  94. });
  95. }).observe(document.querySelector('#__next'), {childList: true});
  96. };
  97.  
  98. window._sc_getMsgEles = () => {
  99. return document.querySelectorAll(queryAddNoParsed("div.w-full div.text-base div.items-start"));
  100. }
  101.  
  102. window._sc_beforeTypesetMsg = (msg) => {
  103. msg.setAttribute(_parsed_mark,'');
  104. // Prevent latex typeset conflict
  105. const displayEles = msg.querySelectorAll('.math-display');
  106. displayEles.forEach(e => {
  107. const texEle = e.querySelector(".katex-mathml annotation");
  108. e.removeAttribute("class");
  109. e.textContent = texEle.textContent;
  110. });
  111. };
  112. window._sc_afterTypesetMsg = (element) => { element.style.display = 'unset';}
  113. }
  114. else if (window.location.host == "you.com" || window.location.host == "www.you.com") {
  115. window._sc_getObserveElement = () => {
  116. return document.querySelector('#chatHistory');
  117. };
  118. window._sc_chatLoaded = () => { return document.querySelector('#chatHistory div[data-pinnedconversationturnid]'); };
  119.  
  120. observerOptions = {
  121. childList : true
  122. };
  123.  
  124. window._sc_mutationHandler = (mutation) => {
  125. mutation.addedNodes.forEach(e => {
  126. const attr = e.getAttribute('data-testid')
  127. if (attr && attr.startsWith("youchat-convTurn")) {
  128. startTurnAttrObservationForTypesetting(e, 'data-pinnedconversationturnid');
  129. }
  130. })
  131. };
  132.  
  133. window._sc_getMsgEles = () => {
  134. return document.querySelectorAll(queryAddNoParsed('#chatHistory div[data-testid="youchat-answer"]'));
  135. };
  136. }
  137. console.log('Waiting for chat loading...')
  138. const mainElement = await getMainObserveElement();
  139. console.log('Chat loaded.')
  140. startMainOvservation(mainElement, observerOptions);
  141. afterMainOvservationStart();
  142. }
  143.  
  144. // After output completed, the attribute of turn element will be changed,
  145. // only with observer won't be enough, so we have this function for sure.
  146. function startTurnAttrObservationForTypesetting(element, doneWithAttr) {
  147. const tmpObserver = new MutationObserver((mutationList, observer) => {
  148. mutationList.forEach(mutation => {
  149. if (mutation.oldValue === null) {
  150. window._sc_typeset();
  151. observer.disconnect;
  152. }
  153. })
  154. });
  155. tmpObserver.observe(element, {
  156. attributeOldValue : true,
  157. attributeFilter: [doneWithAttr],
  158. });
  159. if (element.hasAttribute(doneWithAttr)) {
  160. window._sc_typeset();
  161. tmpObserver.disconnect;
  162. }
  163. }
  164.  
  165. function getMainObserveElement(chatLoaded=false) {
  166. return new Promise(async (resolve, reject) => {
  167. const resolver = () => {
  168. const ele = window._sc_getObserveElement();
  169. if (ele && (chatLoaded || window._sc_chatLoaded())) {
  170. return resolve(ele);
  171. }
  172. window.setTimeout(resolver, 500);
  173. }
  174. resolver();
  175. });
  176. }
  177.  
  178. function startMainOvservation(mainElement, observerOptions) {
  179. const callback = (mutationList, observer) => {
  180. mutationList.forEach(mutation => {
  181. window._sc_mutationHandler(mutation);
  182. });
  183. };
  184. if (window._sc_mainObserver) {
  185. window._sc_mainObserver.disconnect();
  186. }
  187. window._sc_mainObserver = new MutationObserver(callback);
  188. window._sc_mainObserver.observe(mainElement, observerOptions);
  189. }
  190.  
  191. async function addScript(url) {
  192. const scriptElement = document.createElement('script');
  193. const headElement = document.getElementsByTagName('head')[0] || document.documentElement;
  194. if (!headElement.appendChild(scriptElement)) {
  195. // Prevent appendChild overwritten problem.
  196. headElement.append(scriptElement);
  197. }
  198. scriptElement.src = url;
  199. }
  200.  
  201. async function waitMathJaxLoaded() {
  202. while (!MathJax.hasOwnProperty('typeset')) {
  203. if (window._sc_ChatLatex.loadCount > 20000 / 200) {
  204. setTipsElementText("Failed to load MathJax, try refresh.", true);
  205. }
  206. await new Promise((x) => setTimeout(x, 500));
  207. window._sc_ChatLatex.loadCount += 1;
  208. }
  209. }
  210.  
  211. function showTipsElement() {
  212. const tipsElement = window._sc_ChatLatex.tipsElement;
  213. tipsElement.style.position = "fixed";
  214. tipsElement.style.right = "10px";
  215. tipsElement.style.top = "10px";
  216. tipsElement.style.background = '#333';
  217. tipsElement.style.color = '#fff';
  218. tipsElement.style.zIndex = '999999';
  219. document.body.appendChild(tipsElement);
  220. }
  221.  
  222. function setTipsElementText(text, errorRaise=false) {
  223. window._sc_ChatLatex.tipsElement.innerHTML = text;
  224. if (errorRaise) {
  225. throw text;
  226. }
  227. console.log(text);
  228. }
  229.  
  230. function hideTipsElement(timeout=3) {
  231. window.setTimeout(() => {window._sc_ChatLatex.tipsElement.hidden=true; }, 3000);
  232. }
  233.  
  234. async function loadMathJax() {
  235. showTipsElement();
  236. setTipsElementText("Loading MathJax...");
  237. addScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js');
  238. await waitMathJaxLoaded();
  239. setTipsElementText("MathJax Loaded.");
  240. hideTipsElement();
  241. }
  242.  
  243. (async function() {
  244. window._sc_ChatLatex = {
  245. tipsElement: document.createElement("div"),
  246. loadCount: 0
  247. };
  248. window.MathJax = {
  249. tex: {
  250. inlineMath: [['$', '$'], ['\\(', '\\)']],
  251. displayMath : [['$$', '$$', ['\\[', '\\]']]]
  252. },
  253. startup: {
  254. typeset: false
  255. }
  256. };
  257.  
  258. await loadMathJax();
  259. await prepareScript();
  260. })();