I'm currently working on a Tampermonkey script to summarize selected text using the OpenAI API
The UI is a bit rough right now and needs a CSS reset or alternative presentation so that it isn't so heavily affected by the page's underlying CSS, and there's some unanticipated states that I need to tweak if you edit the input/output, but for basic use it seems to work well!
See my TODOs in the source for what I'm going to work on next
Here's the script:
// ==UserScript==
// @name Summarize selected text using OpenAI API
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Summarize selected text using OpenAI API
// @author Josef Faulkner
// @license CC BY-NC 4.0
// @match *://*/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
/*
TODO: Disable the select handler when selecting text within the modal dialog
TODO: Test for situations where a lot of editing is done within the dialog text
TODO: Fix CSS issues on many sites, maybe see if TM has its own dialog system or make it a web component?
TODO: Labels on options
TODO: Allow prompt customization before submit option ?
TODO: Options to supplement evaluated text with page metadata, surrounding headers, etc.
TODO: Figure out how to get the flavor of personas to come through more.
TODO: Find a more robust way to ensure formats are followed (e.g. bullet points)
TODO: Add a speculaion action?
TODO: Map each config option label to a different value for more flexibility
TODO: Add a spinner/throbber while waiting for a response
TODO: Add an option to display token estimates
TODO: Add an option to display response statistics and info
TODO: Replace alerts with proper dialogs so their contents can be copied
TODO: Put this into its own repo
*/
(function() {
'use strict';
const config = {
apiKey: 'YOUR_API_KEY',
apiUrl: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-3.5-turbo',
actionOptions: [
'Summary',
'Explanation',
'Translation',
'Critical Evaluation',
],
sizeOptions: [
'Text',
'"tl;dr"',
'Concise',
'Succinct',
'Short',
'Brief',
'Neutral',
'Entertaining',
'Emoji-Only',
'Emoji-Laden',
'Emoticon-Laden',
'Colorful',
'Upside Down',
'Paragraph',
'Short Essay',
'Draft',
'Very Succinct',
'Very Concise',
'Single short sentence',
'Single sentence',
'One Line',
'Few lines',
'Several lines',
'Detailed',
'Lengthy',
],
formOptions: ['', 'Text', 'Bullet Points', 'Outline', 'JSON', 'Markdown'],
toneOptions: [
'',
'Best Friend',
'Acquaintance',
'Engineer',
'Journalist',
'College Professor',
'High School Teacher',
'Grade School Teacher',
'Kindergarten Teacher',
'Gym Coach',
'Philosopher',
'Alan Watts',
'Guru',
'Yogi',
'Robot',
'Emo Person',
'Catgirl :3',
'Troll',
'Just the Facts',
'Surfer Dude',
'The Terminator',
'Bot That Only Talks Emoji',
], // with inferences option
// include title option
};
let configElements = {};
const MOCK_API = false;
let dialog;
const button = document.createElement('button');
document.addEventListener('selectionchange', function(event) {
const selection = window.getSelection();
if (selection.isCollapsed) {
onSelectionCancelled();
if (event.shiftKey) {
}
} else {
onSelectionMade(selection);
}
});
function onSelectionMade() {
button.style.display = '';
}
function onSelectionCancelled() {
button.style.display = 'none';
}
setTimeout(addButton);
function addButton() {
button.style.zIndex = '9999';
button.style.display = 'none';
button.style.position = 'fixed';
button.style.bottom = '33%';
button.style.right = '0';
button.innerText = 'S';
button.style.borderLeft = '3px solid red';
button.style.borderTop = '3px solid orange';
button.style.borderRight = '3px solid green';
button.style.borderBottom = '3px solid blue';
button.style.cursor = 'pointer';
button.style.opacity = '1';
button.addEventListener('click', handleSButtonClick);
document.body.appendChild(button);
function handleSButtonClick() {
disableButton();
const longText = window.getSelection().toString().trim();
dialog = openDialog(longText);
}
}
function openDialog(text) {
const config = {
title: 'Summarize Text',
closeCallback: enableButton,
buttonConfig: [
{
label: 'Summarize',
onClick: () => {requestSummary(text);},
}, {
label: 'Close',
},
],
configContainer: generateConfigContainer(),
};
return createModalDialog([text, ''], config);
}
function generateConfigContainer() {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'row';
configElements = {};
container.appendChild(createSelect('action', config.actionOptions));
container.appendChild(createSelect('size', config.sizeOptions));
container.appendChild(createSelect('form', config.formOptions));
container.appendChild(createSelect('tone', config.toneOptions));
container.appendChild(createSelect('title', ['', 'Include Title']));
container.appendChild(createSelect('url', ['', 'Include URL']));
return container;
function createSelect(name, options) {
const select = document.createElement('select');
select.name = name;
options.forEach(function(option) {
select.appendChild(createOption(option));
});
configElements[name] = select;
return select;
}
function createOption(label, value) {
const option = document.createElement('option');
option.text = label;
option.value = value || label;
return option;
}
}
function disableButton() {
button.style.opacity = '0.5';
button.disabled = true;
button.style.cursor = 'default';
}
function enableButton() {
button.style.opacity = '1';
button.disabled = false;
button.style.cursor = 'pointer';
}
// TODO: memoize?
let lastRequest;
let lastResponse;
function requestSummary(text) {
if (text === lastRequest) {
handleResponseMessage(lastResponse);
}
lastRequest = text;
summarize(text);
function summarize(selectedText) {
if (selectedText.length <= 0) { return; }
ajaxStart();
if (!config.apiKey || config.apiKey === 'YOUR_API_KEY') {
handleResponseMessage('Error: You must set your API key');
ajaxEnd();
return;
}
if (MOCK_API) {
setTimeout(() => {
ajaxEnd();
handleResponseMessage('MOCK_API: ' + selectedText);
}, 1000);
return;
}
const requestData = getRequestData();
if (!requestData) {
ajaxEnd();
return;
}
GM_xmlhttpRequest({
method: 'POST',
url: config.apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + config.apiKey,
},
data: requestData,
onload: handleOnLoad,
onerror: handleOnError,
});
function handleOnLoad(response) {
ajaxEnd(response);
if (response.status === 200) {
handleResponseJson(response.responseText);
} else {
handleErrorResponse(response);
}
}
function handleOnError(response) {
ajaxEnd(response);
handleErrorResponse(response);
}
function getRequestData() {
function getConfigValue(name, defaultValue = 'normal') {
const cfg = configElements[name];
const response = cfg ? (cfg.value || defaultValue) : defaultValue;
return response.toLowerCase();
}
function getPromptParameters() {
const params = [];
let val;
params.push([
'You will write a',
getConfigValue('size'),
getConfigValue('action'),
'of text clipped from the web.',
].join(' '));
val = getConfigValue('form');
if (val && val !== 'normal') {
params.push('You must format your response with "' + val + '".');
}
val = getConfigValue('tone');
if (val && val !== 'normal') {
params.push('Write your entire response in the style of the following persona: "' + val + '".');
}
val = getConfigValue('title');
if (val[0] === 'i') {
params.push('The clip came from a web page with the title: ' + document.title);
}
val = getConfigValue('url');
if (val[0] === 'i') {
params.push('The clip originated from: ' + document.location.href);
}
return params.join('\n');
}
const chatPrompt = [
getPromptParameters(),
'The text for you to evaluate is the following:',
selectedText,
].join('\n');
if (!confirm('Sending the following prompt:\n\n' + chatPrompt)) {
return;
}
return JSON.stringify({
'model': config.model,
'messages': [
{
'role': 'system',
'content': 'You are a helpful summary bot.',
}, {
'role': 'user',
'content': chatPrompt,
},
],
});
}
function handleResponseJson(jsonText) {
const response = JSON.parse(jsonText);
const message = response.choices[0].message;
handleResponseMessage(message.content);
}
function handleErrorResponse(response) {
console.error(response.status, response);
alert(response.status + ':\n\n' + response.responseText);
handleResponseMessage([response.status, ':', response.responseText].join(' '));
}
}
function ajaxStart() {
dialog.setButtonsDisabled(true);
}
function ajaxEnd() {
dialog.setButtonsDisabled(false);
}
}
function handleResponseMessage(text) {
dialog.setMessage(1, text);
lastResponse = text;
}
/* Dialog code */
function createModalDialog(messages, config) {
messages = Array.isArray(messages) ? messages : [messages];
config = config || {
title: '',
buttonConfig: [
{
label: 'Ok',
},
],
};
const modal = createModal();
const header = createHeader(config.title);
const body = createBody(messages);
const buttonBar = createButtonBar(config.buttonConfig);
const configBar = createConfigBar(config.configContainer);
const closeButton = createCloseButton(modal);
modal.appendChild(createFlexboxWithBody([header, body, configBar, buttonBar], body));
modal.appendChild(closeButton);
document.body.appendChild(modal);
function createModal() {
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '50%';
modal.style.left = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.style.width = '80%';
modal.style.height = '80%';
modal.style.backgroundColor = 'white';
modal.style.border = '1px solid black';
modal.style.overflow = 'hidden';
modal.style.zIndex = '999999999';
return modal;
}
function createHeader(title) {
const header = document.createElement('div');
//header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.padding = '10px';
header.style.backgroundColor = 'lightgray';
header.style.borderBottom = '1px solid black';
const titleElement = document.createElement('div');
titleElement.textContent = title;
titleElement.style.fontSize = '1.5em';
header.appendChild(titleElement);
return header;
}
function createConfigBar(element) {
const configBar = document.createElement('div');
configBar.style.padding = '10px';
configBar.style.backgroundColor = 'lightgray';
configBar.style.borderBottom = '1px solid black';
if (element) {
configBar.appendChild(element);
}
return configBar;
}
function createBody(messages) {
const body = document.createElement('div');
body.style.overflow = 'auto';
body.style.padding = '10px';
body.style.display = 'flex';
body.style.flexDirection = 'column';
body.style.height = '100%';
messages.forEach((message) => {
const messageElement = document.createElement('textarea');
messageElement.value = message;
messageElement.style.width = '100%';
messageElement.style.resize = 'none';
messageElement.style.flexGrow = '1';
body.appendChild(messageElement);
});
return body;
}
function createButtonBar(buttonConfig) {
const buttonBar = document.createElement('div');
buttonBar.style.display = 'flex';
buttonBar.style.justifyContent = 'flex-end';
buttonBar.style.padding = '10px';
buttonBar.style.backgroundColor = 'lightgray';
buttonBar.style.borderTop = '1px solid black';
buttonConfig.forEach((button) => {
const buttonElement = createButton(button.label, button.onClick);
buttonBar.appendChild(buttonElement);
});
return buttonBar;
}
function createButton(label, onClick = closeModal) {
const button = document.createElement('button');
button.textContent = label;
button.style.marginLeft = '10px';
button.style.cursor = 'pointer';
button.addEventListener('click', onClick);
return button;
}
function createCloseButton() {
const closeButton = document.createElement('button');
closeButton.textContent = 'X';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.addEventListener('click', closeModal);
return closeButton;
}
function closeModal() {
document.body.removeChild(modal);
if (typeof config.closeCallback === 'function') {
config.closeCallback(modal);
}
}
function setMessage(index, message) {
const messageElements = body.querySelectorAll('textarea');
messageElements[index].value = message;
}
function setButtonConfig(buttonConfig) {
buttonBar.innerHTML = '';
buttonConfig.forEach((button) => {
const buttonElement = createButton(button.label, button.onClick);
buttonBar.appendChild(buttonElement);
});
}
function setButtonsDisabled(b) {
Array.from(buttonBar.querySelectorAll('button')).forEach(setButtonDisabled);
function setButtonDisabled(el) {
el.disabled = b;
}
}
function getElements() {
return {
modal,
header,
body,
buttonBar,
closeButton,
configBar,
};
}
return {
closeModal,
setMessage,
setButtonConfig,
getElements,
setButtonsDisabled,
};
}
function createFlexboxWithBody(arr, bodyElement) {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = '100%';
arr.forEach((element) => {
container.appendChild(element);
});
bodyElement.style.flexGrow = '1';
return container;
}
})();
To use the plugin:
- Edit the script to set your OpenAI API key in the config object.
- Select some text on a website
- A rainbow-bordered button appears near the scrollbar. Click it.
- Choose options and hit Summarize
- You will get a message informing you what it intends to send as a prompt. Click Ok to confirm, or Cancel to back out.
- After a few moments, the response will be returned