Improved Claude Voice Interface

Voice interface for Claude with TTS functionality, speech recognition, auto-reading and drag positioning

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Improved Claude Voice Interface
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  Voice interface for Claude with TTS functionality, speech recognition, auto-reading and drag positioning
// @author       You
// @match        https://claude.ai/chat/*
// @match        https://claude.ai/*
// @match        https://claude.ai
// @license MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const config = {
        buttonHeight: '50px',
        activeBackgroundColor: '#4a86e8',
        inactiveBackgroundColor: '#6c757d',
        activeTextColor: 'white',
        inactiveTextColor: '#e0e0e0',
        readRateMultiplier: 1.0,
        autoReadNewMessages: true,
        checkInterval: 1000, // Check for new messages every 1 second
        readingDelay: 100,   // Delay to check if 50 characters of message have been loaded
        startAfterCharacters: 50 //Number of characters to require before starting reading
    };

    // State variables
    let isSpeaking = false;
    let isRecording = false;
    let currentMessageElement = null;
    let lastMessageElement = null;
    let checkIntervalId = null;
    let processingMessage = false;
    let recognition = null;

    // Drag state variables
    let isDragging = false;
    let startY = 0;
    let startTop = 0;

    // Wait for the page to fully load
    window.addEventListener('load', function() {
        setTimeout(initializeVoiceInterface, 1500);
    });

    function initializeVoiceInterface() {
        console.log('Claude Voice Interface: Initializing...');

        // CSS for highlighting (can be customized)
        const highlightStyle = document.createElement('style');
        highlightStyle.textContent = `
          .active-highlight {
               background-color: #e6e6d8;
               border-radius: 5px;
           }
        `;
        document.head.appendChild(highlightStyle);

        // Create the voice interface bar
        const voiceBar = document.createElement('div');
        voiceBar.id = 'claude-voice-bar';
        voiceBar.style.width = '100%';
        voiceBar.style.display = 'flex';
        voiceBar.style.justifyContent = 'space-between';
        voiceBar.style.alignItems = 'center';
        voiceBar.style.boxSizing = 'border-box';
        voiceBar.style.padding = '10px';
        voiceBar.style.backgroundColor = '#f8f9fa';
        voiceBar.style.borderTop = '1px solid #dee2e6';
        voiceBar.style.position = 'fixed';
        voiceBar.style.top = '0';
        voiceBar.style.left = '0';
        voiceBar.style.zIndex = '10000'; // Increased z-index to make sure it's above other elements

        // Create the left button (Read/Pause)
        const speakButton = document.createElement('button');
        speakButton.id = 'claude-speak-button';
        speakButton.innerHTML = `${createPlayIcon()} <span style="margin-left: 8px;">READ</span>`;
        speakButton.style.width = '48%';
        speakButton.style.height = config.buttonHeight;
        speakButton.style.borderRadius = '8px';
        speakButton.style.border = 'none';
        speakButton.style.backgroundColor = config.inactiveBackgroundColor;
        speakButton.style.color = config.inactiveTextColor;
        speakButton.style.cursor = 'pointer';
        speakButton.style.display = 'flex';
        speakButton.style.justifyContent = 'center';
        speakButton.style.alignItems = 'center';
        speakButton.style.transition = 'all 0.3s';
        speakButton.style.fontWeight = 'bold';

        // Create the right button (Speech-to-Text)
        const recordButton = document.createElement('button');
        recordButton.id = 'claude-record-button';
        recordButton.innerHTML = `${createEmptyCircleIcon()} <span style="margin-left: 8px;">LISTEN</span>`;
        recordButton.style.width = '48%';
        recordButton.style.height = config.buttonHeight;
        recordButton.style.borderRadius = '8px';
        recordButton.style.border = 'none';
        recordButton.style.backgroundColor = config.inactiveBackgroundColor;
        recordButton.style.color = config.inactiveTextColor;
        recordButton.style.cursor = 'pointer';
        recordButton.style.display = 'flex';
        recordButton.style.justifyContent = 'center';
        recordButton.style.alignItems = 'center';
        recordButton.style.transition = 'all 0.3s';
        recordButton.style.fontWeight = 'bold';

        // Create drag handle
        const dragHandle = document.createElement('div');
        dragHandle.id = 'claude-voice-drag-handle';
		dragHandle.innerHTML = createDragIcon();
        dragHandle.style.width = '50px';
        dragHandle.style.left = '50%'; // Center it
        dragHandle.style.cursor = 'ns-resize';
        dragHandle.style.display = 'flex';
        dragHandle.style.justifyContent = 'center';
        dragHandle.style.alignItems = 'center';

        // Add elements to voice bar
        voiceBar.appendChild(speakButton);
        voiceBar.appendChild(recordButton);
        voiceBar.appendChild(dragHandle);

        // Simply append to the body
        document.body.appendChild(voiceBar);

        console.log('Claude Voice Interface: Interface added to page');

        // Add event listeners
        speakButton.addEventListener('click', toggleSpeaking);
        recordButton.addEventListener('click', toggleRecording);

        // Add drag functionality
        setupDragHandlers(voiceBar, dragHandle);

        // Initialize speech recognition if available
        initializeSpeechRecognition();

        // Start monitoring for new messages if auto-read is enabled
        if (config.autoReadNewMessages) {
            startMessageMonitoring();
        }
    }

    function initializeSpeechRecognition() {
        // Check if browser supports speech recognition
        if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
            const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
            recognition = new SpeechRecognition();
            recognition.continuous = true;

            recognition.onend = function (e) {
                if (isRecording) {
                    recognition.start();
                }
            }

            recognition.onresult = function (e) {
                let results = e.results

                let str = results[results.length - 1][0].transcript;

                if (str[0] === ' ') {
                    str =  ' ' + str[1].toUpperCase() + str.slice(2) + '. ';
                } else {
                    str = str[0].toUpperCase() + str.slice(1) + '. ';
                }

                document.querySelector('.ProseMirror').innerText += str;
            }

            recognition.onerror = function(event) {
                console.error('Recognition error:', event.error);
            };

            // Enable the record button
            const recordButton = document.getElementById('claude-record-button');
            if (recordButton) {
                recordButton.disabled = false;
                recordButton.style.opacity = '1';
            }
        } else {
            console.warn('Speech Recognition not supported in this browser');
        }
    }

    function setupDragHandlers(voiceBar, dragHandle) {
        // Mouse events for desktop
        dragHandle.addEventListener('mousedown', function(e) {
            e.preventDefault();
            startDrag(e.clientY);
        });

        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                doDrag(e.clientY);
            }
        });

        document.addEventListener('mouseup', function() {
            endDrag();
        });

        // Touch events for mobile
        dragHandle.addEventListener('touchstart', function(e) {
            e.preventDefault();
            startDrag(e.touches[0].clientY);
        });

        document.addEventListener('touchmove', function(e) {
            if (isDragging) {
                doDrag(e.touches[0].clientY);
            }
        });

        document.addEventListener('touchend', function() {
            endDrag();
        });

        function startDrag(clientY) {
            isDragging = true;
            startY = clientY;
            startTop = parseInt(voiceBar.style.top || '0');
        }

        function doDrag(clientY) {
            const deltaY = startY - clientY;
            const newTop = Math.max(0, startTop - deltaY);
            voiceBar.style.top = `${newTop}px`;
        }

        function endDrag() {
            if (isDragging) {
                isDragging = false;
            }
        }
    }

    function startMessageMonitoring() {
        console.log('Claude Voice Interface: Starting message monitoring');

        // Store the current last message ID
        const currentLastMessage = findLatestClaudeMessage();
        if (currentLastMessage) {
            lastMessageElement = currentLastMessage
            console.log('Claude Voice Interface: Initial message:', lastMessageElement);
        }

        // Start checking for new messages
        checkIntervalId = setInterval(checkForNewMessages, config.checkInterval);
    }

    function checkForNewMessages() {
        if (processingMessage) return; // Don't check for new messages if we're already processing one

        const latestMessage = findLatestClaudeMessage();
        if (!latestMessage || latestMessage === lastMessageElement) return;

        // If we aren't reading something else
        if (!isSpeaking) {
            console.log('Claude Voice Interface: New message detected');
            lastMessageElement = latestMessage;

            // Check if the message has finished generating
            let isLoading = latestMessage.dataset.isStreaming === 'true';

            //if it's still loading, wait for startAfterReading (50) characters or it to finish loading
            if (!isLoading) {
                readMessage(latestMessage, isLoading);
            } else {
                let lengthCheckInterval = setInterval(() => {
                    try {
                        isLoading = latestMessage.dataset.isStreaming === 'true';

                        if (latestMessage.textContent.trim().length >= config.startAfterCharacters || !isLoading) {
                            readMessage(latestMessage, isLoading);
                            clearInterval(lengthCheckInterval);
                        }
                    } catch (e) {
                        clearInterval(lengthCheckInterval);
                        console.error(`Error: ${e.message}`);
                    }
                }, config.readingDelay);
            }
        }
    }

    function toggleSpeaking() {
        const speakButton = document.getElementById('claude-speak-button');

        // Always cancel existing speech first to avoid buggy behavior
        window.speechSynthesis.cancel();

        if (!isSpeaking) {
            console.log('Claude Voice Interface: Starting speech');
            // Start reading the last message
            const message = findLatestClaudeMessage();
            if (message) {
                readMessage(message);
            }
            return;
        } else {
            console.log('Claude Voice Interface: Stopping speech');
            // Instead of pausing, we'll cancel and remember we want to resume
            resetSpeechState();
            speakButton.innerHTML = `${createPlayIcon()} <span style="margin-left: 8px;">RESUME</span>`;
            return;
        }
    }

    function readMessage(message, isLoading = false, spokenSegments = []) {
        if (!message) {
            console.log('Claude Voice Interface: No message to read');
            return;
        }

        currentMessageElement = message;

        // Prepare the text to be read
        if (!message.textContent.trim()) {
            console.log('Claude Voice Interface: Message has no text content');
            return;
        }

        // Recursively process the DOM to wrap words for highlighting
        function traverse(node) {
            let segments = [];

            // Skip script, style, and other non-content elements
            if (node.nodeType === Node.ELEMENT_NODE) {
                const tagName = node.tagName ? node.tagName.toLowerCase() : null;

                //Skip these tag types
                if (['script', 'style', 'noscript', 'svg', 'canvas', 'button', 'text-reader'].includes(tagName)) {
                    return segments;
                }

                //Special processing for preformatted code blocks
                if (tagName === 'pre') {
                    let codeBlockType = node?.children[0]?.children[0]?.textContent;
                    if (codeBlockType) {
                        segments.push({
                            text: `, ${codeBlockType} section detected, please view in chat, `,
                            el: node?.children[0]?.children[0]
                        });
                    }

                    //Skip further processing, ignore unknown pre block types
                    return segments;
                }

                // Process this element's children recursively
                for (let child of node.childNodes) {segments = segments.concat(traverse(child));}
                return segments;
            }

            // Process text nodes that have content
            if (node.nodeType === Node.TEXT_NODE) {
                const text = node.textContent.trim();
                if (text === '') return segments;

                // Split into words
                const words = node.textContent.split(' ');

                // Split into individual words
                for (const [i, word] of words.entries()) {
                    const wrapper = document.createElement('text-reader');
                    if (i < words.length - 1) {
                        //Put a space back in
                        segments.push({
                            text: word + ' ',
                            el: node.parentElement
                        });
                    } else {
                        //Add a comma and space to get  a brief pause in spoken version and keep separation from the next line of text
                        segments.push({
                            text: word + ', ',
                            el: node.parentElement
                        });
                    }
                }
            }

            return segments;
        }

        // Start the traversal
        let segments = traverse(message);

        //Remove the last segment if we are still streaming in case it is a partial word
        if (isLoading) {
            segments.pop();
        }

        //Remove any previously spoken segments if we started speaking before it finished streaming
        segments = segments.slice(spokenSegments.length);

        //Collect all the readable fragments and create a map to track the current word location
        function updateCharacterMap(segments) {
            let text = '',
            characterMap = {};

            for (let segment of segments) {
                //Create a map of every character location from the previous max to our new max after appending the text together
                let oldLength = text.length;
                text += segment.text;
                let newLength = text.length;

                for (let i = oldLength; i < newLength; i++) {
                    characterMap[i] = segment.el;
                }
            }

            return {text, characterMap};
        }

        let {text, characterMap} = updateCharacterMap(segments);

        // Cancel any ongoing speech first
        window.speechSynthesis.cancel();

        // Create new utterance
        const utterance = new SpeechSynthesisUtterance(text);
        utterance.rate = config.readRateMultiplier;
        /*let voice = speechSynthesis.getVoices().find(voice => {return voice.voiceURI === localStorage['claude-text-reader-voice'];})
        if (voice) {
            utterance.voice = voice;
        }*/

        // Set up events
        let updatedSegments = segments,
            updatedCharacterMap = characterMap;
        utterance.onboundary = function (e) {
            try {
                //console.log(e.name, e.charIndex, characterMap[e.charIndex].classList, characterMap[e.charIndex]);

                //Check to make sure our segments still exist, rebuild the elements/segments list if it's been updated by streaming
                let el = updatedCharacterMap[e.charIndex];
                console.log(document.body.contains(el), el);
                if (!document.body.contains(el)) {
                    console.log('Claud Voice Interface: DOM Refreshed, Updating Element References');
                    updatedSegments = traverse(message);
                    let results = updateCharacterMap(updatedSegments);
                    updatedCharacterMap = results.characterMap
                    el = updatedCharacterMap[e.charIndex];
                }
                if (!el.classList.contains('active-highlight')) {
                    //Remove the class from all the other elements
                    for (let oldEl of Array.from(message.querySelectorAll('.active-highlight'))) {
                        oldEl.classList.remove('active-highlight');
                    }
                    //Add highlight class to the new element
                    el.classList.add('active-highlight');
                }
            } catch (e) {console.error('Claude Voice Interface - error in highlighting');}
        }

        utterance.onstart = function() {
            console.log('Claude Voice Interface: Speech started');
            isSpeaking = true;
            const speakButton = document.getElementById('claude-speak-button');
            speakButton.innerHTML = `${createPauseIcon()} <span style="margin-left: 8px;">PAUSE</span>`;
            speakButton.style.backgroundColor = config.activeBackgroundColor;
            speakButton.style.color = config.activeTextColor;
            speakButton.style.fontWeight = 'bold';
        };

        utterance.onend = function() {
            //Check to see if it was still loading when we started to play the message
            if (isLoading) {
                //Get the updated message and play it, checking to see if it's still loading now and pass in what we've read so far
                console.log('Claude Voice Intercace speech continuing streaming message');
                isLoading = message.dataset.isStreaming === 'true';
                readMessage(message, isLoading, segments);
            } else {
                console.log('Claude Voice Interface: Speech ended');
                resetSpeechState();
            }
        };

        utterance.onerror = function(event) {
            console.error('Claude Voice Interface: Speech synthesis error:', event.error);
            resetSpeechState();
        };

        // Start speaking - slight delay to try and get the boundary event to fire consistently
        setTimeout(() => {
            window.speechSynthesis.speak(utterance);
        }, 50);
    }

    function findLatestClaudeMessage() {
        try {
			return document.querySelectorAll('div[data-test-render-count]')[Array.from(document.querySelectorAll('div[data-test-render-count]')).length - 1].querySelector('div[data-is-streaming]')
		} catch (e) {
			return null;
		}
    }

    function resetSpeechState() {
        // Cancel any ongoing speech
        window.speechSynthesis.cancel();

        // Reset speech state
        isSpeaking = false;

        // Reset button appearance
        const speakButton = document.getElementById('claude-speak-button');
        speakButton.innerHTML = `${createPlayIcon()} <span style="margin-left: 8px;">READ</span>`;
        speakButton.style.backgroundColor = config.inactiveBackgroundColor;
        speakButton.style.color = config.inactiveTextColor;
        speakButton.style.fontWeight = 'normal';

        // Remove any highlighting
        document.querySelectorAll('.active-highlight').forEach(el => el.classList.remove('active-highlight'));
    }

    function toggleRecording() {
        if (!recognition) {
            console.warn('Speech Recognition not initialized');
            alert('Speech Recognition is not supported in this browser');
            return;
        }

        const recordButton = document.getElementById('claude-record-button');

        if (!isRecording) {
            // Start recording
            isRecording = true;

            try {
                recognition.start();

                recordButton.innerHTML = `${createFilledCircleIcon()} <span style="margin-left: 8px;">LISTENING</span>`;
                recordButton.style.backgroundColor = config.activeBackgroundColor;
                recordButton.style.color = config.activeTextColor;
                recordButton.style.fontWeight = 'bold';

                console.log('Claude Voice Interface: Started listening');
            } catch (e) {
                console.error('Error starting recognition:', e);
                isRecording = false;
                alert('Error starting speech recognition: ' + e.message);
            }
        } else {
            // Stop recording
            isRecording = false;

            try {
                recognition.stop();
            } catch (e) {
                console.error('Error stopping recognition:', e);
            }

            recordButton.innerHTML = `${createEmptyCircleIcon()} <span style="margin-left: 8px;">LISTEN</span>`;
            recordButton.style.backgroundColor = config.inactiveBackgroundColor;
            recordButton.style.color = config.inactiveTextColor;
            recordButton.style.fontWeight = 'normal';

            //And try and click the submit button
            document.querySelector('button svg path[d="M208.49,120.49a12,12,0,0,1-17,0L140,69V216a12,12,0,0,1-24,0V69L64.49,120.49a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0l72,72A12,12,0,0,1,208.49,120.49Z"]').parentElement.parentElement.click()
        }
    }

    function insertTextToInput(text) {
        if (!text.trim()) return;

        // First, try the specific selector for Claude's input field
        let inputField = document.querySelector('div.ProseMirror[contenteditable="true"]');

        if (!inputField) {
            console.log('Could not find Claude input with ProseMirror class, trying alternative selectors');

            // If not found, try backup selectors
            const potentialSelectors = [
                'textarea',
                '[role="textbox"]',
                '[contenteditable="true"]',
                'input[type="text"]',
                '.chat-input',
                '[class*="promptTextarea"]',
                '[class*="TextInput"]'
            ];

            for (const selector of potentialSelectors) {
                const elements = document.querySelectorAll(selector);
                for (const element of elements) {
                    // Skip hidden elements or elements not visible in the viewport
                    if (element.offsetParent === null) continue;

                    // Check if this is likely to be the chat input
                    const placeholder = element.getAttribute('placeholder') || '';
                    const aria = element.getAttribute('aria-label') || '';
                    const classes = element.className || '';

                    if (placeholder.includes('message') ||
                        placeholder.includes('Message') ||
                        aria.includes('message') ||
                        aria.includes('Message') ||
                        classes.includes('message') ||
                        classes.includes('Message') ||
                        classes.includes('input') ||
                        classes.includes('Input')) {
                        inputField = element;
                        break;
                    }
                }
                if (inputField) break;
            }
        } else {
            console.log('Found Claude input with ProseMirror class');
        }

        if (inputField) {
            console.log('Claude Voice Interface: Found input field, inserting text');

            // For textarea or input elements
            if (inputField.tagName === 'TEXTAREA' || inputField.tagName === 'INPUT') {
                inputField.value = text;

                // Trigger multiple events to ensure the UI updates
                inputField.dispatchEvent(new Event('input', { bubbles: true }));
                inputField.dispatchEvent(new Event('change', { bubbles: true }));
                inputField.focus();
            }
            // For contenteditable divs (including ProseMirror)
            else if (inputField.getAttribute('contenteditable') === 'true') {
                // Focus the element first
                inputField.focus();

                // Clear existing content
                inputField.innerHTML = '';

                // Insert text properly for contenteditable
                // Using document.execCommand for better compatibility with contenteditable
                document.execCommand('insertText', false, text);

                // Backup - use innerHTML if document.execCommand didn't work
                if (!inputField.textContent) {
                    inputField.innerHTML = text;
                }

                // Fire several events to make sure Claude's UI notices the change
                inputField.dispatchEvent(new Event('input', { bubbles: true }));
                inputField.dispatchEvent(new Event('change', { bubbles: true }));

                // Additional event for React-based UIs
                const customEvent = new CustomEvent('input-change', { bubbles: true, detail: { text } });
                inputField.dispatchEvent(customEvent);
            }

            console.log('Text inserted into input field');
        } else {
            console.warn('Claude Voice Interface: Could not find input field');
            alert('Could not find input field to insert text. Check console for details.');
            console.error('Failed to find any matching input field. Text was:', text);
        }
    }

    function resetRecordingState() {
        // Stop recognition if active
        if (recognition && isRecording) {
            try {
                recognition.stop();
            } catch (e) {
                console.error('Error stopping recognition during reset:', e);
            }
        }

        // Reset recording state
        isRecording = false;
        recognizedText = '';
        collectedSpeechSegments = [];

        // Clear any pending timeouts
        if (listeningTimeoutId) {
            clearTimeout(listeningTimeoutId);
            listeningTimeoutId = null;
        }

        // Reset button appearance
        const recordButton = document.getElementById('claude-record-button');
        if (recordButton) {
            recordButton.innerHTML = `${createEmptyCircleIcon()} <span style="margin-left: 8px;">LISTEN</span>`;
            recordButton.style.backgroundColor = config.inactiveBackgroundColor;
            recordButton.style.color = config.inactiveTextColor;
            recordButton.style.fontWeight = 'normal';
        }
    }

    // Icon creation functions
    function createPlayIcon() {
        return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
        </svg>`;
    }

    function createPauseIcon() {
        return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"/>
        </svg>`;
    }

    function createEmptyCircleIcon() {
        return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
        </svg>`;
    }

    function createFilledCircleIcon() {
        return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
            <circle cx="8" cy="8" r="7" fill="currentColor"/>
        </svg>`;
    }

    function createDragIcon() {
        return `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
			<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
			<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
			<g id="SVGRepo_iconCarrier">
				<path d="M5 9H13H19M5 15H19" stroke="#878787" stroke-width="2" stroke-linecap="round"></path>
			</g>
		</svg>`
    }
})();