ChatGPT helper

幫助快速組織常用咒文

目前為 2023-03-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT helper
  3. // @namespace we684123
  4. // @version 0.0.1
  5. // @author we684123
  6. // @description 幫助快速組織常用咒文
  7. // @license MIT
  8. // @icon https://chat.openai.com/favicon.ico
  9. // @match https://chat.openai.com/chat
  10. // @match https://chat.openai.com/chat/*
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_deleteValue
  14. // @run-at document-end
  15. // ==/UserScript==
  16.  
  17.  
  18. // 定義常用咒文
  19. let customize
  20.  
  21. // 定義初始化咒文
  22. let init_customize = [
  23. {
  24. name: '繁體中文初始化', // 按鈕的顯示名稱
  25. position: 'start', // start = 最前面 , end = 最後面
  26. autoEnter: true, // 是否自動按下 Enter
  27. content: [ // 要被放入輸入框的文字
  28. `以下問答請使用繁體中文,並使用台灣用語。\n`,
  29. `代碼請改用程式碼,"返回"改為"回傳""水平"改為"水準""質量"改為"品質",`,
  30. `"示例"改為"範例""博客"改為"部落格""數據庫"改為"資料庫",`,
  31. `"打印"改為"印出""默認"改為"預設""變量""變數"\n`,
  32. `以上為台灣用語的一部分。`,
  33. ].join("")
  34. }, {
  35. name: '解釋程式碼', // 按鈕的顯示名稱
  36. position: 'start', // start = 最前面 , end = 最後面
  37. autoEnter: false, // 是否自動按下 Enter
  38. content: [ // 要被放入輸入框的文字
  39. `請解釋這段程式碼\n\n`,
  40. ].join("")
  41. }, {
  42. name: '改善程式碼', // 按鈕的顯示名稱
  43. position: 'start', // start = 最前面 , end = 最後面
  44. autoEnter: false, // 是否自動按下 Enter
  45. content: [ // 要被放入輸入框的文字
  46. `請改善這段程式碼\n\n`,
  47. ].join("")
  48. }, {
  49. name: '請繼續', // 按鈕的顯示名稱
  50. position: 'start', // start = 最前面 , end = 最後面
  51. autoEnter: false, // 是否自動按下 Enter
  52. content: [ // 要被放入輸入框的文字
  53. `請繼續`,
  54. ].join("")
  55. }
  56. ];
  57.  
  58. // 定義雜七雜八
  59. const HELPER_MENU_TEST = 'input helper'; // 按鈕文字
  60. const CONTAINER_CLASS = 'helper_textcontainer'; // 按鈕用容器
  61. const MAIN_BUTTON_CLASS = 'main_button'; // 選單按鈕
  62. const SETTING_BUTTON_CLASS = 'setting_button'; // 控制按鈕
  63. const MENU_CLASS = 'main_menu'; // 選單
  64.  
  65. const NAV_SELECTOR = 'nav.flex.h-full.flex-1.flex-col.space-y-1.p-2'; // 左邊選單的定位(整體)
  66. const NAV_MENU = 'nav > div.overflow-y-auto'; // 左邊選單的定位(上層)
  67. const TEXT_INPUTBOX_POSITION = 'textarea.m-0'; // 輸入框的定位
  68. const SUBMIT_BUTTON_POSITION = 'button.absolute'; // 送出按鈕的定位
  69. const INPUT_EVENT = new Event('input', { bubbles: true }); // 模擬輸入於輸入框的事件
  70.  
  71.  
  72. // 準備 function
  73. const insertCustomize = (customize, name) => {
  74. // customize = 設定的object
  75. // name = 觸發按鈕的名稱
  76.  
  77. console.log('insertCustomize 開始執行');
  78. const textInputbox = document.querySelector(TEXT_INPUTBOX_POSITION);
  79. const submitButton = document.querySelector(SUBMIT_BUTTON_POSITION);
  80. const item = customize.find(i => i.name === name);
  81.  
  82. if (item) {
  83. console.log(`已找到名稱為 ${name} 的元素`);
  84.  
  85. // start = 最前面 , end = 最後面
  86. // 有想要出游標定位,之後再說
  87. // 例如 `請從 "" 繼續輸出` 就希望定在 "" 之間
  88. if (item.position === 'start') {
  89. textInputbox.value = item.content + textInputbox.value;
  90. } else {
  91. textInputbox.value += item.content;
  92. }
  93.  
  94. // 模擬,讓對話框可以自動更新高度
  95. textInputbox.dispatchEvent(INPUT_EVENT);
  96. textInputbox.focus();
  97.  
  98. console.log(`textInputbox.value =\n${textInputbox.value}`);
  99.  
  100. // 如果需要,自動按下 Enter
  101. if (item.autoEnter) {
  102. console.log('autoEnter (O)');
  103. submitButton.click();
  104. }
  105.  
  106. console.log('====== insertCustomize 結束 ======');
  107. } else {
  108. console.error(`找不到名稱為 ${name} 的元素`);
  109. }
  110. };
  111.  
  112.  
  113. const buttonStyles = {
  114. [`.${CONTAINER_CLASS}`]: {
  115. position: 'relative',
  116. display: 'flex',
  117. alignItems: 'center',
  118. justifyContent: 'center',
  119. borderBottom: '1px solid rgba(255, 255, 255, 0.2)',
  120. border: '1px solid #fff',
  121. borderRadius: '5px',
  122. width: '100%',
  123. boxSizing: 'border-box',
  124. },
  125. [`.${MAIN_BUTTON_CLASS}`]: {
  126. padding: '8px 12px',
  127. fontSize: '14px',
  128. color: '#fff',
  129. border: '1px solid #fff',
  130. backgroundColor: '#525252',
  131. borderRadius: '5px',
  132. cursor: 'pointer',
  133. width: '85%',
  134. boxSizing: 'border-box',
  135. margin: '0 auto', // 水平置中
  136. },
  137. [`.${SETTING_BUTTON_CLASS}`]: {
  138. padding: '8px 12px',
  139. fontSize: '14px',
  140. color: '#fff',
  141. backgroundColor: '#525252',
  142. border: 'none',
  143. borderRadius: '5px',
  144. cursor: 'pointer',
  145. width: '15%',
  146. boxSizing: 'border-box',
  147. },
  148. [`.${MENU_CLASS}`]: {
  149. display: 'none',
  150. position: 'absolute',
  151. left: '100%',
  152. top: 0,
  153. zIndex: 1,
  154. width: '100%',
  155. backgroundColor: '#2b2c2f', // 新增背景色設定
  156. border: '1px solid #fff', // 新增邊框設定
  157. borderRadius: '15px', // 新增圓角設定
  158. },
  159. [`.${MENU_CLASS} button`]: {
  160. display: 'block',
  161. width: '100%',
  162. height: '100%',
  163. padding: '8px 12px',
  164. fontSize: '14px',
  165. color: '#fff',
  166. backgroundColor: '#2b2c2f',
  167. border: '1px solid #fff',
  168. borderRadius: '5px', // 圓角
  169. cursor: 'pointer',
  170. },
  171. };
  172.  
  173.  
  174.  
  175. const createMenu = (containerNode) => {
  176. const menu = document.createElement('div');
  177. menu.id = 'helper_menu';
  178. menu.classList.add(MENU_CLASS);
  179. menu.style.display = 'none';
  180. menu.style.width = `${containerNode.offsetWidth}px`
  181.  
  182. customize.forEach(element => {
  183. const menuItem = createMenuItem(element);
  184. menu.appendChild(menuItem);
  185. });
  186.  
  187. return menu;
  188. };
  189.  
  190. const createMenuItem = (element) => {
  191. const menuItem = document.createElement('button');
  192. menuItem.innerText = element.name;
  193. menuItem.id = element.name;
  194. menuItem.addEventListener('click', (event) => {
  195. console.log('按鈕資訊:', event.target);
  196. insertCustomize(customize, event.target.id);
  197. });
  198.  
  199. return menuItem;
  200. };
  201. const addButton = (containerNode, customize, buttonText = 'Click Me') => {
  202. const createMainButton = () => {
  203. const mainButton = document.createElement('button');
  204. mainButton.innerText = buttonText;
  205. mainButton.classList.add(MAIN_BUTTON_CLASS);
  206. mainButton.style.width = '85%';
  207. return mainButton;
  208. };
  209.  
  210. const createSettingButton = () => {
  211. const settingButton = document.createElement('button');
  212. settingButton.innerText = '⚙️';
  213. settingButton.classList.add(MAIN_BUTTON_CLASS);
  214. settingButton.style.width = '15%';
  215. settingButton.id = 'settingButton';
  216. return settingButton;
  217. };
  218.  
  219. const createButtonContainer = () => {
  220. const buttonContainer = document.createElement('div');
  221. buttonContainer.classList.add(CONTAINER_CLASS);
  222.  
  223. const mainButton = createMainButton();
  224. const settingButton = createSettingButton()
  225.  
  226. buttonContainer.appendChild(settingButton);
  227. buttonContainer.appendChild(mainButton);
  228. return buttonContainer;
  229. };
  230.  
  231.  
  232. const style = document.createElement('style');
  233. style.type = 'text/css';
  234. let cssText = '';
  235. for (let selector in buttonStyles) {
  236. cssText += selector + ' {\n';
  237. for (let property in buttonStyles[selector]) {
  238. cssText += ` ${property}: ${buttonStyles[selector][property]};\n`;
  239. }
  240. cssText += '}\n';
  241. }
  242. style.innerHTML = cssText;
  243.  
  244. const assButton = createButtonContainer()
  245. const menu = createMenu(containerNode);
  246.  
  247. // 滑鼠移入按鈕時,顯示選單
  248. assButton.addEventListener('mouseenter', () => {
  249. menu.style.display = 'block';
  250. });
  251.  
  252. // 用 buttonWrapper 把 button 跟 menu 包起來,才能達到 hover 效果
  253. const buttonWrapper = document.createElement('div');
  254. buttonWrapper.style.width = `${containerNode.offsetWidth}px`;
  255. buttonWrapper.appendChild(assButton);
  256. buttonWrapper.appendChild(menu);
  257.  
  258. containerNode.appendChild(buttonWrapper);
  259. containerNode.appendChild(style);
  260.  
  261. // 滑鼠移出 buttonWrapper 時,隱藏選單
  262. buttonWrapper.addEventListener('mouseleave', () => {
  263. menu.style.display = 'none';
  264. });
  265. console.log('已新增按鈕');
  266. };
  267. function showPopup() {
  268. // 找到 settingButton 元素
  269. const settingButton = document.getElementById('settingButton');
  270.  
  271. // 當點擊 settingButton 時觸發事件
  272. settingButton.addEventListener('click', () => {
  273. // 創建彈出視窗
  274. const popup = document.createElement('div');
  275. popup.style.position = 'fixed';
  276. popup.style.top = '50%';
  277. popup.style.left = '50%';
  278. popup.style.transform = 'translate(-50%, -50%)';
  279. popup.style.background = '#525467';
  280. popup.style.border = '1px solid black';
  281. popup.style.padding = '30px';
  282. popup.style.width = '80%';
  283. popup.style.maxWidth = '800px';
  284. popup.style.height = '60%';
  285. popup.style.maxHeight = '1200px';
  286.  
  287. popup.style.zIndex = '9999';
  288.  
  289. // 創建新增按鈕
  290. const addButton = document.createElement('button');
  291. addButton.textContent = '新增(add)';
  292. addButton.style.margin = '10px';
  293. addButton.style.border = '2px solid #ffffff';
  294. addButton.addEventListener('click', () => {
  295. // 新增一個 item
  296. const newItem = {
  297. name: '',
  298. position: '',
  299. content: ''
  300. };
  301. customize.push(newItem);
  302. renderTable();
  303. });
  304. popup.appendChild(addButton);
  305.  
  306. // 創建編輯按鈕
  307. const editButton = document.createElement('button');
  308. editButton.textContent = '編輯(edit)';
  309. editButton.style.margin = '10px';
  310. editButton.style.border = '2px solid #ffffff';
  311. editButton.addEventListener('click', () => {
  312. // 編輯一個 item
  313. const index = prompt('請輸入要編輯的編號(edit index)');
  314. if (index && index >= 1 && index <= customize.length) {
  315. const item = customize[index - 1];
  316.  
  317. // 編輯 name
  318. const newName = prompt('請輸入新的 name', item.name);
  319. if (newName !== null) {
  320. item.name = newName;
  321. }
  322.  
  323. // 編輯 position
  324. do {
  325. newPosition = prompt('請輸入新的 position (只能輸入 start 或 end)', item.position);
  326. } while (newPosition !== null && newPosition !== 'start' && newPosition !== 'end');
  327. if (newPosition !== null) {
  328. item.position = newPosition;
  329. }
  330.  
  331. // 編輯 position
  332. do {
  333. newAutoEnter = prompt('請輸入新的 AutoEnter (只能輸入 y 或 n)', item.autoEnter);
  334. } while (newAutoEnter !== null && newAutoEnter !== 'y' && newAutoEnter !== 'n');
  335. if (newAutoEnter !== null) {
  336. if (newAutoEnter === 'y') {
  337. item.autoEnter = true;
  338. } else {
  339. item.autoEnter = false;
  340. }
  341. }
  342.  
  343. // 編輯 content
  344. // const textarea = document.createElement('textarea');
  345. // textarea.value = item.content;
  346. // textarea.style.width = '100%';
  347. // textarea.style.height = '100px';
  348. const newContent = prompt('請輸入新的 content', item.content);
  349. if (newContent !== null) {
  350. item.content = newContent;
  351. }
  352.  
  353. // 重新渲染表格
  354. renderTable();
  355. } else {
  356. alert('輸入的編號不合法');
  357. }
  358. });
  359. popup.appendChild(editButton);
  360.  
  361. // 創建刪除按鈕
  362. const deleteButton = document.createElement('button');
  363. deleteButton.textContent = '刪除(delete)';
  364. deleteButton.style.margin = '10px';
  365. deleteButton.style.border = '2px solid #ffffff';
  366. deleteButton.addEventListener('click', () => {
  367. // 刪除一個 item
  368. const index = prompt('請輸入要刪除的編號(delete index)');
  369. if (index && index >= 1 && index <= customize.length) {
  370. customize.splice(index - 1, 1);
  371. renderTable();
  372. } else {
  373. alert('輸入的編號不合法 (invalid index)');
  374. }
  375. });
  376. popup.appendChild(deleteButton);
  377.  
  378. // 創建關閉按鈕
  379. const closeButton = document.createElement('button');
  380. closeButton.textContent = '儲存並離開(save&exit)';
  381. closeButton.style.position = 'absolute';
  382. closeButton.style.top = '5px';
  383. closeButton.style.right = '5px';
  384. closeButton.addEventListener('click', () => {
  385. console.log(customize);
  386. // 儲存修改後的 customize 資料
  387. GM_setValue('customizeData', customize);
  388.  
  389. // // 重寫一次 helper_menu
  390. // const helper_menu = document.getElementById('helper_menu');
  391. // const menu = createMenu(helper_menu);
  392. // helper_menu.replaceWith(menu);
  393.  
  394. // 上面的做不出來
  395. // 所以只好重新整理頁面
  396. location.reload();
  397.  
  398. document.body.removeChild(popup);
  399. });
  400. popup.appendChild(closeButton);
  401.  
  402. // 創建表格
  403. const table = document.createElement('table');
  404. popup.appendChild(table);
  405.  
  406. // 創建表頭
  407. const thead = document.createElement('thead');
  408. const tr = document.createElement('tr');
  409. const th1 = document.createElement('th');
  410. const th2 = document.createElement('th');
  411. const th3 = document.createElement('th');
  412. const th4 = document.createElement('th');
  413. const th5 = document.createElement('th');
  414. th1.textContent = '編號(index)';
  415. th2.textContent = '名稱(name)';
  416. th3.textContent = '位置(position)';
  417. th4.textContent = '自動輸入(autoEnter)?';
  418. th5.textContent = '內容(content)';
  419. tr.appendChild(th1);
  420. tr.appendChild(th2);
  421. tr.appendChild(th3);
  422. tr.appendChild(th4);
  423. tr.appendChild(th5);
  424. thead.appendChild(tr);
  425. table.appendChild(thead);
  426.  
  427. // 創建表身
  428. const tbody = document.createElement('tbody');
  429. table.appendChild(tbody);
  430.  
  431. // 渲染表格
  432. function renderTable() {
  433. // 先清空表格內容
  434. tbody.innerHTML = '';
  435.  
  436. // 重新渲染表格
  437. customize.forEach((item, index) => {
  438. const tr = document.createElement('tr');
  439. const td1 = document.createElement('td');
  440. const td2 = document.createElement('td');
  441. const td3 = document.createElement('td');
  442. const td4 = document.createElement('td');
  443. const td5 = document.createElement('td');
  444. td1.textContent = index + 1;
  445. td2.textContent = item.name;
  446. td3.textContent = item.position;
  447. td4.textContent = item.autoEnter;
  448. td5.textContent = item.content;
  449. tr.appendChild(td1);
  450. tr.appendChild(td2);
  451. tr.appendChild(td3);
  452. tr.appendChild(td4);
  453. tr.appendChild(td5);
  454. tbody.appendChild(tr);
  455. });
  456. }
  457.  
  458. // 渲染初始表格
  459. renderTable();
  460.  
  461. // 點擊彈窗外的地方關閉彈窗
  462. popup.addEventListener('click', (event) => {
  463. if (event.target === popup) {
  464. document.body.removeChild(popup);
  465. }
  466. });
  467.  
  468. // 將彈出視窗加入頁面中
  469. document.body.appendChild(popup);
  470. });
  471. }
  472.  
  473.  
  474. // // 失敗的監控(((
  475. // // 現在只能用 setInterval + setTimeout 去添加按鈕
  476. // const observeElementChanges = (trigger, selector, callback) => {
  477. // const triggerNode = document.querySelector(trigger);
  478. // const aimsNode = document.querySelector(selector);
  479.  
  480. // if (!triggerNode) {
  481. // console.warn(`目標元素 ${selector} 不存在,無法監控`);
  482. // return;
  483. // }
  484.  
  485. // const observer = new MutationObserver((mutationsList) => {
  486. // for (const mutation of mutationsList) {
  487. // if (mutation.type === 'childList') {
  488. // callback(mutation, aimsNode);
  489. // }
  490. // }
  491. // });
  492. // console.log(`目標元素 ${selector} 存在,監控中`);
  493. // observer.observe(triggerNode, { childList: true });
  494. // };
  495.  
  496. const main = () => {
  497. const container = document.getElementById('helper_menu');
  498. let GM_customize = GM_getValue('customizeData', customize); // 讀取 customize 數據
  499. if (GM_customize) {
  500. customize = GM_customize;
  501. } else {
  502. customize = init_customize;
  503. GM_setValue('customizeData', customize);
  504. }
  505.  
  506. //找不到就新增
  507. if (!container) {
  508. const aimsNode = document.querySelector(NAV_MENU);
  509. // 新增一個容器
  510. const container = document.createElement('div');
  511. container.classList.add(CONTAINER_CLASS);
  512. container.id = 'helper_menu';
  513. container.style.width = `${aimsNode.offsetWidth}px`; // 設定 container 寬度為父元素寬度
  514.  
  515. // 將容器元素插入到目標元素後面
  516. aimsNode.parentNode.insertBefore(container, aimsNode.nextSibling);
  517.  
  518. // 新增一個按鈕元素
  519. addButton(container, customize, HELPER_MENU_TEST);
  520.  
  521. showPopup()
  522. }
  523. };
  524.  
  525. const checkUrl = () => {
  526. if (window.location.href !== checkUrl.currentUrl) {
  527. console.log(`網址變更為 ${window.location.href}`);
  528. checkUrl.currentUrl = window.location.href;
  529. setTimeout(main, 100);
  530. }
  531. };
  532.  
  533. (() => {
  534. // 妥協的結果,用 setInterval + setTimeout 去添加按鈕
  535. // 之後一定要改好!!!
  536. checkUrl.currentUrl = window.location.href;
  537. setInterval(checkUrl, 1000);
  538. setTimeout(main, 2000);
  539. // GM_deleteValue('customizeData');
  540. })();