OpenAI Playground Hotkeys and Accessibility Enhancements

OpenAI playground accessibility enhancements with keyboard navigation. Quickly edit/delete/expand messages.

目前为 2025-04-17 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name OpenAI Playground Hotkeys and Accessibility Enhancements
  3. // @namespace http://tampermonkey.net/
  4. // @license MIT
  5. // @author Rekt
  6. // @version 1.1.0
  7. // @description OpenAI playground accessibility enhancements with keyboard navigation. Quickly edit/delete/expand messages.
  8. // @match https://platform.openai.com/playground/prompts*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Tracks if user is editing a message
  16. let isEditing = false;
  17.  
  18. // --- Message Box and Action Selectors ---
  19. function getMessageBoxes() {
  20. return Array.from(document.querySelectorAll("div.LT9iz > div > div > div:nth-child(1) > div"));
  21. }
  22.  
  23. function getButtonsBarInMessageBox(msgBox) {
  24. return msgBox.querySelector(".rqgky > ._5qprQ");
  25. }
  26.  
  27. function getButtonsInMessageBox(msgBox) {
  28. let btnBar = getButtonsBarInMessageBox(msgBox);
  29. if (!btnBar) return [];
  30. return Array.from(btnBar.querySelectorAll("button")).filter(btn => btn.offsetParent !== null);
  31. }
  32.  
  33. // --- Message Type Detection ---
  34. function isAssistantMessage(msgBox) {
  35. let roleElement = msgBox.querySelector("div");
  36. if (roleElement && roleElement.innerText) {
  37. return roleElement.innerText.toLowerCase().includes("assistant");
  38. }
  39. let boxes = getMessageBoxes();
  40. let idx = boxes.indexOf(msgBox);
  41. return idx !== -1 && idx % 2 === 1;
  42. }
  43.  
  44. // --- Robust Edit Button Selector ---
  45. function getEditButton(msgBox) {
  46. let btnBar = getButtonsBarInMessageBox(msgBox);
  47. if (!btnBar) return null;
  48. let isAssistant = isAssistantMessage(msgBox);
  49. let btns = getButtonsInMessageBox(msgBox);
  50.  
  51. if (isAssistant) {
  52. let btn = btnBar.querySelector("button:nth-child(1)");
  53. if (btn && btn.offsetParent !== null) return btn;
  54. } else {
  55. let btn = btnBar.querySelector("button:nth-child(4)");
  56. if (btn && btn.offsetParent !== null) return btn;
  57. }
  58.  
  59. for (let btn of btns) {
  60. let svg = btn.querySelector("svg");
  61. let ariaLabel = svg ? svg.getAttribute("aria-label") : "";
  62. if (
  63. (btn.innerText && btn.innerText.toLowerCase().includes('edit')) ||
  64. (btn.title && btn.title.toLowerCase().includes('edit')) ||
  65. (ariaLabel && ariaLabel.toLowerCase().includes('edit'))
  66. ) {
  67. return btn;
  68. }
  69. }
  70. return null;
  71. }
  72.  
  73. // --- Delete Button Selector ---
  74. function getDeleteButton(msgBox) {
  75. let btnBar = getButtonsBarInMessageBox(msgBox);
  76. if (!btnBar) return null;
  77. let btns = getButtonsInMessageBox(msgBox);
  78. for (let btn of btns) {
  79. let svg = btn.querySelector("svg");
  80. let ariaLabel = svg ? svg.getAttribute("aria-label") : "";
  81. if (
  82. (btn.innerText && btn.innerText.toLowerCase().includes('delete')) ||
  83. (btn.title && btn.title.toLowerCase().includes('delete')) ||
  84. (ariaLabel && ariaLabel.toLowerCase().includes('delete')) ||
  85. (ariaLabel && ariaLabel.toLowerCase().includes('trash'))
  86. ) {
  87. return btn;
  88. }
  89. }
  90. if (btns.length) return btns[btns.length - 1];
  91. return null;
  92. }
  93.  
  94. // --- Expand Button Selector ---
  95. function getExpandButton(msgBox) {
  96. let btn = msgBox.querySelector(".G3JSV .BODGE > button");
  97. return btn && btn.offsetParent !== null ? btn : null;
  98. }
  99.  
  100. // --- Keyboard + Click State ---
  101. let selectedIdx = 0;
  102.  
  103. // --- Highlight/Select Logic ---
  104. function highlightSelected(noScroll) {
  105. const boxes = getMessageBoxes();
  106. boxes.forEach((box, idx) => {
  107. box.style.outline = "";
  108. box.style.zIndex = "";
  109. box.dataset.vimnavSelected = "";
  110. });
  111. if (boxes[selectedIdx]) {
  112. if (!noScroll) boxes[selectedIdx].scrollIntoView({block: "center", behavior: "smooth"});
  113. boxes[selectedIdx].style.outline = "2px solid #00ffd0";
  114. boxes[selectedIdx].style.zIndex = "10";
  115. boxes[selectedIdx].dataset.vimnavSelected = "1";
  116. }
  117. }
  118.  
  119. // --- Input Blocking ---
  120. function notInInput() {
  121. const ae = document.activeElement;
  122. if (!ae) return true;
  123. if (ae.matches("input, textarea, select")) return false;
  124. if (ae.closest("[contenteditable='true']")) return false;
  125. return true;
  126. }
  127.  
  128. function inContentEditable() {
  129. const ae = document.activeElement;
  130. if (!ae) return false;
  131. if (ae.closest("[contenteditable='true']")) return true;
  132. return false;
  133. }
  134.  
  135. // --- Move Selection ---
  136. function moveSelection(delta) {
  137. const boxes = getMessageBoxes();
  138. if (!boxes.length) return;
  139. selectedIdx = Math.max(0, Math.min(selectedIdx + delta, boxes.length - 1));
  140. highlightSelected();
  141. }
  142.  
  143. // --- Toggle Edit for Selected Message ---
  144. function toggleEditOfSelectedMessage() {
  145. const boxes = getMessageBoxes();
  146. if (!boxes.length || selectedIdx >= boxes.length) return;
  147. const box = boxes[selectedIdx];
  148. const btn = getEditButton(box);
  149. if (btn) {
  150. btn.click();
  151. }
  152. }
  153.  
  154. // --- Toggle Expand for Selected Message ---
  155. function expandSelectedMessage() {
  156. const boxes = getMessageBoxes();
  157. if (!boxes.length || selectedIdx >= boxes.length) return;
  158. const box = boxes[selectedIdx];
  159. const btn = getExpandButton(box);
  160. if (btn) btn.click();
  161. }
  162.  
  163. // --- Keyboard Navigation, Expand/Contract, Toggle Edit, Delete ---
  164. document.addEventListener("keydown", function(e) {
  165. if (e.key === 'Escape' && inContentEditable()) {
  166. toggleEditOfSelectedMessage();
  167. e.preventDefault();
  168. return;
  169. }
  170. if (isEditing) return; // Prevent navigation when editing
  171. if (!notInInput()) return;
  172. const boxes = getMessageBoxes();
  173. if (!boxes.length) return;
  174.  
  175. if (e.key === 'j' || e.key === 'ArrowDown') {
  176. moveSelection(+1);
  177. e.preventDefault();
  178. } else if (e.key === 'k' || e.key === 'ArrowUp') {
  179. moveSelection(-1);
  180. e.preventDefault();
  181. } else if (e.key === 'e') {
  182. toggleEditOfSelectedMessage();
  183. e.preventDefault();
  184. } else if (e.key === 'x' || e.key === 'Delete') {
  185. let box = boxes[selectedIdx];
  186. let btn = getDeleteButton(box);
  187. if (btn) {
  188. btn.click();
  189. e.preventDefault();
  190. }
  191. } else if (e.key === ' ' || e.key === 'Spacebar') {
  192. if (!inContentEditable()) {
  193. expandSelectedMessage();
  194. e.preventDefault();
  195. }
  196. }
  197. });
  198.  
  199. // --- Click/DoubleClick for Selection & Expanding ---
  200. function handleMsgBoxClick(e, idx) {
  201. selectedIdx = idx;
  202. highlightSelected(true);
  203. }
  204.  
  205. function handleMsgBoxDoubleClick(e, idx) {
  206. selectedIdx = idx;
  207. highlightSelected(true);
  208. expandSelectedMessage();
  209. }
  210.  
  211. // --- On New Messages/UI Mutation: Maintain Highlight, Re-install Click Listeners ---
  212. function ensureHighlightAndListeners() {
  213. const boxes = getMessageBoxes();
  214. boxes.forEach((box, idx) => {
  215. if (!box.dataset.vimnavClickSet) {
  216. box.addEventListener('click', e => handleMsgBoxClick(e, idx));
  217. box.addEventListener('dblclick', e => handleMsgBoxDoubleClick(e, idx));
  218. box.dataset.vimnavClickSet = '1';
  219. }
  220. // Add focus/blur listeners to editable elements within message boxes
  221. const editableElements = box.querySelectorAll("[contenteditable='true'], input, textarea");
  222. editableElements.forEach(el => {
  223. el.addEventListener('focus', () => {
  224. isEditing = true;
  225. });
  226. el.addEventListener('blur', () => {
  227. isEditing = false;
  228. });
  229. });
  230. });
  231. if (selectedIdx >= boxes.length) selectedIdx = boxes.length - 1;
  232. if (selectedIdx < 0) selectedIdx = 0;
  233. highlightSelected(true);
  234. }
  235.  
  236. // Initial Setup
  237. setTimeout(ensureHighlightAndListeners, 1500);
  238.  
  239. // Observe DOM Changes
  240. const observer = new MutationObserver(ensureHighlightAndListeners);
  241. observer.observe(document.body, {childList: true, subtree: true});
  242.  
  243. // Expose for Debugging
  244. window.OpenAIPlaygroundMsgNav = {
  245. getMessageBoxes,
  246. highlightSelected,
  247. moveSelection,
  248. getEditButton,
  249. getDeleteButton,
  250. expandSelectedMessage,
  251. toggleEditOfSelectedMessage
  252. };
  253. })();