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

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

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

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