Friendlier ChatGPT Prompt Box

Allows you to just start typing with unfocused prompt box, scroll the conversation from the prompt box, quote pasted text, and more. Adds free space at the end of the conversation, so you can prescroll before text generation. Configurable via userscript storage.

  1. // ==UserScript==
  2. // @name Friendlier ChatGPT Prompt Box
  3. // @description Allows you to just start typing with unfocused prompt box, scroll the conversation from the prompt box, quote pasted text, and more. Adds free space at the end of the conversation, so you can prescroll before text generation. Configurable via userscript storage.
  4. //
  5. // @namespace http://tampermonkey.net/
  6. // @version 2023.11.16
  7. //
  8. // @author Henrik Hank
  9. // @license MIT (https://opensource.org/license/mit/)
  10. //
  11. // @match *://chat.openai.com/*
  12. // @run-at document-idle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_setClipboard
  17. // @grant GM_addStyle
  18. // ==/UserScript==
  19.  
  20. void function userscript() {
  21. "use strict";
  22.  
  23. let scrollAmountPx; // In px.
  24. let scrollAmountMultiplicator;
  25. let scrollContainerEndSpaceVH; // In `vh` CSS units.
  26. let scrollContainerEndSpaceHexColor;
  27. let lastPrompt = "";
  28. let hasJustPressedCtrlV = false;
  29. let lastSelectionStart = 0;
  30.  
  31. const SCROLL_CONTAINER_CSS_SELECTOR = "main > [role='presentation'] > .overflow-hidden > .h-full > [class^='react-scroll-to-bottom--']";
  32. const PROMPT_BOX_ID = "prompt-textarea";
  33.  
  34.  
  35. void function main() {
  36. // Retrieve user settings.
  37. scrollAmountPx = GM_getValue("scrollAmountCSSPx");
  38. if (! Number.isSafeInteger(scrollAmountPx)) {
  39. scrollAmountPx = 90;
  40. }
  41.  
  42. scrollAmountMultiplicator = GM_getValue("scrollAmountMultiplicator");
  43. if (! Number.isFinite(scrollAmountMultiplicator)) {
  44. scrollAmountMultiplicator = 4;
  45. }
  46.  
  47. scrollContainerEndSpaceVH = GM_getValue("conversationExtraEndSpaceViewportPct");
  48. if (! Number.isSafeInteger(scrollContainerEndSpaceVH) || scrollContainerEndSpaceVH < 0 || scrollContainerEndSpaceVH > 100) {
  49. scrollContainerEndSpaceVH = 65;
  50. }
  51.  
  52. scrollContainerEndSpaceHexColor = GM_getValue("conversationExtraEndSpaceRGBOrRGBAHexColor");
  53. const match = (scrollContainerEndSpaceHexColor ?? "").toString().match(/^#?([0-9a-f]{6})([0-9a-f]{2})?$/i);
  54. if (match != null) {
  55. scrollContainerEndSpaceHexColor = `#${match[1]}${match[2] ?? "ff"}`;
  56. } else {
  57. scrollContainerEndSpaceHexColor = "#80800011";
  58. }
  59.  
  60. // Make settings available for editing by persisting them.
  61. GM_setValue("scrollAmountCSSPx", scrollAmountPx);
  62. GM_setValue("scrollAmountMultiplicator", scrollAmountMultiplicator);
  63. GM_setValue("conversationExtraEndSpaceViewportPct", scrollContainerEndSpaceVH);
  64. GM_setValue("conversationExtraEndSpaceRGBOrRGBAHexColor", scrollContainerEndSpaceHexColor);
  65.  
  66. // CSS.
  67. GM_addStyle(`
  68. ${SCROLL_CONTAINER_CSS_SELECTOR} > div::after {
  69. content: "";
  70. height: ${scrollContainerEndSpaceVH}vh;
  71. background-color: ${scrollContainerEndSpaceHexColor};
  72. }
  73. `);
  74.  
  75. // Event handlers (on body, because DOM will be patched).
  76. document.body.addEventListener("keydown", onBodyKeyDown);
  77. document.body.addEventListener("input", onBodyInput);
  78. GM_registerMenuCommand("Copy Last Prompt (Empty After Reload)", onCopyLastPromptCommand);
  79. }.call();
  80.  
  81.  
  82. function onBodyKeyDown(event) {
  83. // Reject events purely about modifier keys.
  84. if (event.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT || event.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) {
  85. return;
  86. }
  87.  
  88. // Normalize modifiers.
  89. let modifiers = "";
  90. if (event.shiftKey) {
  91. modifiers += "Shift";
  92. }
  93. if (event.ctrlKey) {
  94. modifiers += "Ctrl";
  95. }
  96. if (event.altKey) {
  97. modifiers += "Alt";
  98. }
  99. if (event.metaKey) {
  100. modifiers += "Meta";
  101. }
  102.  
  103. const keyCombo = `${modifiers}+${event.key}`;
  104.  
  105. // With focused prompt box.
  106. if (document.activeElement.id === PROMPT_BOX_ID) {
  107. const promptBox = document.activeElement;
  108.  
  109. // Edit selection, just pasted text or current line.
  110. if (keyCombo === "Ctrl+q" || keyCombo === "Ctrl+b") {
  111. const origCursorIndex = promptBox.selectionDirection === "backward" ? promptBox.selectionStart : promptBox.selectionEnd;
  112.  
  113. const hasSelection = promptBox.selectionStart !== promptBox.selectionEnd;
  114. if (! hasSelection) {
  115. if (hasJustPressedCtrlV) {
  116. // Select pasted text.
  117. promptBox.setSelectionRange(lastSelectionStart, promptBox.selectionStart);
  118. }
  119. }
  120.  
  121. // Extend selection to full start and end line.
  122. let selectionStart = promptBox.selectionStart;
  123. while (selectionStart > 0 && promptBox.value[selectionStart - 1] !== "\n") {
  124. selectionStart--;
  125. }
  126.  
  127. let selectionEnd = promptBox.selectionEnd;
  128. while (selectionEnd < promptBox.value.length - 1 && promptBox.value[selectionEnd - 1] !== "\n") {
  129. selectionEnd++;
  130. }
  131.  
  132. promptBox.setSelectionRange(selectionStart, selectionEnd);
  133.  
  134. // Quote.
  135. if (keyCombo === "Ctrl+q") {
  136. editSelection(promptBox, origCursorIndex, { forLine: (line) => {
  137. if (line.length === 0) {
  138. // Save AI tokens.
  139. return [ ">", 1 ];
  140. } else {
  141. return [ "> " + line, 2 ];
  142. }
  143. } });
  144. }
  145.  
  146. // Make code block.
  147. else if (keyCombo === "Ctrl+b") {
  148. editSelection(promptBox, origCursorIndex, { forSelection: (selection) => {
  149. return [ "```\n" + selection.replace(/(?=\n$|$)/, "\n```"), 4 ];
  150. } });
  151. }
  152.  
  153. notifyAboutInput(promptBox);
  154. event.preventDefault();
  155. }
  156.  
  157. // Scroll with focused prompt box.
  158. else {
  159. if (
  160. (promptBox.value === "" || event.ctrlKey) &&
  161. (event.key === "ArrowUp" || event.key === "ArrowDown")
  162. ) {
  163. const scrollContainer = document.querySelector(SCROLL_CONTAINER_CSS_SELECTOR);
  164.  
  165. const amount = scrollAmountPx * (event.shiftKey ? scrollAmountMultiplicator : 1);
  166. scrollContainer?.scrollBy({
  167. left: 0,
  168. top: event.key === "ArrowUp" ? -amount : amount,
  169. behavior: "smooth",
  170. });
  171.  
  172. event.preventDefault();
  173. }
  174. }
  175.  
  176. //
  177. hasJustPressedCtrlV = keyCombo === "Ctrl+v";
  178. lastSelectionStart = promptBox.selectionStart;
  179. }
  180.  
  181. // With unfocused prompt box.
  182. else {
  183. // Focus prompt box.
  184. if (keyCombo === "Ctrl+p") {
  185. document.getElementById(PROMPT_BOX_ID)?.focus();
  186. event.preventDefault();
  187. }
  188.  
  189. // Start typing into prompt box while still unfocused.
  190. else {
  191. const hasDialog = document.querySelector("[role='dialog'][data-state='open']") != null;
  192. const otherTextboxFocused = [ "INPUT" /*renaming convo*/, "TEXTAREA" /*editing prev. msg.*/ ].includes(document.activeElement.tagName);
  193. const hasOnlyAllowedModifiers = modifiers === "" || modifiers === "Shift";
  194.  
  195. if (! hasDialog && ! otherTextboxFocused && hasOnlyAllowedModifiers) {
  196. const promptBox = document.getElementById(PROMPT_BOX_ID);
  197. let acted = false;
  198.  
  199. if (
  200. /^[ \p{Letter}\p{Number}\p{Punctuation}\p{Symbol}]$/ui.test(event.key) &&
  201. ! (event.key === " " && document.activeElement.tagName === "BUTTON")
  202. ) {
  203. promptBox.setRangeText(event.key, promptBox.selectionStart, promptBox.selectionEnd, "end");
  204. acted = true;
  205. } else if (event.key === "Backspace") {
  206. if (promptBox.selectionStart === promptBox.selectionEnd) {
  207. const start = Math.max(promptBox.selectionStart - 1, 0);
  208. promptBox.setRangeText("", start, promptBox.selectionEnd, "end");
  209. } else {
  210. promptBox.setRangeText("", promptBox.selectionStart, promptBox.selectionEnd, "end");
  211. }
  212. acted = true;
  213. }
  214.  
  215. if (acted) {
  216. promptBox.focus();
  217. notifyAboutInput(promptBox);
  218. event.preventDefault();
  219. }
  220. }
  221. }
  222.  
  223. //
  224. hasJustPressedCtrlV = false;
  225. }
  226. }
  227.  
  228.  
  229. function onBodyInput(event) {
  230. if (event.target.id === PROMPT_BOX_ID) {
  231. lastPrompt = event.target.value;
  232. }
  233. }
  234.  
  235.  
  236. function onCopyLastPromptCommand() {
  237. GM_setClipboard(lastPrompt, "text");
  238. }
  239.  
  240.  
  241. function editSelection(textarea, cursorIndex, { forLine, forSelection }) {
  242. const origSelectionStart = textarea.selectionStart;
  243. const origSelectionEnd = textarea.selectionEnd;
  244. let selection = textarea.value.substring(origSelectionStart, origSelectionEnd);
  245. const isCursorAtEnd = cursorIndex === origSelectionEnd;
  246. let cursorAdvancement;
  247.  
  248. if (forLine != null) {
  249. const lines = selection.split("\n");
  250.  
  251. const origCursorIndex = cursorIndex;
  252. let origLineStartIndex = origSelectionStart;
  253.  
  254. const numLines = lines.at(-1) === "" ? lines.length - 1 : lines.length;
  255. for (let i = 0; i < numLines; i++) {
  256. const origLine = lines[i];
  257. [lines[i], cursorAdvancement] = forLine(origLine);
  258. if (origCursorIndex > origLineStartIndex) {
  259. cursorIndex += cursorAdvancement;
  260. }
  261. origLineStartIndex += origLine.length + 1;
  262. }
  263.  
  264. selection = lines.join("\n");
  265. }
  266.  
  267. if (forSelection != null) {
  268. [selection, cursorAdvancement] = forSelection(selection);
  269. cursorIndex += cursorAdvancement;
  270. }
  271.  
  272. textarea.setRangeText(selection, origSelectionStart, origSelectionEnd);
  273.  
  274. if (isCursorAtEnd) {
  275. cursorIndex = origSelectionStart + selection.length;
  276. }
  277. textarea.setSelectionRange(cursorIndex, cursorIndex);
  278. }
  279.  
  280.  
  281. /**
  282. * Make official ChatGPT code save the current prompt box contents. Otherwise, the change will be undone when unfocusing the prompt box.
  283. */
  284. function notifyAboutInput(textarea) {
  285. textarea.dispatchEvent(new Event("input", { bubbles: true }));
  286. }
  287. }.call();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址