Claude Style Selector v2.0.0
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();
})();