// ==UserScript== // @name Say, Pi // @namespace http://www.saypi.ai/ // @version 1.1.2 // @description Speak to Pi with OpenAI's Whisper // @author Ross Cadogan // @match https://pi.ai/* // @grant GM_xmlhttpRequest // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; const localConfig = { webServerUrl: "http://localhost:3000", apiServerUrl: "http://localhost:5000", // Add other configuration properties as needed }; // Define a global configuration property const productionConfig = { webServerUrl: "https://www.saypi.ai", apiServerUrl: "https://api.saypi.ai", // Add other configuration properties as needed }; const config = productionConfig; // Create a MutationObserver to listen for changes to the DOM var observer = new MutationObserver(function (mutations) { // Check each mutation for (var i = 0; i < mutations.length; i++) { var mutation = mutations[i]; // If nodes were added, check each one if (mutation.addedNodes.length > 0) { for (var j = 0; j < mutation.addedNodes.length; j++) { var node = mutation.addedNodes[j]; // If the node is the appropriate container element, add the button and stop observing if (node.nodeName.toLowerCase() === 'div' && node.classList.contains('fixed') && node.classList.contains('bottom-16')) { var footer = node; var buttonContainer = footer.querySelector('.relative.flex.flex-col'); if (buttonContainer) { addTalkButton(buttonContainer); } else { console.log('No button container found in footer'); } observer.disconnect(); return; } } } } }); function injectScript(callback) { return injectScriptRemote(callback); } function injectScriptRemote(callback) { // Get the URL of the remote script var remoteScriptUrl = config.webServerUrl + '/static/js/literal.js'; GM_xmlhttpRequest({ method: "GET", url: remoteScriptUrl, onload: function (response) { var scriptElement = document.createElement("script"); scriptElement.type = "text/javascript"; scriptElement.id = 'saypi-script'; const configText = 'var config = ' + JSON.stringify(config) + ';'; scriptElement.textContent = configText + response.responseText; document.body.appendChild(scriptElement); // Call the callback function after the script is added if (callback) { callback(); } } }); } function injectScriptLocal(callback) { var scriptElement = document.createElement("script"); scriptElement.type = "text/javascript"; scriptElement.id = 'saypi-script'; const scriptText = ` // Paste the contents of static/js/literal.js here to avoid CORS issues ` const configText = 'var config = ' + JSON.stringify(config) + ';'; scriptElement.textContent = configText + scriptText; document.body.appendChild(scriptElement); // Call the callback function after the script is added if (callback) { callback(); } } function addTalkButton(container) { var button = document.createElement('button'); button.id = 'talkButton'; button.type = 'button'; button.className = 'relative flex mt-1 mb-1 rounded-full px-2 py-3 text-center bg-cream-550 hover:bg-cream-650 hover:text-brand-green-700 text-muted'; // Set ARIA label and tooltip const label = 'Talk (Hold Control + Space to use hotkey. Double click to toggle auto-submit on/off)' button.setAttribute('aria-label', label); button.setAttribute('title', label); // enable autosubmit by default button.dataset.autosubmit = 'true'; button.classList.add('autoSubmit'); container.appendChild(button); addTalkButtonStyles(); addTalkIcon(button); // Call the function to inject the script after the button has been added injectScript(registerAudioButtonEvents); } function addTalkIcon(button) { var iconHtml = ` `; var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); button.appendChild(icon); icon.outerHTML = iconHtml; } function addStyles(css) { const style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } function addTalkButtonStyles() { // Get the button and register for mousedown and mouseup events var button = document.getElementById('talkButton'); button.style.marginTop = '0.25rem'; button.style.borderRadius = '18px'; button.style.width = '120px'; // button animation addStyles(` @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(0.9); } 100% { transform: scale(1); } } #talkButton:active .waveform, #talkButton.active .waveform { animation: pulse 1s infinite; } #talkButton .waveform { fill: #776d6d; } #talkButton.autoSubmit .waveform { fill: rgb(65 138 47); /* Pi's text-brand-green-600 */ } `); } function registerAudioButtonEvents() { var button = document.getElementById('talkButton'); button.addEventListener('mousedown', function () { idPromptTextArea(); unsafeWindow.startRecording(); }); button.addEventListener('mouseup', function () { unsafeWindow.stopRecording(); }); registerHotkey(); // "warm up" the microphone by acquiring it before the user presses the button document.getElementById('talkButton').addEventListener('mouseenter', setupRecording); document.getElementById('talkButton').addEventListener('mouseleave', tearDownRecording); window.addEventListener('beforeunload', tearDownRecording); // Attach a double click event listener to the talk button button.addEventListener('dblclick', function () { // Toggle the CSS classes to indicate the mode button.classList.toggle('autoSubmit'); // Store the state on the button element using a custom data attribute if (button.getAttribute('data-autosubmit') === 'true') { button.setAttribute('data-autosubmit', 'false'); console.log('autosubmit disabled'); } else { button.setAttribute('data-autosubmit', 'true'); console.log('autosubmit enabled'); } }); } function registerHotkey() { // Register a hotkey for the button let ctrlDown = false; document.addEventListener('keydown', function (event) { if (event.ctrlKey && event.code === 'Space' && !ctrlDown) { ctrlDown = true; // Simulate mousedown event let mouseDownEvent = new Event('mousedown'); document.getElementById('talkButton').dispatchEvent(mouseDownEvent); talkButton.classList.add('active'); // Add the active class } }); document.addEventListener('keyup', function (event) { if (ctrlDown && event.code === 'Space') { ctrlDown = false; // Simulate mouseup event let mouseUpEvent = new Event('mouseup'); document.getElementById('talkButton').dispatchEvent(mouseUpEvent); talkButton.classList.remove('active'); } }); } function idPromptTextArea() { var textarea = document.getElementById('prompt'); if (!textarea) { // Find the first