ChatGPT-input-helper

Help organize commonly used spells quickly

当前为 2023-04-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT-input-helper
  3. // @name:zh-TW ChatGPT-input-helper 快速輸入常用咒文
  4. // @namespace https://github.com/we684123/ChatGPT-input-helper
  5. // @version 0.0.5
  6. // @author we684123
  7. // @description Help organize commonly used spells quickly
  8. // @description:zh-TW 幫助快速組織常用咒文
  9. // @license MIT
  10. // @icon https://chat.openai.com/favicon.ico
  11. // @match https://chat.openai.com/chat
  12. // @match https://chat.openai.com/chat/*
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_deleteValue
  16. // @run-at document-end
  17. // ==/UserScript==
  18.  
  19. (function (factory) {
  20. typeof define === 'function' && define.amd ? define(factory) :
  21. factory();
  22. })((function () { 'use strict';
  23.  
  24. const sentinel = (() => {
  25. const isArray = Array.isArray;
  26. let selectorToAnimationMap = {};
  27. let animationCallbacks = {};
  28. let styleEl;
  29. let styleSheet;
  30. let cssRules;
  31. return {
  32. // `on` 方法用於添加 CSS 選擇器的監聽器。
  33. // cssSelectors: 一個字符串或字符串數組,包含要監聽的 CSS 選擇器。
  34. // callback: 用於處理觸發的事件的回調函數。
  35. on: function (cssSelectors, callback) {
  36. // 如果沒有提供回調函數,則直接返回。
  37. if (!callback)
  38. return;
  39. // 如果 `styleEl` 未定義,創建一個新的 `style` 標籤並將其添加到文檔的 `head` 中。
  40. // 還會為 `animationstart` 事件添加事件監聽器。
  41. if (!styleEl) {
  42. const doc = document;
  43. const head = doc.head;
  44. doc.addEventListener("animationstart", function (ev) {
  45. const callbacks = animationCallbacks[ev.animationName];
  46. if (!callbacks)
  47. return;
  48. ev.stopImmediatePropagation();
  49. for (const cb of callbacks) {
  50. cb(ev.target);
  51. }
  52. }, true);
  53. styleEl = doc.createElement("style");
  54. // head.insertBefore(styleEl, head.firstChild); // 這個是原版的,改用下面的
  55. head.append(styleEl); // 感謝 chatgpt-exporter 搞好久 (┬┬﹏┬┬)
  56. styleSheet = styleEl.sheet;
  57. cssRules = styleSheet.cssRules;
  58. }
  59. // 根據提供的選擇器創建一個新的動畫。
  60. const selectors = isArray(cssSelectors) ? cssSelectors : [cssSelectors];
  61. selectors.forEach((selector) => {
  62. // 獲取或創建動畫 ID。
  63. let animIds = selectorToAnimationMap[selector];
  64. if (!animIds) {
  65. const isCustomName = selector[0] == "!";
  66. const animId = isCustomName
  67. ? selector.slice(1)
  68. : "sentinel-" + Math.random().toString(16).slice(2);
  69. // 創建新的 keyframes 規則。
  70. const keyframeRule = cssRules[styleSheet.insertRule("@keyframes " +
  71. animId +
  72. "{from{transform:none;}to{transform:none;}}", cssRules.length)];
  73. keyframeRule._id = selector;
  74. // 如果選擇器不是自定義名稱,則為其創建對應的CSS 規則。
  75. if (!isCustomName) {
  76. const selectorRule = cssRules[styleSheet.insertRule(selector + "{animation-duration:0.0001s;animation-name:" + animId + ";}", cssRules.length)];
  77. selectorRule._id = selector;
  78. }
  79. animIds = [animId];
  80. selectorToAnimationMap[selector] = animIds;
  81. }
  82. // 遍歷動畫 ID,將回調函數添加到動畫回調列表中。
  83. animIds.forEach((animId) => {
  84. animationCallbacks[animId] = animationCallbacks[animId] || [];
  85. animationCallbacks[animId].push(callback);
  86. });
  87. });
  88. },
  89. // `off` 方法用於移除 CSS 選擇器的監聽器。
  90. // cssSelectors: 一個字符串或字符串數組,包含要停止監聽的 CSS 選擇器。
  91. // callback: 可選的回調函數。如果提供,則僅移除與之匹配的監聽器。
  92. off: function (cssSelectors, callback) {
  93. // 將提供的選擇器轉換為數組形式。
  94. const selectors = isArray(cssSelectors) ? cssSelectors : [cssSelectors];
  95. // 遍歷選擇器,移除對應的監聽器。
  96. selectors.forEach((selector) => {
  97. const animIds = selectorToAnimationMap[selector];
  98. if (!animIds)
  99. return;
  100. animIds.forEach((animId) => {
  101. const callbacks = animationCallbacks[animId];
  102. if (!callbacks)
  103. return;
  104. // 如果提供了回調函數,則僅移除與之匹配的監聽器。
  105. if (callback) {
  106. const index = callbacks.indexOf(callback);
  107. if (index !== -1) {
  108. callbacks.splice(index, 1);
  109. }
  110. }
  111. else {
  112. delete animationCallbacks[animId];
  113. }
  114. // 如果該選擇器沒有任何回調函數,則從選擇器映射和 CSS 規則中移除它。
  115. if (callbacks.length === 0) {
  116. delete selectorToAnimationMap[selector];
  117. const rulesToDelete = [];
  118. for (let i = 0, len = cssRules.length; i < len; i++) {
  119. const rule = cssRules[i];
  120. if (rule._id === selector) {
  121. rulesToDelete.push(rule);
  122. }
  123. }
  124. rulesToDelete.forEach((rule) => {
  125. const index = Array.prototype.indexOf.call(cssRules, rule);
  126. if (index !== -1) {
  127. styleSheet.deleteRule(index);
  128. }
  129. });
  130. }
  131. });
  132. });
  133. }
  134. };
  135. })();
  136.  
  137. function onloadSafe(fn) {
  138. if (document.readyState === "complete") {
  139. fn();
  140. }
  141. else {
  142. window.addEventListener("load", fn);
  143. }
  144. }
  145.  
  146. function styleInject(css, ref) {
  147. if ( ref === void 0 ) ref = {};
  148. var insertAt = ref.insertAt;
  149.  
  150. if (!css || typeof document === 'undefined') { return; }
  151.  
  152. var head = document.head || document.getElementsByTagName('head')[0];
  153. var style = document.createElement('style');
  154. style.type = 'text/css';
  155.  
  156. if (insertAt === 'top') {
  157. if (head.firstChild) {
  158. head.insertBefore(style, head.firstChild);
  159. } else {
  160. head.appendChild(style);
  161. }
  162. } else {
  163. head.appendChild(style);
  164. }
  165.  
  166. if (style.styleSheet) {
  167. style.styleSheet.cssText = css;
  168. } else {
  169. style.appendChild(document.createTextNode(css));
  170. }
  171. }
  172.  
  173. var css_248z$1 = ".buttonStyles-module_container__l-r9Y{align-items:center;border:1px solid #fff;border-radius:5px;box-sizing:border-box;display:flex;justify-content:center;position:relative;width:100%}.buttonStyles-module_mainButton__b08pW{border:1px solid #fff;border-radius:5px;margin:0 auto;padding:8px 12px;width:85%}.buttonStyles-module_mainButton__b08pW,.buttonStyles-module_settingButton__-opQi{background-color:#202123;box-sizing:border-box;color:#fff;cursor:pointer;font-size:14px}.buttonStyles-module_settingButton__-opQi{border:none;border-radius:5px;padding:8px 14px;width:15%}.buttonStyles-module_menu__aeYDY{background-color:#202123;border:1px solid #fff;border-radius:15px;display:none;left:100px;position:absolute;width:100%;z-index:1}.buttonStyles-module_menuButton__eg9D8{background-color:#202123;border:1px solid #fff;border-radius:5px;color:#fff;cursor:pointer;display:block;font-size:14px;height:100%;padding:8px 12px;width:100%}";
  174. var styles = {"container":"buttonStyles-module_container__l-r9Y","mainButton":"buttonStyles-module_mainButton__b08pW","settingButton":"buttonStyles-module_settingButton__-opQi","menu":"buttonStyles-module_menu__aeYDY","menuButton":"buttonStyles-module_menuButton__eg9D8"};
  175. styleInject(css_248z$1);
  176.  
  177. // library.ts
  178. const config = {
  179. name: "aims-helper",
  180. init_customize: [
  181. {
  182. name: '繁體中文初始化',
  183. position: 'start',
  184. autoEnter: true,
  185. content: [
  186. `以下問答請使用繁體中文,並使用台灣用語。\n`,
  187. ].join("")
  188. }, {
  189. name: '請繼續',
  190. position: 'start',
  191. autoEnter: true,
  192. content: [
  193. `請繼續`,
  194. ].join("")
  195. }, {
  196. name: '請從""繼續',
  197. position: 'start',
  198. autoEnter: false,
  199. content: [
  200. `請從""繼續`,
  201. ].join("")
  202. }
  203. ],
  204. // ↓ 左邊選單的定位(上層)
  205. NAV_MENU: 'nav > div.overflow-y-auto',
  206. // ↓ 輸入框的定位
  207. TEXT_INPUTBOX_POSITION: 'textarea.m-0',
  208. // ↓ 送出按鈕的定位
  209. SUBMIT_BUTTON_POSITION: 'button.absolute',
  210. // ↓ 選單按鈕
  211. MAIN_BUTTON_CLASS: 'main_button',
  212. // ↓ 控制按鈕
  213. SETTING_BUTTON_CLASS: 'setting_button',
  214. // ↓ 選單
  215. MENU_CLASS: 'main_menu',
  216. // ↓ 按鈕文字
  217. HELPER_MENU_TEXT: 'input helper',
  218. // ↓ 按鈕用容器
  219. CONTAINER_CLASS: 'helper_textcontainer',
  220. // ↓ 模擬輸入於輸入框的事件
  221. INPUT_EVENT: new Event('input', { bubbles: true }),
  222. };
  223.  
  224. // 將自定義內容插入到輸入框中
  225. const insertCustomize = (customize, name) => {
  226. const textInputbox = document.querySelector(config.TEXT_INPUTBOX_POSITION);
  227. const submitButton = document.querySelector(config.SUBMIT_BUTTON_POSITION);
  228. const item = customize.find((i) => i.name === name);
  229. if (item) {
  230. if (item.position === 'start') {
  231. textInputbox.value = item.content + textInputbox.value;
  232. }
  233. else {
  234. textInputbox.value += item.content;
  235. }
  236. textInputbox.dispatchEvent(config.INPUT_EVENT);
  237. textInputbox.focus();
  238. if (item.autoEnter) {
  239. submitButton.click();
  240. }
  241. }
  242. else {
  243. console.error(`找不到名稱為 ${name} 的元素`);
  244. }
  245. };
  246.  
  247. // 創建主按鈕
  248. const createMainButton = (buttonText) => {
  249. const mainButton = document.createElement("button");
  250. mainButton.innerText = buttonText;
  251. mainButton.classList.add(styles.mainButton);
  252. mainButton.style.width = "86%";
  253. return mainButton;
  254. };
  255. // 創建設定按鈕
  256. const createSettingButton = () => {
  257. const settingButton = document.createElement("button");
  258. settingButton.innerText = "⚙️";
  259. settingButton.classList.add(styles.settingButton);
  260. settingButton.style.width = "14%";
  261. settingButton.id = "settingButton";
  262. return settingButton;
  263. };
  264. // 創建選項
  265. const createMenuItem = (element, customize) => {
  266. const menuItem = document.createElement("button");
  267. menuItem.innerText = element.name;
  268. menuItem.id = element.name;
  269. menuItem.classList.add(styles.menuButton);
  270. menuItem.addEventListener("click", (event) => {
  271. insertCustomize(customize, event.target.id);
  272. });
  273. return menuItem;
  274. };
  275. // 創建選單(包含多個選項)
  276. const createMenu = (containerNode, customize) => {
  277. const menu = document.createElement("div");
  278. menu.id = "helper_menu";
  279. menu.classList.add(styles.menu);
  280. menu.style.display = "none";
  281. menu.style.width = `${containerNode.offsetWidth}px`;
  282. customize.forEach((element) => {
  283. const menuItem = createMenuItem(element, customize);
  284. menu.appendChild(menuItem);
  285. });
  286. return menu;
  287. };
  288.  
  289. const bindElementContainer = (elements, containerClass) => {
  290. const container = document.createElement("div");
  291. if (containerClass) {
  292. container.classList.add(containerClass);
  293. }
  294. elements.forEach((element) => {
  295. container.appendChild(element);
  296. });
  297. return container;
  298. };
  299.  
  300. // addMenuBtn 函數用於新增包含主按鈕和設定按鈕的選單按鈕
  301. function addMenuBtnWrapper(containerNode, customize, buttonText = "Click Me" // 主按鈕的文字,預設值為 "Click Me"
  302. ) {
  303. // 創建主按鈕和設定按鈕
  304. const mainButton = createMainButton(buttonText);
  305. const settingButton = createSettingButton();
  306. // 將主按鈕和設定按鈕組合在一個容器中
  307. const assButton = bindElementContainer([settingButton, mainButton], config.CONTAINER_CLASS);
  308. // 根據客製化選單項目創建選單
  309. const menu = createMenu(containerNode, customize);
  310. // 當滑鼠移到按鈕上時,顯示選單
  311. assButton.addEventListener("mouseenter", () => {
  312. menu.style.display = "block";
  313. });
  314. // 創建按鈕包裹器,並將組合按鈕和選單加入其中
  315. const buttonWrapper = document.createElement("div");
  316. buttonWrapper.style.width = `${containerNode.offsetWidth}px`;
  317. buttonWrapper.appendChild(assButton);
  318. buttonWrapper.appendChild(menu);
  319. // 將按鈕包裹器加入到容器節點中
  320. containerNode.appendChild(buttonWrapper);
  321. // 當滑鼠離開按鈕包裹器時,隱藏選單
  322. buttonWrapper.addEventListener("mouseleave", () => {
  323. setTimeout(() => {
  324. menu.style.display = "none";
  325. }, 300);
  326. });
  327. console.log("已新增按鈕");
  328. }
  329.  
  330. function setCustomizeBtn(customize) {
  331. // 找到 settingButton 元素
  332. const settingButton = document.getElementById('settingButton');
  333. let newPosition;
  334. let newAutoEnter;
  335. // 當點擊 settingButton 時觸發事件
  336. settingButton.addEventListener('click', () => {
  337. // 創建彈出視窗
  338. const popup = document.createElement('div');
  339. popup.style.position = 'fixed';
  340. popup.style.top = '50%';
  341. popup.style.left = '50%';
  342. popup.style.transform = 'translate(-50%, -50%)';
  343. popup.style.background = '#525467';
  344. popup.style.border = '1px solid black';
  345. popup.style.padding = '30px';
  346. popup.style.width = '80%';
  347. popup.style.maxWidth = '800px';
  348. popup.style.height = '60%';
  349. popup.style.maxHeight = '1200px';
  350. popup.style.zIndex = '9999';
  351. // 創建新增按鈕
  352. const addButton = document.createElement('button');
  353. addButton.textContent = '新增(add)';
  354. addButton.style.margin = '10px';
  355. addButton.style.border = '2px solid #ffffff';
  356. addButton.addEventListener('click', () => {
  357. // 新增一個 item
  358. const newItem = {
  359. name: '',
  360. position: '',
  361. content: ''
  362. };
  363. customize.push(newItem);
  364. renderTable();
  365. });
  366. popup.appendChild(addButton);
  367. // 創建編輯按鈕
  368. const editButton = document.createElement('button');
  369. editButton.textContent = '編輯(edit)';
  370. editButton.style.margin = '10px';
  371. editButton.style.border = '2px solid #ffffff';
  372. editButton.addEventListener('click', () => {
  373. // 編輯一個 item
  374. const index = prompt('請輸入要編輯的編號(edit index)');
  375. if (index && Number(index) >= 1 && index <= customize.length) {
  376. const item = customize[Number(index) - 1];
  377. // 編輯 name
  378. const newName = prompt('請輸入新的 name', item.name);
  379. if (newName !== null) {
  380. item.name = newName;
  381. }
  382. // 編輯 position
  383. do {
  384. newPosition = prompt('請輸入新的 position (只能輸入 start 或 end)', item.position);
  385. } while (newPosition !== null && newPosition !== 'start' && newPosition !== 'end');
  386. if (newPosition !== null) {
  387. item.position = newPosition;
  388. }
  389. // 編輯 position
  390. do {
  391. newAutoEnter = prompt('請輸入新的 AutoEnter (只能輸入 y 或 n)', item.autoEnter ? 'y' : 'n');
  392. } while (newAutoEnter !== null && newAutoEnter !== 'y' && newAutoEnter !== 'n');
  393. if (newAutoEnter !== null) {
  394. if (newAutoEnter === 'y') {
  395. item.autoEnter = true;
  396. }
  397. else {
  398. item.autoEnter = false;
  399. }
  400. }
  401. // 編輯 content
  402. // const textarea = document.createElement('textarea');
  403. // textarea.value = item.content;
  404. // textarea.style.width = '100%';
  405. // textarea.style.height = '100px';
  406. const newContent = prompt('請輸入新的 content', item.content);
  407. if (newContent !== null) {
  408. item.content = newContent;
  409. }
  410. // 重新渲染表格
  411. renderTable();
  412. }
  413. else {
  414. alert('輸入的編號不合法');
  415. }
  416. });
  417. popup.appendChild(editButton);
  418. // 創建刪除按鈕
  419. const deleteButton = document.createElement('button');
  420. deleteButton.textContent = '刪除(delete)';
  421. deleteButton.style.margin = '10px';
  422. deleteButton.style.border = '2px solid #ffffff';
  423. deleteButton.addEventListener('click', () => {
  424. // 刪除一個 item
  425. const index = prompt('請輸入要刪除的編號(delete index)');
  426. if (index && Number(index) >= 1 && index <= customize.length) {
  427. customize.splice(Number(index) - 1, 1);
  428. renderTable();
  429. }
  430. else {
  431. alert('輸入的編號不合法 (invalid index)');
  432. }
  433. });
  434. popup.appendChild(deleteButton);
  435. // 創建關閉按鈕
  436. const closeButton = document.createElement('button');
  437. closeButton.textContent = '儲存並離開(save&exit)';
  438. closeButton.style.position = 'absolute';
  439. closeButton.style.top = '5px';
  440. closeButton.style.right = '5px';
  441. closeButton.addEventListener('click', () => {
  442. console.log(customize);
  443. // 儲存修改後的 customize 資料
  444. GM_setValue('customizeData', customize);
  445. // // 重寫一次 helper_menu
  446. // const helper_menu = document.getElementById('helper_menu');
  447. // const menu = createMenu(helper_menu);
  448. // helper_menu.replaceWith(menu);
  449. // 上面的做不出來
  450. // 所以只好重新整理頁面
  451. location.reload();
  452. document.body.removeChild(popup);
  453. });
  454. popup.appendChild(closeButton);
  455. // 創建表格
  456. const table = document.createElement('table');
  457. popup.appendChild(table);
  458. // 創建表頭
  459. const thead = document.createElement('thead');
  460. const tr = document.createElement('tr');
  461. const th1 = document.createElement('th');
  462. const th2 = document.createElement('th');
  463. const th3 = document.createElement('th');
  464. const th4 = document.createElement('th');
  465. const th5 = document.createElement('th');
  466. th1.textContent = '編號(index)';
  467. th2.textContent = '名稱(name)';
  468. th3.textContent = '位置(position)';
  469. th4.textContent = '自動輸入(autoEnter)?';
  470. th5.textContent = '內容(content)';
  471. tr.appendChild(th1);
  472. tr.appendChild(th2);
  473. tr.appendChild(th3);
  474. tr.appendChild(th4);
  475. tr.appendChild(th5);
  476. thead.appendChild(tr);
  477. table.appendChild(thead);
  478. // 創建表身
  479. const tbody = document.createElement('tbody');
  480. table.appendChild(tbody);
  481. // 渲染表格
  482. function renderTable() {
  483. // 先清空表格內容
  484. tbody.innerHTML = '';
  485. // 重新渲染表格
  486. customize.forEach((item, index) => {
  487. const tr = document.createElement('tr');
  488. const td1 = document.createElement('td');
  489. const td2 = document.createElement('td');
  490. const td3 = document.createElement('td');
  491. const td4 = document.createElement('td');
  492. const td5 = document.createElement('td');
  493. td1.textContent = index + 1;
  494. td2.textContent = item.name;
  495. td3.textContent = item.position;
  496. td4.textContent = item.autoEnter;
  497. td5.textContent = item.content;
  498. tr.appendChild(td1);
  499. tr.appendChild(td2);
  500. tr.appendChild(td3);
  501. tr.appendChild(td4);
  502. tr.appendChild(td5);
  503. tbody.appendChild(tr);
  504. });
  505. }
  506. // 渲染初始表格
  507. renderTable();
  508. // 點擊彈窗外的地方關閉彈窗
  509. popup.addEventListener('click', (event) => {
  510. if (event.target === popup) {
  511. document.body.removeChild(popup);
  512. }
  513. });
  514. // 將彈出視窗加入頁面中
  515. document.body.appendChild(popup);
  516. });
  517. }
  518.  
  519. var css_248z = ".custom-element{background-color:#f9f9f9;border:1px solid #ccc;border-radius:4px;color:#333;font-size:14px;padding:10px}";
  520. styleInject(css_248z);
  521.  
  522. main();
  523. function main() {
  524. // 頁面載入完成後執行
  525. onloadSafe(() => {
  526. // 監聽 nav 元素
  527. console.log("=====監聽 nav 元素=====");
  528. // 定義常用咒文
  529. let customize;
  530. sentinel.on("nav", (nav) => {
  531. console.log("===== trigger sentinel.on nav =====");
  532. // 讀取 customize 設定
  533. let GM_customize = GM_getValue("customizeData", customize);
  534. // 如果 user 已經有設定了就用 user 的,沒有就用預設值
  535. if (GM_customize) {
  536. customize = GM_customize;
  537. }
  538. else {
  539. customize = config.init_customize;
  540. GM_setValue("customizeData", customize);
  541. }
  542. //找不到就新增
  543. const container = document.getElementById("helper_menu");
  544. if (!container) {
  545. // 獲得目標元素
  546. const aimsNode = document.querySelector(config.NAV_MENU);
  547. // 新增一個容器
  548. const container = document.createElement("div");
  549. container.classList.add(config.CONTAINER_CLASS);
  550. container.id = "helper_menu";
  551. if (aimsNode) {
  552. // 設定 container 寬度為父元素寬度
  553. container.style.width = `${aimsNode.offsetWidth}px`; // 設定 container 寬度為父元素寬度
  554. // 將容器元素插入到目標元素後面
  555. aimsNode.parentNode?.insertBefore(container, aimsNode.nextSibling);
  556. // 新增一個按鈕元素
  557. addMenuBtnWrapper(container, customize, config.HELPER_MENU_TEXT);
  558. // 設定 "設定按鈕"的點擊事件
  559. setCustomizeBtn(customize);
  560. }
  561. }
  562. });
  563. });
  564. }
  565.  
  566. }));