Greasy Fork 还支持 简体中文。

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