// ==UserScript== // @name Say, Pi // @namespace http://www.saypi.ai/ // @version 1.0.0 // @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'; // Define a global configuration property const config = { webServerUrl: "https://www.saypi.ai", apiServerUrl: "https://api.saypi.ai", // Add other configuration properties as needed }; // 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) { addAudioButton(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 addAudioButton(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 button.setAttribute('aria-label', 'Talk (Press Control + Space to use hotkey)'); button.setAttribute('title', 'Talk (Press Control + Space to use hotkey)'); container.appendChild(button); addAudioButtonStyles(); addAudioIcon(button); // Call the function to inject the script after the button has been added injectScript(registerAudioButtonEvents); } function addAudioIcon(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 addAudioButtonStyles() { // 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; } `); } 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); } 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