OpenAI Playground Hotkeys and Accessibility Enhancements

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

// ==UserScript==
// @name         OpenAI Playground Hotkeys and Accessibility Enhancements
// @namespace    http://tampermonkey.net/
// @license      MIT
// @author       Rekt
// @version      1.1.0
// @description  OpenAI playground accessibility enhancements with keyboard navigation. Quickly edit/delete/expand messages.
// @match        https://platform.openai.com/playground/prompts*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Tracks if user is editing a message
    let isEditing = false;

    // --- Message Box and Action Selectors ---
    function getMessageBoxes() {
        return Array.from(document.querySelectorAll("div.LT9iz > div > div > div:nth-child(1) > div"));
    }

    function getButtonsBarInMessageBox(msgBox) {
        return msgBox.querySelector(".rqgky > ._5qprQ");
    }

    function getButtonsInMessageBox(msgBox) {
        let btnBar = getButtonsBarInMessageBox(msgBox);
        if (!btnBar) return [];
        return Array.from(btnBar.querySelectorAll("button")).filter(btn => btn.offsetParent !== null);
    }

    // --- Message Type Detection ---
    function isAssistantMessage(msgBox) {
        let roleElement = msgBox.querySelector("div");
        if (roleElement && roleElement.innerText) {
            return roleElement.innerText.toLowerCase().includes("assistant");
        }
        let boxes = getMessageBoxes();
        let idx = boxes.indexOf(msgBox);
        return idx !== -1 && idx % 2 === 1;
    }

    // --- Robust Edit Button Selector ---
    function getEditButton(msgBox) {
        let btnBar = getButtonsBarInMessageBox(msgBox);
        if (!btnBar) return null;
        let isAssistant = isAssistantMessage(msgBox);
        let btns = getButtonsInMessageBox(msgBox);

        if (isAssistant) {
            let btn = btnBar.querySelector("button:nth-child(1)");
            if (btn && btn.offsetParent !== null) return btn;
        } else {
            let btn = btnBar.querySelector("button:nth-child(4)");
            if (btn && btn.offsetParent !== null) return btn;
        }

        for (let btn of btns) {
            let svg = btn.querySelector("svg");
            let ariaLabel = svg ? svg.getAttribute("aria-label") : "";
            if (
                (btn.innerText && btn.innerText.toLowerCase().includes('edit')) ||
                (btn.title && btn.title.toLowerCase().includes('edit')) ||
                (ariaLabel && ariaLabel.toLowerCase().includes('edit'))
            ) {
                return btn;
            }
        }
        return null;
    }

    // --- Delete Button Selector ---
    function getDeleteButton(msgBox) {
        let btnBar = getButtonsBarInMessageBox(msgBox);
        if (!btnBar) return null;
        let btns = getButtonsInMessageBox(msgBox);
        for (let btn of btns) {
            let svg = btn.querySelector("svg");
            let ariaLabel = svg ? svg.getAttribute("aria-label") : "";
            if (
                (btn.innerText && btn.innerText.toLowerCase().includes('delete')) ||
                (btn.title && btn.title.toLowerCase().includes('delete')) ||
                (ariaLabel && ariaLabel.toLowerCase().includes('delete')) ||
                (ariaLabel && ariaLabel.toLowerCase().includes('trash'))
            ) {
                return btn;
            }
        }
        if (btns.length) return btns[btns.length - 1];
        return null;
    }

    // --- Expand Button Selector ---
    function getExpandButton(msgBox) {
        let btn = msgBox.querySelector(".G3JSV .BODGE > button");
        return btn && btn.offsetParent !== null ? btn : null;
    }

    // --- Keyboard + Click State ---
    let selectedIdx = 0;

    // --- Highlight/Select Logic ---
    function highlightSelected(noScroll) {
        const boxes = getMessageBoxes();
        boxes.forEach((box, idx) => {
            box.style.outline = "";
            box.style.zIndex = "";
            box.dataset.vimnavSelected = "";
        });
        if (boxes[selectedIdx]) {
            if (!noScroll) boxes[selectedIdx].scrollIntoView({block: "center", behavior: "smooth"});
            boxes[selectedIdx].style.outline = "2px solid #00ffd0";
            boxes[selectedIdx].style.zIndex = "10";
            boxes[selectedIdx].dataset.vimnavSelected = "1";
        }
    }

    // --- Input Blocking ---
    function notInInput() {
        const ae = document.activeElement;
        if (!ae) return true;
        if (ae.matches("input, textarea, select")) return false;
        if (ae.closest("[contenteditable='true']")) return false;
        return true;
    }

    function inContentEditable() {
        const ae = document.activeElement;
        if (!ae) return false;
        if (ae.closest("[contenteditable='true']")) return true;
        return false;
    }

    // --- Move Selection ---
    function moveSelection(delta) {
        const boxes = getMessageBoxes();
        if (!boxes.length) return;
        selectedIdx = Math.max(0, Math.min(selectedIdx + delta, boxes.length - 1));
        highlightSelected();
    }

    // --- Toggle Edit for Selected Message ---
    function toggleEditOfSelectedMessage() {
        const boxes = getMessageBoxes();
        if (!boxes.length || selectedIdx >= boxes.length) return;
        const box = boxes[selectedIdx];
        const btn = getEditButton(box);
        if (btn) {
            btn.click();
        }
    }

    // --- Toggle Expand for Selected Message ---
    function expandSelectedMessage() {
        const boxes = getMessageBoxes();
        if (!boxes.length || selectedIdx >= boxes.length) return;
        const box = boxes[selectedIdx];
        const btn = getExpandButton(box);
        if (btn) btn.click();
    }

    // --- Keyboard Navigation, Expand/Contract, Toggle Edit, Delete ---
    document.addEventListener("keydown", function(e) {
        if (e.key === 'Escape' && inContentEditable()) {
            toggleEditOfSelectedMessage();
            e.preventDefault();
            return;
        }
        if (isEditing) return; // Prevent navigation when editing
        if (!notInInput()) return;
        const boxes = getMessageBoxes();
        if (!boxes.length) return;

        if (e.key === 'j' || e.key === 'ArrowDown') {
            moveSelection(+1);
            e.preventDefault();
        } else if (e.key === 'k' || e.key === 'ArrowUp') {
            moveSelection(-1);
            e.preventDefault();
        } else if (e.key === 'e') {
            toggleEditOfSelectedMessage();
            e.preventDefault();
        } else if (e.key === 'x' || e.key === 'Delete') {
            let box = boxes[selectedIdx];
            let btn = getDeleteButton(box);
            if (btn) {
                btn.click();
                e.preventDefault();
            }
        } else if (e.key === ' ' || e.key === 'Spacebar') {
            if (!inContentEditable()) {
                expandSelectedMessage();
                e.preventDefault();
            }
        }
    });

    // --- Click/DoubleClick for Selection & Expanding ---
    function handleMsgBoxClick(e, idx) {
        selectedIdx = idx;
        highlightSelected(true);
    }

    function handleMsgBoxDoubleClick(e, idx) {
        selectedIdx = idx;
        highlightSelected(true);
        expandSelectedMessage();
    }

    // --- On New Messages/UI Mutation: Maintain Highlight, Re-install Click Listeners ---
    function ensureHighlightAndListeners() {
        const boxes = getMessageBoxes();
        boxes.forEach((box, idx) => {
            if (!box.dataset.vimnavClickSet) {
                box.addEventListener('click', e => handleMsgBoxClick(e, idx));
                box.addEventListener('dblclick', e => handleMsgBoxDoubleClick(e, idx));
                box.dataset.vimnavClickSet = '1';
            }
            // Add focus/blur listeners to editable elements within message boxes
            const editableElements = box.querySelectorAll("[contenteditable='true'], input, textarea");
            editableElements.forEach(el => {
                el.addEventListener('focus', () => {
                    isEditing = true;
                });
                el.addEventListener('blur', () => {
                    isEditing = false;
                });
            });
        });
        if (selectedIdx >= boxes.length) selectedIdx = boxes.length - 1;
        if (selectedIdx < 0) selectedIdx = 0;
        highlightSelected(true);
    }

    // Initial Setup
    setTimeout(ensureHighlightAndListeners, 1500);

    // Observe DOM Changes
    const observer = new MutationObserver(ensureHighlightAndListeners);
    observer.observe(document.body, {childList: true, subtree: true});

    // Expose for Debugging
    window.OpenAIPlaygroundMsgNav = {
        getMessageBoxes,
        highlightSelected,
        moveSelection,
        getEditButton,
        getDeleteButton,
        expandSelectedMessage,
        toggleEditOfSelectedMessage
    };
})();

QingJ © 2025

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