Claude Style Selector v2.0.0

← Back to User Scripts

Description for Style Selector v2.0.0.

Script Content

// ==UserScript==
// @name         Claude Style Selector
// @namespace    https://github.com/tjhleeds/user-scripts/
// @version      2.0.0
// @description  Display and select Claude conversation styles from a sidebar
// @author       tjhleeds using Claude and Jules
// @match        https://claude.ai/
// @match        https://claude.ai/new
// @match        https://claude.ai/chat/*
// @match        https://claude.ai/project/*
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    const OBSERVER_TIMEOUT_MS = 10000;
    const ELEMENT_READY_DELAY_MS = 50;
    let styles = [];
    let sidebarElement = null;
    let showButtonElement = null;
    let stylesDiscovered = false;

    function hideSidebar() {
        if (sidebarElement) {
            sidebarElement.style.display = 'none';
        }
        if (showButtonElement) {
            showButtonElement.style.display = 'block';
        }
        const mainContent = document.querySelector('body > div:first-child');
        if (mainContent) {
            mainContent.style.marginRight = '0';
        }
        localStorage.setItem('claude-style-selector-sidebar-hidden', 'true');
    }

    function showSidebar() {
        if (sidebarElement) {
            sidebarElement.style.display = 'block';
        }
        if (showButtonElement) {
            showButtonElement.style.display = 'none';
        }
        const mainContent = document.querySelector('body > div:first-child');
        if (mainContent) {
            mainContent.style.marginRight = '200px';
        }
        localStorage.setItem('claude-style-selector-sidebar-hidden', 'false');
    }

    /**
     * Wait for an element to appear in the DOM using MutationObserver
     * @param {Function} checkFn - Function that returns the element when found, or null/undefined
     * @param {number} timeoutMs - Timeout in milliseconds
     * @returns {Promise} Resolves with the element, rejects on timeout
     */
    function waitForElement(checkFn, timeoutMs = OBSERVER_TIMEOUT_MS) {
        return new Promise((resolve, reject) => {
            // Check if element already exists
            const element = checkFn();
            if (element) {
                // Add delay before resolving to ensure element is fully rendered
                setTimeout(() => resolve(element), ELEMENT_READY_DELAY_MS);
                return;
            }

            let timeoutId;
            const observer = new MutationObserver(() => {
                const element = checkFn();
                if (element) {
                    clearTimeout(timeoutId);
                    observer.disconnect();
                    // Add delay before resolving to ensure element is fully rendered
                    setTimeout(() => resolve(element), ELEMENT_READY_DELAY_MS);
                }
            });

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

            timeoutId = setTimeout(() => {
                observer.disconnect();
                reject('Timeout waiting for element');
            }, timeoutMs);
        });
    }

    function getCurrentlySelectedStyle() {
        // Check which style button is currently pressed
        const buttons = document.querySelectorAll('button[aria-pressed="true"]');
        for (const button of buttons) {
            const svgPath = button.querySelector('svg path[d*="M15.5117"]');
            if (svgPath) {
                // This is a pressed style button - find the style name
                const p = button.querySelector('p');
                return p ? p.textContent.trim() : null;
            }
        }
        return null;
    }

    async function selectStyle(styleName) {
        try {
            // Open the tools menu
            const openToolsMenuButton = document.querySelector('[aria-label="Open tools menu"]');
            if (!openToolsMenuButton) {
                throw new Error('Tools menu button not found');
            }
            openToolsMenuButton.click();

            // Wait for style button to appear AND be visible
            const styleButton = await waitForElement(() => {
                const buttons = document.querySelectorAll('button');
                for (const button of buttons) {
                    const svgPath = button.querySelector('svg path[d*="M15.5117"]');
                    if (svgPath && button.offsetParent !== null) {
                        return button;
                    }
                }
                return null;
            });

            styleButton.click();

            // Wait for the specific style to appear in the submenu and be visible
            const foundButton = await waitForElement(() => {
                const paragraphs = document.querySelectorAll('p');
                for (const p of paragraphs) {
                    if (p.textContent.trim() === styleName) {
                        const button = p.closest('button');
                        if (button && button.offsetParent !== null) return button;
                    }
                }
                return null;
            });

            foundButton.click();
            console.log('Claude Style Selector: Selected style:', styleName);

            // Wait for prompt textarea to be available and click it
            await waitForElement(() => document.querySelector('[aria-label="Write your prompt to Claude"]'), 2000);
            const promptTextarea = document.querySelector('[aria-label="Write your prompt to Claude"]');
            if (promptTextarea) {
                promptTextarea.click();
            }
        } catch (error) {
            const openToolsMenuButton = document.querySelector('[aria-label="Open tools menu"]');
            if (openToolsMenuButton) {
                openToolsMenuButton.click(); // Try to close menu on error
            }
            throw error;
        }
    }

    function updateSidebarSelection() {
        if (!sidebarElement) return;

        const currentStyle = getCurrentlySelectedStyle();
        const listItems = sidebarElement.querySelectorAll('li');

        for (const item of listItems) {
            const styleName = item.getAttribute('data-style-name');
            if (styleName === currentStyle) {
                item.style.color = '#fff';
                item.style.fontWeight = '600';
            } else {
                item.style.color = '#ccc';
                item.style.fontWeight = '400';
            }
        }
    }

    function onStylesDiscovered(discoveredStyles) {
        if (stylesDiscovered || discoveredStyles.length === 0) return;
        stylesDiscovered = true;

        console.log('Claude Style Selector: Discovered styles:', discoveredStyles);
        styles = discoveredStyles;

        // Wait for chat interface to be ready before creating sidebar
        waitForElement(() => document.querySelector('[aria-label="Send message"]'))
            .then(() => {
                console.log('Claude Style Selector: Chat interface loaded, creating sidebar.');
                if (sidebarElement) {
                    sidebarElement.remove();
                }
                createSidebar();
                if (!showButtonElement) {
                    createShowButton();
                }

                // Check if sidebar was previously hidden
                if (localStorage.getItem('claude-style-selector-sidebar-hidden') === 'true') {
                    hideSidebar();
                } else {
                    showSidebar();
                }
            })
            .catch(error => {
                 console.error('Claude Style Selector: Chat interface not found, cannot create sidebar.', error);
            });
    }

    function setupFetchInterceptor() {
        const originalFetch = unsafeWindow.fetch;
        const styleListUrlRegex = /https:\/\/claude\.ai\/api\/organizations\/[a-f0-9-]+\/list_styles/;

        unsafeWindow.fetch = async (...args) => {
            const [url] = args;
            const response = await originalFetch(...args);

            if (typeof url === 'string' && styleListUrlRegex.test(url)) {
                try {
                    const clonedResponse = response.clone();
                    const data = await clonedResponse.json();

                    const defaultStyles = data.defaultStyles.map(s => s.name);
                    const customStyles = data.customStyles.map(s => s.name);
                    const allStyles = [...defaultStyles, ...customStyles];

                    onStylesDiscovered(allStyles);
                } catch (error) {
                    console.error('Claude Style Selector: Error parsing styles API response:', error);
                }
            }

            return response;
        };
        console.log('Claude Style Selector: Fetch interceptor set up.');
    }

    function createSidebar() {
        const sidebar = document.createElement('div');
        sidebar.id = 'claude-style-selector-sidebar';
        sidebar.style.cssText = `
            position: fixed;
            top: 0;
            right: 0;
            width: 200px;
            height: 100vh;
            background: #1a1a1a;
            border-left: 1px solid #333;
            padding: 20px;
            box-sizing: border-box;
            z-index: 1000;
            overflow-y: auto;
            font-family: system-ui, -apple-system, sans-serif;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        `;

        const title = document.createElement('h3');
        title.textContent = 'Styles';
        title.style.cssText = `
            color: #fff;
            font-size: 16px;
            margin: 0;
            font-weight: 600;
        `;
        header.appendChild(title);

        const hideButton = document.createElement('button');
        hideButton.textContent = 'Hide';
        hideButton.style.cssText = `
            background: none;
            border: 1px solid #555;
            color: #ccc;
            padding: 2px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        `;
        hideButton.addEventListener('click', hideSidebar);
        header.appendChild(hideButton);

        sidebar.appendChild(header);

        const styleList = document.createElement('ul');
        styleList.style.cssText = `
            list-style: none;
            padding: 0;
            margin: 0;
        `;

        for (const styleName of styles) {
            const listItem = document.createElement('li');
            listItem.setAttribute('data-style-name', styleName);
            listItem.style.cssText = `
                color: #ccc;
                padding: 8px 0;
                font-size: 14px;
                cursor: pointer;
                transition: color 0.2s;
            `;
            listItem.textContent = styleName;

            // Add click handler
            listItem.addEventListener('click', async () => {
                try {
                    await selectStyle(styleName);
                    updateSidebarSelection();
                } catch (error) {
                    console.error('Claude Style Selector: Failed to select style:', error);
                }
            });

            // Add hover effect
            listItem.addEventListener('mouseenter', () => {
                if (listItem.style.color === 'rgb(255, 255, 255)') return; // Don't change if selected
                listItem.style.color = '#fff';
            });
            listItem.addEventListener('mouseleave', () => {
                if (listItem.style.fontWeight === '600') return; // Don't change if selected
                listItem.style.color = '#ccc';
            });

            styleList.appendChild(listItem);
        }

        sidebar.appendChild(styleList);
        document.body.appendChild(sidebar);
        sidebarElement = sidebar;

        // Push main content left to make room for sidebar
        const mainContent = document.querySelector('body > div:first-child');
        if (mainContent) {
            mainContent.style.marginRight = '200px';
        }

        // Set initial selection state
        updateSidebarSelection();

        console.log('Claude Style Selector: Sidebar created with', styles.length, 'styles');
    }

    function createShowButton() {
        const button = document.createElement('button');
        button.id = 'claude-style-selector-show-button';
        button.textContent = 'Styles';
        button.style.cssText = `
            position: fixed;
            top: 50%;
            right: 0;
            transform: translateY(-50%) rotate(-90deg);
            transform-origin: bottom right;
            background: #1a1a1a;
            color: #fff;
            border: 1px solid #333;
            border-bottom: none;
            padding: 8px 16px;
            cursor: pointer;
            z-index: 999;
            display: none;
            font-family: system-ui, -apple-system, sans-serif;
            font-size: 14px;
            border-top-left-radius: 4px;
            border-top-right-radius: 4px;
        `;
        button.addEventListener('click', showSidebar);
        document.body.appendChild(button);
        showButtonElement = button;
    }

    // Start the fetch interceptor
    setupFetchInterceptor();
})();