Natural Language Processing Tools

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

screenshot

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: