OpenAI Playground Hotkeys and Accessibility Enhancements

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
    };
})();