// ==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