Read Aloud Speedster

Set a default playback speed for Read Aloud on ChatGPT.com, browse between messages via navigation buttons, and open a settings menu by clicking the speed display to toggle additional UI tweaks. Features include color-coded icons under ChatGPT's responses; highlighted color for bold text; dark and light mode support; compact sidebar with separators; square design, and much more.

Installer ce script?
Script suggéré par l'auteur

Vous pourriez également aimer YouTube Alchemy.

Installer ce script
// ==UserScript==
// @name         Read Aloud Speedster
// @description  Set a default playback speed for Read Aloud on ChatGPT.com, browse between messages via navigation buttons, and open a settings menu by clicking the speed display to toggle additional UI tweaks. Features include color-coded icons under ChatGPT's responses; highlighted color for bold text; dark and light mode support; compact sidebar with separators; square design, and much more.
// @author       Tim Macy
// @license      AGPL-3.0-or-later
// @version      4.0
// @namespace    TimMacy.ReadAloudSpeedster
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @match        https://*.chatgpt.com/*
// @grant        GM.setValue
// @grant        GM.getValue
// @run-at       document-start
// @homepageURL  https://github.com/TimMacy/ReadAloudSpeedster
// @supportURL   https://github.com/TimMacy/ReadAloudSpeedster/issues
// ==/UserScript==

/************************************************************************
*                                                                       *
*                    Copyright © 2025 Tim Macy                          *
*                    GNU Affero General Public License v3.0             *
*                    Version: 4.0 - Read Aloud Speedster                *
*                                                                       *
*             Visit: https://github.com/TimMacy                         *
*                                                                       *
************************************************************************/

(function() {
    'use strict';
    const className = "sm:mt-5";
    const escapedClassName = CSS.escape(className);
    const styleSheet = document.createElement('style');
    styleSheet.textContent = `
        /**************************************
                 default root settings
        **************************************/

        :root {
            --user-chat-width: 100%; // original 70%
            --sidebar-width: 260px;
            --sidebar-section-margin-top: 1.25rem;
            --sidebar-section-first-margin-top: .5rem;
            --sidebar-rail-width: calc(var(--spacing)*13);
            --header-height: calc(var(--spacing)*13);
            --white: #fff;
            --black: #000;
            --gray-50: #f9f9f9;
            --gray-100: #ececec;
            --gray-200: #e3e3e3;
            --gray-300: #cdcdcd;
            --gray-400: #b4b4b4;
            --gray-500: #9b9b9b;
            --gray-600: #676767;
            --gray-700: #424242;
            --gray-750: #2f2f2f;
            --gray-800: #212121;
            --gray-900: #171717;
            --gray-950: #0d0d0d;
            --red-500: #e02e2a;
            --red-700: #911e1b;
            --brand-purple: #ab68ff;
            --yellow-900: #4d3b00;
        }

        /**************************************
                    general settings
        **************************************/

        /* chatbox - reduced vertical margin */
        .${escapedClassName} {
            margin-top: .5rem !important;
            margin-bottom: .25rem !important;
        }

        /* chatbox - fade effect for content */
        main form {
            border-top-left-radius: .25em !important;
            border-top-right-radius: .25em !important;
        }

        #thread-bottom-container {
            box-shadow: 0 -20px 20px 0px var(--main-surface-primary) !important;
        }

        /* copy icon */
        button[aria-label="Copy"],
        div[role="menuitem"]:has(path[d^="M12 7.1a"]),
        header button:has(path[d^="M12.668 10.667C12"]),
        button[data-testid="copy-turn-action-button"] svg,
        button.surface-nav-element:has(svg path[d^="M12 7.1a"]) {
            color: darkorange !important;
            opacity:.9;
        }

        /* copied */
        button:has(svg path[d^="M15.483"]) {
            color: springgreen;
        }

        .light button:has(svg path[d^="M15.483"]) {
            color: limegreen;
        }

        /* thumbs up icon */
        button .icon-md path[d^="M12.1318"],
        button svg path[d^="M10.9153"],
        button[aria-label="Good response"],
        div[role="menuitem"]:has(path[d^="m4.5 4.944"]),
        button[data-testid="good-response-turn-action-button"] svg {
            color: #00ad00 !important;
            opacity:.9;
        }

        /* thumbs down icon */
        button[aria-label="Bad response"],
        button .icon-md path[d^="M11.8727"],
        button svg path[d^="M12.6687"],
        button.surface-nav-element:has(svg path[d^="M11.868 21"]),
        button[data-testid="bad-response-turn-action-button"] svg {
            color: crimson !important;
            opacity:.9;
        }

        /* edit in canvas icon */
        button[aria-label="Edit message"],
        button[aria-label="Edit in canvas"],
        button:has(svg path[d^="M12.0303 4.11328"]) {
            color: yellow !important;
            opacity: .8;
        }

        .light button[aria-label="Edit message"],
        .light button[aria-label="Edit in canvas"],
        .light button:has(svg path[d^="M12.0303 4.11328"]) {
            color: indigo !important;
            opacity: .8;
        }

        /* switch model icon */
        main .flex.justify-start button[aria-haspopup="menu"][data-state="closed"] > div {
            color: gray !important;
        }

        .light main .flex.justify-start button[aria-haspopup="menu"][data-state="closed"] > div {
            color: dimgray !important;
        }

        /* read aloud and stop icon */
        button[aria-label="Read aloud"],
        div[role="menuitem"]:has(path[d^="M9 6.25v5.5"]),
        button[data-testid="voice-play-turn-action-button"] svg {
            color: deepskyblue !important;
            opacity:.9;
        }

        button[aria-label="Stop"] {color: deepskyblue !important;}

        /* share icon */
        article button[aria-label="Share"] {
            opacity:.8;
        }

        /* hover opacity icons */
        :is(
            button[aria-label="Copy"],
            div[role="menuitem"]:has(path[d^="M12 7.1a"]),
            header button:has(path[d^="M12.668 10.667C12"]),
            button[data-testid="copy-turn-action-button"] svg,
            button.surface-nav-element:has(svg path[d^="M12 7.1a"]),
            button .icon-md path[d^="M12.1318"],
            button svg path[d^="M10.9153"],
            button[aria-label="Good response"],
            div[role="menuitem"]:has(path[d^="m4.5 4.944"]),
            button[data-testid="good-response-turn-action-button"] svg,
            button[aria-label="Bad response"],
            button .icon-md path[d^="M11.8727"],
            button svg path[d^="M12.6687"],
            button.surface-nav-element:has(svg path[d^="M11.868 21"]),
            button[data-testid="bad-response-turn-action-button"] svg,
            button[aria-label="Edit message"],
            button[aria-label="Edit in canvas"],
            button:has(svg path[d^="M12.0303 4.11328"]),
            .light button[aria-label="Edit message"],
            .light button[aria-label="Edit in canvas"],
            .light button:has(svg path[d^="M12.0303 4.11328"]),
            button[aria-label="Read aloud"],
            div[role="menuitem"]:has(path[d^="M9 6.25v5.5"]),
            button[data-testid="voice-play-turn-action-button"] svg,
            article button[aria-label="Share"]
        ):hover {opacity:1;}

        /* sora star icon */
        a:has(svg path[d^="M9.822 2.077c"]),
        div.pointer-events-none path[d^="M10.258"],
        button.surface-nav-element path[d^="M10.258"],
        div[role="menuitem"]:has(path[d^="M9.822 2.077c"]),
        button.surface-nav-element path[d^="M9.822 2.077c"],
        div[role="menuitem"]:has(path[d^="M10.258 1.555c"]) {
            color: gold;
        }

        /* highlight color - dark mode */
        .markdown strong {
            color: springgreen !important;
        }

        /* highlight color - light mode */
        .light .markdown strong {
            color: darkviolet !important;
        }

        /* red delete color */
        .text-token-text-destructive,
        button:has(path[d^="m10 11.5 4"]),
        [data-testid="delete-chat-menu-item"],
        div[role="menuitem"]:has(path[d^="M10.556 4a1 1 0"]) {
            color: #e02e2a !important;
        }

        .text-token-text-destructive:hover,
        button:has(path[d^="m10 11.5 4"]):hover,
        [data-testid="delete-chat-menu-item"]:hover,
        div[role="menuitem"]:has(path[d^="M10.556 4a1 1 0"]):hover {
            color: white !important;
            background: rgba(255, 0, 0, .5) !important;
        }

        /* sore green restore color */
        div[role="menuitem"]:has(path[d^="m4.5 4.944"]):hover {
            color: white !important;
            background: rgba(0, 255, 0, .5) !important;
        }

        /* stop icon size inner */
        #thread-bottom-container .icon-lg {
            height: calc(var(--spacing)*5);;
            width: calc(var(--spacing)*5);;
        }

        /* select color */
        ::selection {
            background-color: var(--text-primary);
            color: var(--main-surface-tertiary);
        }

        /* change width of chat containers */
        div.text-base.my-auto:has(.bg-token-main-surface-tertiary),
        #thread-bottom-container > div {
            margin: 0 6.263%;
            padding: 0;
        }

        #thread-bottom-container.mb-4.flex.flex-col > #thread-bottom {
            margin: 0 12.525%;
        }

        #thread-bottom > div {
            padding-inline: 0 !important;
            --thread-content-margin: 0 !important;
        }

        [data-message-author-role="user"] > div > div {
            max-width: 100%;
        }

        .px-\\(--thread-content-margin\\):has([data-message-author-role="user"]) {
            margin: 20px 6.263% 20px 37.574%;
            padding: 0;
        }

        .px-\\(--thread-content-margin\\):has([data-message-author-role="assistant"]) {
            margin: 20px 6.263%;
            padding: 0;
        }

        .grow.overflow-hidden > div > div {
            overflow-x:hidden;
        }

        [class^="_tableContainer_"] {
            padding-right: 12.525%;
        }

        .border-token-border-sharp [class^="_tableContainer_"] {
            padding-right: 0;
        }

        .\\[--composer-overlap-px\\:24px\\] {
            --composer-overlap-px: 0;
        }

        .flex.max-w-full.flex-col.grow:empty + .flex.min-h-\\[46px\\].justify-start [class*="mask-image"] {
            margin-left: calc(6.263% - var(--spacing)*6) !important;
        }

        main div.text-base.my-auto:has(.loading-shimmer) {
            padding-left: 6.263%;
        }

        main .mx-\\[calc\\(--spacing\\(-2\\)-1px\\)\\]:not(.loading-shimmer) {
            margin-left:-6px;
        }

        div.text-base,div[class*="turn-messages"] {
            --thread-content-max-width: unset!important;
            max-width: 1129px;
        }

        #prosemirror-editor-container,
        #prosemirror-editor-container > .markdown.prose {
            width: 100% !important;
        }

        main.min-h-0 .h-full.w-full >.justify-center {
            margin: 0 5dvw !important;
        }

        main div.flex.basis-auto.flex-col .pb-25 {
            padding:0;
        }

        :root:has(#stage-slideover-sidebar) main div.flex.basis-auto.flex-col.grow.overflow-hidden > div {
            position: fixed;
            bottom: 125px;
            height: calc(100dvh - 177px);
            width: -webkit-fill-available;
            width: -moz-available;
            width: fill-available;
        }

        /* menu hover shadow fix */
        .shadow-long:is(.dark *) {
            --tw-shadow: 0px 8px 16px 0px var(--tw-shadow-color,#00000052),0px 0px 1px 0px var(--tw-shadow-color,#0000009e) !important;
            box-shadow: var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow) !important;
            border:1px solid #272727 !important;
        }

        .shadow-long {
            --tw-shadow: 0px 8px 12px 0px var(--tw-shadow-color,var(--shadow-color-1,#00000014)),0px 0px 1px 0px var(--tw-shadow-color,var(--shadow-color-2,#0000009e)) !important;
            box-shadow: var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow) !important;
            border: 1px solid #e6e6e6 !important;
        }

        /**************************************
                 Read Aloud Speedster
        **************************************/

        .speed-control-container {
            position: relative;
            display: flex;
            align-items: center;
            margin: 0 8px;
        }

        .speed-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 36px;
            min-width: 36px;
            font-size: .75rem;
            line-height: 1rem;
            font-weight: 600;
            background: transparent;
            color: var(--text-secondary);
            cursor: pointer;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        .speed-btn.minus {
            border-radius: 50%;
            border-right: none;
        }

        .speed-btn.plus {
            border-radius: 50%;
            border-left: none;
        }

        .speed-btn.plus::before,
        .speed-btn.minus::before,
        .speed-display::before,
        .speed-display::after {
            content: '';
            position: absolute;
            width: 1px;
            height: 12px;
            background-color: var(--border-default);
            display: var(--show-dividers, block);
        }

        .speed-btn.plus::before {
            right: 0;
        }

        .speed-btn.minus::before {
            left: 0;
        }

        .speed-display::after {
            transform: translateX(18px);
        }

        .speed-display::before {
            transform: translateX(-18px);
        }

        .speed-btn:hover,.speed-control-config-popup button:hover {
            background-color: #ffffff1a;
        }

        .light .speed-btn:hover,.light .speed-control-config-popup button:hover {
            background-color: #0d0d0d05;
        }

        .speed-btn:active,.speed-control-config-popup button:active {
            background-color: #ffffff0d
        }

        .light .speed-btn:active,.light .speed-control-config-popup button:active {
            background-color: #0d0d0d0d
        }

        .speed-display {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 36px;
            min-width: 36px;
            padding: .5rem;
            font-size: .75rem;
            line-height: 1rem;
            font-weight: 600;
            background: transparent;
            color: var(--text-secondary);
            cursor: default;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        .speed-control-config-popup {
            position: absolute;
            bottom: 100%;
            left: 50%;
            transform: translateX(-50%);
            background: var(--main-surface-primary);
            border: 1px solid var(--border-default);
            border-radius: 3px;
            padding: 15px 30px;
            margin-bottom: 4px;
            z-index: 2077;
            display: none;
            flex-direction: column;
            gap: 10px;
            max-height: 40dvh;
            text-rendering:optimizeLegibility !important;
            -webkit-font-smoothing:antialiased !important;
        }

        .speed-control-config-popup .popup-header {
            display: grid;
            grid-template-columns: 1fr auto 1fr;
            align-items: baseline;
            justify-content: center;
            font-family: -apple-system, "Roboto", "Arial", sans-serif;
            color: var(--text-secondary);
            font-weight: 600;
            width: 100%
            text-decoration: none;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        .speed-control-config-popup .popup-title {
            grid-column: 2;
            text-align: center;
            text-decoration: none;
            text-overflow: ellipsis;
            white-space: normal;
            cursor: pointer;
            display:block;
            opacity: .8;
            cursor: pointer;
            transition: opacity .5s;
        }

        .speed-control-config-popup .popup-content {
            overflow-y: auto;
            overflow-x: hidden;
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 10px;
            padding-bottom: 30px;
        }

        .speed-control-config-popup .popup-footer {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            width: 100%;
        }

        .speed-control-config-popup .popup-footer a {
            font-family: -apple-system, "Roboto", "Arial", sans-serif;
            font-size: .75rem;
            line-height: 1.5em;
            font-weight: 500;
            color: var(--text-secondary);
            text-decoration: none;
            transition: color 0.2s ease-in-out;
        }

        .speed-control-config-popup .popup-footer a:hover { color: #369eff; }

        .CentAnni-version-label {
            grid-column: 3;
            padding: 0;
            margin: 0 0 0 5px;
            color: ghostwhite;
            cursor: default;
            opacity: .3;
            justify-self: start;
            max-width: 10ch;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            font-size: 9px;
            line-height: 1.2;
            transition: opacity .5s;
        }

        .speed-control-config-popup .popup-title:hover,.popup-title:hover + .CentAnni-version-label {
            opacity: 1;
        }

        .speed-control-config-popup .popup-footer::before {
            content: "";
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 3.2rem;
            pointer-events: none;
            box-shadow: 0 -30px 20px 0 var(--main-surface-primary);
        }

        .speed-control-config-popup.show {
            display: flex;
        }

        .speed-control-config-popup input {
            transition: border-color 0.2s ease-in-out;
        }

        .speed-control-config-popup input[type="number"] {
            width: 6ch;
            border: 1px solid rgba(255, 255, 255, 0.27);
            border-radius: 3px;
            background: transparent;
            color: var(--text-primary);
            text-align: center;
            margin-right: 10px;
        }

        .light .speed-control-config-popup input[type="number"] {
            border-color: rgba(0, 0, 0, 0.27);
        }

        .speed-control-config-popup input[type="number"]:hover {
            border-color: color(display-p3 0.1216 0.3059 0.5804)
        }

        .speed-control-config-popup input[type="number"]:focus {
            border-color: color(display-p3 0 0.402 1)
        }

        .speed-control-config-popup .toggle-label {
            width: 100%;
            padding-left: 10px;
        }

        .speed-control-config-popup input[type="checkbox"],
        .speed-control-config-popup .toggle-label:hover {
            text-decoration: underline;
            cursor: pointer;
        }

        .speed-control-config-popup .speed-label {
            user-select: none;
            pointer-events: none;
        }

        .speed-control-config-popup button {
            padding: 4px 8px;
            border: 1px solid rgba(255, 255, 255, 0.27);
            border-radius: 3px;
            background: transparent;
            color: var(--text-secondary);
            cursor: pointer;
        }

        .light .speed-control-config-popup button {
            border-color: rgba(0, 0, 0, 0.27);
        }

        .speed-control-config-popup .toggle-container {
            display: flex;
            align-items: center;
            text-wrap: nowrap;
        }

        .CentAnni-style-nav-btn.enabled  {
            opacity: 1;
        }

        .CentAnni-style-nav-btn.disabled {
            opacity: .5;
        }

        .CentAnni-style-nav-btn:active {
            opacity: .8;
        }
    `;

    // append css
    (document.head
        ? Promise.resolve(document.head)
        : new Promise(resolve => {
            if (document.readyState === 'loading')
                document.addEventListener('DOMContentLoaded', () => resolve(document.head),{once:true});
            else resolve(document.head);
        })
    ).then(head => {
        if (head)
            head.appendChild(styleSheet);
        else {
            document.documentElement.appendChild(styleSheet);
            console.error("Read Aloud Speedster: Failed to find head element. Using backup to append stylesheet.");
        }
    });

    const features = {
        squareDesign: {
            label: 'Square Design',
            enabled: false,
            sheet: null,
            style: `
                /* button 'send prompt' radius */
                button[aria-label="Send prompt"], button[aria-label="Stop streaming"], button[aria-label="Start voice mode"] {
                    border-radius: 4px !important;
                }

                /* button radii */
                .btn, .rounded-full {
                    border-radius: 2px !important;
                }

                /* button minus radius */
                .speed-btn.minus {
                    border-radius: 2px 0 0 2px;
                }

                /* button plus radius */
                .speed-btn.plus {
                    border-radius: 0 2px 2px 0;
                }

                /* chatbox - radius */
                .rounded-md,
                .__menu-item,
                .rounded-xl,
                .rounded-3xl,
                .rounded-b-3xl,
                .rounded-t-3xl,
                .rounded-\\[36px\\],
                .rounded-\\[28px\\],
                .rounded-\\[24px\\],
                .composer-btn::before,
                .surface-popover:before,
                .__menu-item-trailing-btn {
                    border-radius: 2px !important;
                }

                /* popup radii and overlay */
                .rounded-t-2xl,
                .rounded-\\[10px\\],
                .rounded-lg, .rounded-2xl {
                    border-radius: 2px !important;
                }

                /* reply radii */
                .rounded-b-lg,
                .rounded-\\[14px\\],
                .rounded-t-\\[20px\\] {
                    border-radius: 0 !important;
                }

                /* canvas */
                main .text-black\\!,
                #prosemirror-context-children > div,
                main .shadow-xl:not([role="toolbar"]),
                main .shadow-lg:not([role="toolbar"]),
                main div.border-token-border-default.z-70 {
                    border-radius: 0 !important;
                    right: -1px !important;
                    bottom: -1px !important;

                }

                .speed-btn,
                .speed-display,
                .composer-btn:enabled {
                    border: 1px solid var(--border-default);
                }

                :root {
                    --show-dividers: none !important;
                }

                .bg-token-border-default {
                    background-color: transparent;
                }

                button.composer-btn[data-pill="true"][aria-haspopup="menu"] {
                    margin-left: 8px;
                }

                main div:has(.loading-shimmer) a>span.rounded-ee-full,
                main div:has(.loading-shimmer) a>span.rounded-se-full {
                    border-start-end-radius: 2px;
                    border-end-end-radius: 2px;
                }
            `
        },
        darkerMode: {
            label: "Darker Background for Header and Chatbox",
            enabled: false,
            sheet: null,
            style: `
                main form > div:first-child {
                    background-color: #141414 !important;
                    border: 1px solid #2d2d2d;
                }

                .h-header-height {
                    background: #181818 !important;
                }
            `
        },
        keepIconsVisible: {
            label: "Keep Icons Visible",
            enabled: false,
            sheet: null,
            style: `
                main [class*="[mask-image"] {
                    mask-image: none !important;
                    -webkit-mask-image: none !important;
                }
                .group\\/turn-messages .pointer-events-none.opacity-0 {
                    opacity: 1 !important;
                    pointer-events: auto !important;
                }
            `
        },
        reduceAnimation: {
            label: "No Icon Animation",
            enabled: false,
            sheet: null,
            style: `
                .motion-safe\\:transition-opacity {
                    transition-duration: unset;
                    transition-property: none;
                    transition-timing-function: unset;
                }
            `
        },
        jumpToChat: {
            label: "Navigate to User's Responses Instead of ChatGPT's",
            enabled: false,
            sheet: null,
            style: ``
        },
        hideShareIcon: {
            label: "Hide Share Icon",
            enabled: false,
            sheet: null,
            style: `
                article button[aria-label="Share"] {
                    display: none;
                }
            `
        },
        hidePlusAvatar: {
            label: "Hide Plus/Pro Icon in Avatar",
            enabled: false,
            sheet: null,
            style: `
                header button[aria-label="Open profile menu"] span,
                main button[aria-label="Open Profile Menu"] span span,
                #page-header #conversation-header-actions button[aria-label="Open profile menu"] span {
                    display: none;
                }
            `
        },
        hideViewPlans: {
            label: "Hide 'View plans'",
            enabled: true,
            sheet: null,
            style: `
                div.__menu-item:has(svg path[d^="M8.44824"]) {
                    display: none !important;
                }
            `
        },
        hideGetProBtn: {
            label: "Hide 'Get Pro' Button",
            enabled: true,
            sheet: null,
            style: `
                .flex > button.btn-primary:first-child:last-child {
                    display: none;
                }
            `
        },
        hideDictateBtn: {
            label: "Hide 'Dictate' Button",
            enabled: false,
            sheet: null,
            style: `
                button[aria-label="Dictate button"] {
                    display: none;
                }
            `
        },
        disableVoiceModeBtn: {
            label: "Disable Voice Mode Button",
            enabled: false,
            sheet: null,
            style: `
                button[aria-label="Start voice mode"] {
                    pointer-events: none;
                    opacity: 0.5;
                }
            `
        },
        hideMistakesTxt: {
            label: "Hide 'ChatGPT can make mistakes' Text",
            enabled: false,
            sheet: null,
            style: `
                div.text-token-text-secondary[class*="md:px-"] {
                    display: none;
                }
                .xl\\:px-5, main form {
                    padding-bottom: 1rem;
                }
            `
        },
        codexNxtSora: {
            label: "Codex and Sora Buttons Next to Each Other",
            enabled: true,
            sheet: null,
            style: `
                nav > aside > a.group.__menu-item#sora,
                nav > aside > a.group.__menu-item[href="/codex"] {
                    width:calc(50% - 6px);
                }

                nav > aside > a.group.__menu-item#sora {
                    transform:translate(100%,-100%);
                    margin-bottom:-36px;
                }
            `
        },
        projectNxtMore: {
            label: "'New project' and 'See more' Buttons Next to Each Other",
            enabled: true,
            sheet: null,
            style: `
                nav #snorlax-heading {
                    display:flex;
                    flex-direction:column;
                }

                nav > #snorlax-heading > div:first-child,
                nav > #snorlax-heading > div:last-child {
                    width:calc(50% - 6px);
                }

                nav > #snorlax-heading > div:last-child {
                    position: absolute;
                    transform:translateX(100%);
                    flex-direction:row-reverse;
                    padding:8px 10px 8px 20px;
                    order:-1;
                }
            `
        },
        navIconsUp: {
            label: "Compact Search and Library Buttons",
            enabled: true,
            sheet: null,
            style: `
                nav > aside > a:has(svg path[d^="M2.6687"]),
                nav > aside > div:has(svg path[d^="M14.0857"]) div.text-token-text-tertiary,
                nav > aside > a:has(svg path[d^="M9.38759"]) div.text-token-text-tertiary {
                    display: none;
                }

                .tall\\:top-header-height,
                nav > aside.last\\:mb-5.mt-\\(--sidebar-section-first-margin-top\\) {
                    height: 0;
                    padding:0;
                    margin-bottom: -8px;
                }

                nav > aside > div:has(svg path[d^="M14.0857"]),nav > aside > a:has(svg path[d^="M9.38759"]) {
                    margin: 0;
                    z-index: 31;
                    color: var(--text-tertiary);
                }

                nav > aside > div:has(svg path[d^="M14.0857"]) {
                    transform: translate(52px, -44px);
                    width: 40px;
                }

                nav > aside > a:has(svg path[d^="M9.38759"]) {
                    transform: translate(100px, -80px);
                    width: 92px;
                }

                nav > aside > div:has(svg path[d^="M14.0857"]):hover,
                nav > aside > a:has(svg path[d^="M9.38759"]):hover,
                nav button:has(svg path[d^="M6.83496"]):hover {
                    color:var(--text-primary);
                }
            `
        },
        sidebarSections: {
            label: "Compact Sidebar with Separators",
            enabled: true,
            sheet: null,
            style: `
                nav .__menu-item:not(:has(svg path[d^="M14.0857"])):not(:has(svg path[d^="M9.38759"])),
                nav .__menu-item-trailing-btn {
                    min-height: calc(var(--spacing)*8);
                    max-height:32px;
                }

                .self-stretch {
                    align-self:center;
                }

                nav .__menu-item-trailing-btn:hover {
                    background: rgba(255, 255, 255, .1);
                }

                nav .light .__menu-item-trailing-btn:hover {
                    background: rgba(1, 1, 1, .1);
                }

                nav .mt-\\(--sidebar-section-first-margin-top\\),
                nav .pt-\\(--sidebar-section-first-margin-top\\),
                nav .mt-\\(--sidebar-section-margin-top\\),
                nav .pt-\\(--sidebar-section-margin-top\\) {
                    margin-top: 10px!important;
                    padding: 0!important;
                }

                nav .mt-\\(--sidebar-section-first-margin-top\\)::before,
                nav .pt-\\(--sidebar-section-first-margin-top\\)::before,
                nav .mt-\\(--sidebar-section-margin-top\\)::before,
                nav .pt-\\(--sidebar-section-margin-top\\)::before {
                    content: '';
                    position: absolute;
                    width: 100%;
                    height: 1px;
                    background-color: color(srgb 1 1 1 / 0.17);
                    display: block;
                    transform: translateY(-5px);
                }

                nav .light .mt-\\(--sidebar-section-first-margin-top\\)::before,
                nav .light .pt-\\(--sidebar-section-first-margin-top\\)::before,
                nav .light .mt-\\(--sidebar-section-margin-top\\)::before,
                nav .light .pt-\\(--sidebar-section-margin-top\\)::before {
                    background-color: color(srgb 0 0 0 / 0.17);
                }

                nav .tall\\:top-header-height {
                    margin-top:0!important;
                }

                nav .tall\\:top-header-height::before {
                    background-color:transparent;
                }

                .tall\\:top-header-height,
                nav > aside.last\\:mb-5.mt-\\(--sidebar-section-first-margin-top\\) {
                    margin-bottom: -10px!important;
                }

                nav > #history > aside > h2 {
                    padding:3px 10px 0 10px;
                }
            `
        },
        justifyText: {
            label: "Justify Text",
            enabled: true,
            sheet: null,
            style: `
                .markdown {
                    text-align: justify;
                }

                .markdown h1 {
                    text-align: left;
                }
            `
        },
        removeFocusOutlines: {
            label: "Remove Focus Outlines (used for keyboard users and screen readers)",
            enabled: false,
            sheet: null,
            style: `
                :focus {
                    outline: none;
                    box-shadow: 0 0 0 0 transparent;
                }
            `
        },
    };

    function applyFeature(key) {
        const feature = features[key];
        if (!feature) return;
        if (feature.enabled) {
            if (feature.style && !feature.sheet) {
                feature.sheet = document.createElement('style');
                feature.sheet.textContent = feature.style;
                document.head.appendChild(feature.sheet);
            }
        } else if (feature.sheet) {
            feature.sheet.remove();
            feature.sheet = null;
        }
    }

    // load feature settings from config or use defaults
    const loadCSSsettings = async () => {
        // Apply defaults immediately
        for (const key in features) {
            applyFeature(key);
        }

        // Fetch stored values concurrently
        const entries = await Promise.all(
            Object.keys(features).map(async key => [key, await GM.getValue(key)])
        );

        for (const [key, value] of entries) {
            if (value !== undefined) {
                features[key].enabled = value;
                applyFeature(key);
            }
        }
    };loadCSSsettings();

    let speedDisplayElement = null;
    let playingAudio = new Set();
    let playListener = null;
    let rateListener = null;
    let controlsContainer = null;
    let configPopup = null;
    let observer = null;
    let playbackSpeed = 1;
    let ignoreRateChange = false;
    let lastUserRate = playbackSpeed;
    let savedSpeed;

    const MIN_SPEED = 1;
    const MAX_SPEED = 17;
    const DELTA = 0.25;

    // load playback speed
    async function initializeSpeed() {
        savedSpeed = await GM.getValue('defaultSpeed', 1);
        playbackSpeed = savedSpeed;
        lastUserRate = playbackSpeed;

        updateSpeedDisplay();
        setPlaybackSpeed();
    }

    // set playback speed and manage listeners
    function setPlaybackSpeed() {
        playingAudio.forEach(audio => audio.playbackRate = playbackSpeed);

        if (!playListener) {
            playListener = e => {
                const audio = e.target;
                if (!(audio instanceof HTMLAudioElement)) return;
                audio.playbackRate = playbackSpeed;
                playingAudio.add(audio);

                const remove = () => {playingAudio.delete(audio);};
                audio.addEventListener('pause',remove,{once:true});
                audio.addEventListener('ended',remove,{once:true});
            };
            document.addEventListener('play',playListener,true);
        }

        if (!rateListener) {
            rateListener = e => {
                const audio = e.target;
                if (!(audio instanceof HTMLAudioElement)) return;
                if (ignoreRateChange) {ignoreRateChange = false;return;}
                audio.playbackRate = lastUserRate;
            };
            document.addEventListener('ratechange',rateListener,true);
        }
    }

    // config popup
    function createConfigPopup() {
        if (configPopup) {
            document.removeEventListener('click', handleDocumentClick);
            configPopup.remove();
        }

        configPopup = document.createElement('div');
        configPopup.classList.add('speed-control-config-popup');

        const headerWrapper = document.createElement('div');
        headerWrapper.classList.add('popup-header');

        const title = document.createElement('a');
        title.href = 'https://github.com/TimMacy/ReadAloudSpeedster';
        title.target = '_blank';
        title.rel = 'noopener';
        title.textContent = 'Read Aloud Speedster';
        title.title = 'GitHub Repository for Read Aloud Speedster';
        title.classList.add('popup-title');

        const versionSpan = document.createElement('span');
        const scriptVersion = GM.info.script.version;
        versionSpan.textContent = `v${scriptVersion}`;
        versionSpan.classList.add('CentAnni-version-label');

        headerWrapper.appendChild(title);
        headerWrapper.appendChild(versionSpan);

        const content = document.createElement('div');
        content.classList.add('popup-content');

        // input for speed
        const speedContainer = document.createElement('div');
        speedContainer.classList.add('toggle-container');

        const speedLabel = document.createElement('span');
        speedLabel.classList.add('speed-label');
        speedLabel.textContent = 'Default Playback Speed';

        const input = document.createElement('input');
        input.id = 'defaultSpeedInput';
        input.type = 'number';
        input.min = MIN_SPEED;
        input.max = MAX_SPEED;
        input.step = DELTA;
        input.value = savedSpeed;

        speedContainer.appendChild(input);
        speedContainer.appendChild(speedLabel);
        content.appendChild(speedContainer);

        const toggleElements = [];
        for (const key in features) {
            const container = document.createElement('div');
            container.classList.add('toggle-container');
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = `${key}Toggle`;
            checkbox.checked = features[key].enabled;
            const label = document.createElement('label');
            label.classList.add('toggle-label');
            label.textContent = features[key].label;
            label.htmlFor = checkbox.id;
            container.appendChild(checkbox);
            container.appendChild(label);
            toggleElements.push({ key, checkbox });
            content.appendChild(container);
        }

        // save button
        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save';

        async function handleSave() {
            const newSpeed = parseFloat(input.value);
            if (newSpeed >= MIN_SPEED && newSpeed <= MAX_SPEED) {
                await GM.setValue('defaultSpeed', newSpeed);
                playbackSpeed = newSpeed;
                updateSpeedDisplay();
                setPlaybackSpeed();
            }

            let navChanged = false;
            for (const { key,checkbox } of toggleElements) {
                if (features[key].enabled !== checkbox.checked) {
                    features[key].enabled = checkbox.checked;
                    await GM.setValue(key,features[key].enabled);
                    applyFeature(key);
                    if (key === 'jumpToChat') navChanged = true;
                }
            }

            if (navChanged) {
                navCleanup?.();
                navCleanup = navBtns();
            }

            configPopup.classList.remove('show');
        }

        saveButton.classList.add('save-button');
        saveButton.addEventListener('click', handleSave);

        document.addEventListener('click', handleDocumentClick);

        configPopup.appendChild(headerWrapper);
        configPopup.appendChild(content);

        const footer = document.createElement('div');
        footer.classList.add('popup-footer');

        const copyrightLink = document.createElement('a');
        copyrightLink.href = 'https://github.com/TimMacy';
        copyrightLink.target = '_blank';
        copyrightLink.rel = 'noopener';
        copyrightLink.textContent = 'Copyright © 2025 Tim Macy';
        copyrightLink.title = 'Copyright © 2025 Tim Macy';

        footer.appendChild(copyrightLink);
        footer.appendChild(saveButton);

        configPopup.appendChild(footer);
        document.body.appendChild(configPopup);

        return configPopup;
    }

    function handleDocumentClick(e) {
        if (!configPopup.contains(e.target) && !e.target.classList.contains('speed-display')) {
            configPopup.classList.remove('show');
        }
    }

    // speed display
    function updateSpeedDisplay() {
        if (speedDisplayElement) {
            speedDisplayElement.textContent = `${playbackSpeed}x`; // raw speed value without formatting
        }
    }

    // create controls
    function createControlButtons() {
        if (controlsContainer && document.body.contains(controlsContainer)) return;

        controlsContainer = document.createElement('div');
        controlsContainer.classList.add('speed-control-container');

        const minusButton = document.createElement('button');
        minusButton.textContent = '-';
        minusButton.classList.add('speed-btn', 'minus');

        const speedDisplay = document.createElement('span');
        speedDisplay.classList.add('speed-display');
        speedDisplay.textContent = `${playbackSpeed}x`; // raw speed value without formatting
        speedDisplayElement = speedDisplay;

        const plusButton = document.createElement('button');
        plusButton.textContent = '+';
        plusButton.classList.add('speed-btn', 'plus');

        function handleMinus() {
            ignoreRateChange = true;
            playbackSpeed = Math.max(MIN_SPEED, playbackSpeed - DELTA);
            lastUserRate = playbackSpeed;
            updateSpeedDisplay();
            setPlaybackSpeed();
        }

        function handlePlus() {
            ignoreRateChange = true;
            playbackSpeed = Math.min(MAX_SPEED, playbackSpeed + DELTA);
            lastUserRate = playbackSpeed;
            updateSpeedDisplay();
            setPlaybackSpeed();
        }

        function handleSpeedClick(e) {
            e.stopPropagation();
            if (!configPopup || !document.body.contains(configPopup)) {
                configPopup = createConfigPopup();
            }
            configPopup.classList.toggle('show');

            if (configPopup.classList.contains('show')) {
                const rect = e.target.getBoundingClientRect();
                configPopup.style.position = 'absolute';
                configPopup.style.bottom = `${window.innerHeight - rect.top + 10}px`;
                configPopup.style.left = `${rect.left + (rect.width / 2)}px`;
                configPopup.style.transform = 'translateX(-50%)';
            }
        }

        minusButton.addEventListener('click', handleMinus);
        plusButton.addEventListener('click', handlePlus);
        speedDisplay.addEventListener('click', handleSpeedClick);

        controlsContainer.appendChild(minusButton);
        controlsContainer.appendChild(speedDisplay);
        controlsContainer.appendChild(plusButton);

        const target = document.querySelector('div[style*="var(--vt-composer-system-hint-action)"]');
        if (target) target.insertAdjacentElement('beforebegin', controlsContainer);
        else if (document.querySelector('div[style*="var(--vt-composer-attach-file-action)"]')?.insertAdjacentElement('afterend', controlsContainer));
    }

    // message navigation button section
    const HEADER_OFFSET = 52;
    const UP_ARROW_PATH = 'M10 3.293l-6.354 6.353a1 1 0 001.414 1.414L9 6.414V17a1 1 0 102 0V6.414l3.939 3.939a1 1 0 001.415-1.414L10 3.293z';
    const DOWN_ARROW_PATH = 'M10 16.707l6.354-6.353a1 1 0 00-1.414-1.414L11 13.586V3a1 1 0 10-2 0v10.586L5.061 8.94a1 1 0 10-1.415 1.415L10 16.707z';
    const createIcon = (pathData) => {
        const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
        svg.setAttribute('width','20');
        svg.setAttribute('height','20');
        svg.setAttribute('viewBox','0 0 20 20');
        svg.setAttribute('fill','currentColor');
        const path = document.createElementNS('http://www.w3.org/2000/svg','path');
        path.setAttribute('fill-rule','evenodd');
        path.setAttribute('clip-rule','evenodd');
        path.setAttribute('d',pathData);
        svg.appendChild(path);
        const wrapper = document.createElement('div');
        wrapper.className = 'flex w-full items-center justify-center';
        wrapper.appendChild(svg);
        return wrapper;
    };

    const createNavButton = (pathData,label) => {
        const btn = document.createElement('button');
        btn.className = 'CentAnni-style-nav-btn btn relative btn-ghost text-token-text-primary';
        btn.setAttribute('aria-label',label);
        btn.appendChild(createIcon(pathData));
        return btn;
    };

    const upBtn = createNavButton(UP_ARROW_PATH, 'Jump to previous message');
    const downBtn = createNavButton(DOWN_ARROW_PATH, 'Jump to next message');

    let navCleanup = null;
    function navBtns() {
        const targetChat = document.querySelector('main > #thread div.flex.basis-auto.flex-col.grow');
        const actions = document.querySelector('#conversation-header-actions');
        const shareBtn = actions.querySelector('button[aria-label="Share"]');
        if (!shareBtn || !actions || !targetChat) return;

        let chatObserver = null;
        let messageCache = [];

        const role = features.jumpToChat?.enabled?'user':'assistant';
        const queryMessages = () => Array.from(targetChat.querySelectorAll(`[data-message-author-role="${role}"]:not([data-message-id^="placeholder-request"])`));
        const populateCache = () => {messageCache = queryMessages();};

        const getNextMessage = () => {
            const current = window.scrollY + HEADER_OFFSET;
            for (const msg of messageCache) {
                const top = msg.getBoundingClientRect().top + window.scrollY;
                if (top > current + 1) return msg;
            }
            return null;
        };

        const getPrevMessage = () => {
            const current = window.scrollY + HEADER_OFFSET;
            for (let i = messageCache.length - 1; i >= 0; i--) {
                const top = messageCache[i].getBoundingClientRect().top + window.scrollY;
                if (top < current - 1) return messageCache[i];
            }
            return null;
        };

        const checkForNewBelow = () => {
            const msgs = queryMessages();
            if (msgs.length > messageCache.length) {
                const newMsgs = msgs.slice(messageCache.length);
                messageCache.push(...newMsgs);
                return newMsgs[0];
            }
            return null;
        };

        const setState = (btn, enabled) => {
            btn.classList.toggle("enabled",enabled);
            btn.classList.toggle("disabled",!enabled);
        };

        const update = () => {
            setState(upBtn,!!getPrevMessage());
            setState(downBtn,!!getNextMessage());
        };

        const jump = (prev) => {
            let target = prev?getPrevMessage():getNextMessage();
            if (!prev && !target) target = checkForNewBelow();
            if (target) target.scrollIntoView({behavior:'auto',block:'start'});
            update();
        };

        const createButtons = () => {
            if (document.body.contains(upBtn)) return;

            upBtn.onclick = () => jump(true);
            downBtn.onclick = () => jump(false);

            actions.insertBefore(downBtn,shareBtn);
            actions.insertBefore(upBtn,downBtn);

            populateCache();
            startObserver();
        };

        const startObserver = () => {
            if (chatObserver||!targetChat) return;
            if (queryMessages().length) {
                populateCache();
                update();
                return;
            }

            chatObserver = new MutationObserver(() => {
                if (queryMessages().length) {
                    populateCache();
                    stopObserver();
                    setTimeout(update,250);
                }
            });
            chatObserver.observe(targetChat,{childList:true});
        };

        const stopObserver = () => {
            if (chatObserver) {
                chatObserver.disconnect();
                chatObserver = null;
            }
        };

        createButtons();

        return () => {
            stopObserver();
            messageCache = [];
            upBtn.remove();
            downBtn.remove();
        };
    }

    // handle cleanup
    function cleanup() {
        if (playListener) {
            document.removeEventListener('play',playListener,true);
            playListener = null;
        }
        if (rateListener) {
            document.removeEventListener('ratechange',rateListener,true);
            rateListener = null;
        }
        playingAudio.clear();
        if (observer) {observer.disconnect();observer=null;}
        controlsContainer?.remove();
        controlsContainer = null;
        speedDisplayElement = null;
        configPopup?.remove();
        navCleanup?.();
        navCleanup = null;
    }

    // initialization after DOM has loaded
    function init() {
        observer = new MutationObserver(mutations => {
            const hasMainMutations = mutations.some(mutation => mutation.target.closest("#main"));
            if (!hasMainMutations)return;

            // observer for new audio elements
            const audioFound = mutations.some(mutation => Array.from(mutation.addedNodes).some(node => node.nodeName === 'AUDIO' || (node.querySelector && node.querySelector('audio'))));

            // handle UI updates and audio playback speed
            if (audioFound) setPlaybackSpeed();
            if (!document.body.contains(controlsContainer)) createControlButtons();
            if (!document.querySelector('#conversation-header-actions button[aria-label="Jump to next message"]')) {
                navCleanup?.();
                navCleanup = navBtns();
            }
        });

        if (document.body) {
            observer.observe(document.body,{childList:true,subtree:true});

            initializeSpeed();
            createControlButtons();
            navCleanup = navBtns();
        }
    }

    // wait for document to be ready and cleanup when page unloads
    document.readyState === "loading"?document.addEventListener("DOMContentLoaded", init,{once:true}):init();
    window.addEventListener('unload', cleanup);
})();