Greasy Fork 还支持 简体中文。

OpenAI Playground Hotkeys and Accessibility Enhancements

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

目前為 2025-04-16 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name OpenAI Playground Hotkeys and Accessibility Enhancements
  3. // @namespace http://tampermonkey.net/
  4. // @license MIT
  5. // @version 1.0.0
  6. // @description OpenAI playground accessibility enhancements with keyboard navigation. Quickly edit/delete/expand messages.
  7. // @match https://platform.openai.com/playground/prompts*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // --- Message Box and Action Selectors ---
  15. function getMessageBoxes() {
  16. return Array.from(document.querySelectorAll("div.LT9iz > div > div > div:nth-child(1) > div"));
  17. }
  18.  
  19. function getButtonsBarInMessageBox(msgBox) {
  20. return msgBox.querySelector(".rqgky > ._5qprQ");
  21. }
  22.  
  23. function getButtonsInMessageBox(msgBox) {
  24. let btnBar = getButtonsBarInMessageBox(msgBox);
  25. if (!btnBar) return [];
  26. return Array.from(btnBar.querySelectorAll("button")).filter(btn => btn.offsetParent !== null);
  27. }
  28.  
  29. // --- Message Type Detection ---
  30. function isAssistantMessage(msgBox) {
  31. // Check for role label (e.g., "Assistant" or "User")
  32. let roleElement = msgBox.querySelector("div"); // Adjust selector if specific class exists
  33. if (roleElement && roleElement.innerText) {
  34. return roleElement.innerText.toLowerCase().includes("assistant");
  35. }
  36. // Fallback to index-based: even = user, odd = assistant
  37. let boxes = getMessageBoxes();
  38. let idx = boxes.indexOf(msgBox);
  39. return idx !== -1 && idx % 2 === 1;
  40. }
  41.  
  42. // --- Robust Edit Button Selector ---
  43. function getEditButton(msgBox) {
  44. let btnBar = getButtonsBarInMessageBox(msgBox);
  45. if (!btnBar) return null;
  46. let isAssistant = isAssistantMessage(msgBox);
  47. let btns = getButtonsInMessageBox(msgBox);
  48.  
  49. // Precise selector based on message type
  50. if (isAssistant) {
  51. let btn = btnBar.querySelector("button:nth-child(1)");
  52. if (btn && btn.offsetParent !== null) return btn;
  53. } else {
  54. let btn = btnBar.querySelector("button:nth-child(4)");
  55. if (btn && btn.offsetParent !== null) return btn;
  56. }
  57.  
  58. // Fallback: scan for edit button by text, title, or aria-label
  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 (!notInInput()) return;
  171. const boxes = getMessageBoxes();
  172. if (!boxes.length) return;
  173.  
  174. if (e.key === 'j' || e.key === 'ArrowDown') {
  175. moveSelection(+1);
  176. e.preventDefault();
  177. } else if (e.key === 'k' || e.key === 'ArrowUp') {
  178. moveSelection(-1);
  179. e.preventDefault();
  180. } else if (e.key === 'e') {
  181. toggleEditOfSelectedMessage();
  182. e.preventDefault();
  183. } else if (e.key === 'x' || e.key === 'Delete') {
  184. let box = boxes[selectedIdx];
  185. let btn = getDeleteButton(box);
  186. if (btn) {
  187. btn.click();
  188. e.preventDefault();
  189. }
  190. } else if (e.key === ' ' || e.key === 'Spacebar') {
  191. if (!inContentEditable()) {
  192. expandSelectedMessage();
  193. e.preventDefault();
  194. }
  195. }
  196. });
  197.  
  198. // --- Click/DoubleClick for Selection & Expanding ---
  199. function handleMsgBoxClick(e, idx) {
  200. selectedIdx = idx;
  201. highlightSelected(true);
  202. }
  203.  
  204. function handleMsgBoxDoubleClick(e, idx) {
  205. selectedIdx = idx;
  206. highlightSelected(true);
  207. expandSelectedMessage();
  208. }
  209.  
  210. // --- On New Messages/UI Mutation: Maintain Highlight, Re-install Click Listeners ---
  211. function ensureHighlightAndListeners() {
  212. const boxes = getMessageBoxes();
  213. boxes.forEach((box, idx) => {
  214. if (!box.dataset.vimnavClickSet) {
  215. box.addEventListener('click', e => handleMsgBoxClick(e, idx));
  216. box.addEventListener('dblclick', e => handleMsgBoxDoubleClick(e, idx));
  217. box.dataset.vimnavClickSet = '1';
  218. }
  219. });
  220. if (selectedIdx >= boxes.length) selectedIdx = boxes.length - 1;
  221. if (selectedIdx < 0) selectedIdx = 0;
  222. highlightSelected(true);
  223. }
  224.  
  225. // Initial Setup
  226. setTimeout(ensureHighlightAndListeners, 1500);
  227.  
  228. // Observe DOM Changes
  229. const observer = new MutationObserver(ensureHighlightAndListeners);
  230. observer.observe(document.body, {childList: true, subtree: true});
  231.  
  232. // Expose for Debugging
  233. window.OpenAIPlaygroundMsgNav = {
  234. getMessageBoxes,
  235. highlightSelected,
  236. moveSelection,
  237. getEditButton,
  238. getDeleteButton,
  239. expandSelectedMessage,
  240. toggleEditOfSelectedMessage
  241. };
  242. })();