drafts icon indicating copy to clipboard operation
drafts copied to clipboard

Related Words Action

Open extratone opened this issue 5 months ago • 0 comments

Updated 07122025-072113


Social

Dev


Conversation with Gemini

I would like to create an Action for Drafts (docs.getdrafts.com) that queries the ml endpoint of the Datamuse API (https://www.datamuse.com/api/) for the currently-selected word, parses the responses, presents them as a selectable list (including all of the relevant returned data) using Drafts' custom html previews (https://docs.getdrafts.com/docs/actions/html-forms) from which the user's selection will replace the selected term.

Of course, David. Here is a detailed breakdown and the complete code for a Drafts Action that accomplishes your desired workflow.

This Action will get the currently selected text in a draft, query the Datamuse API for words with similar meaning, and then present the results in a user-friendly, selectable HTML list. Upon selection, the original text will be replaced with the chosen word.


Datamuse Related Words - Drafts Action

This action is composed of a single "Script" step. Below is the JavaScript code you will need.

The Code

JavaScript

// A Drafts Action to find words with similar meaning using the Datamuse API
// and replace the current selection with the user's choice from an HTML preview.

// Ensure there is a selection to work with.
const selectedText = editor.getSelectedText();
if (!selectedText || selectedText.length === 0) {
    alert("Please select a word to look up.");
    context.cancel();
} else {
    // Define the Datamuse API endpoint.
    const baseURL = "https://api.datamuse.com/words";
    const queryString = \`?ml=${encodeURIComponent(selectedText)}&md=dps\`;

    // Create and send the HTTP request.
    let http = HTTP.create();
    let response = http.request({
        "url": \`${baseURL}${queryString}\`,
        "method": "GET"
    });

    if (response.success) {
        let words = JSON.parse(response.responseText);
        if (words.length > 0) {
            // Create and configure the HTML preview.
            let html = \`
<!DOCTYPE html>
<html>
<head>
<title>Datamuse Related Words</title>
<style>
    body { font-family: -apple-system, sans-serif; margin: 0; }
    #wordList { list-style-type: none; padding: 0; }
    .word-item { border-bottom: 1px solid #ddd; padding: 10px; cursor: pointer; }
    .word-item:hover { background-color: #f0f0f0; }
    .word { font-weight: bold; font-size: 1.2em; }
    .defs { margin-left: 15px; color: #555; }
    .score { float: right; color: #888; }
</style>
</head>
<body>
    <ul id="wordList">
\`;

            // Populate the list with words from the API response.
            for (let word of words) {
                let definitions = word.defs ? word.defs.join('<br>') : 'No definition available';
                html += \`<li class="word-item" onclick="selectWord('${word.word}')">
                    <span class="score">${word.score}</span>
                    <div class="word">${word.word}</div>
                    <div class="defs">${definitions}</div>
                </li>\`;
            }

            html += \`
    </ul>
    <script>
        function selectWord(word) {
            // Send the selected word back to the Drafts action.
            Drafts.send("selectedWord", word);
            Drafts.continue();
        }
    </script>
</body>
</html>
\`;

            // Display the HTML preview.
            let preview = HTMLPreview.create();
            preview.show(html);

            // Process the user's selection from the preview.
            if (preview.didSucceed) {
                const newWord = preview.context.previewValues.selectedWord;
                if (newWord) {
                    editor.setSelectedText(newWord);
                }
            }

        } else {
            alert(\`No related words found for "${selectedText}".\`);
        }
    } else {
        alert(\`Error contacting Datamuse API: ${response.statusCode}\`);
        context.cancel();
    }
}

How to Create the Action in Drafts

  1. Open Drafts and navigate to the Action list.
  2. Tap the + button to create a new action.
  3. Name the action something descriptive, like "Datamuse Related Words".
  4. In the "STEPS" section, tap the + and select the "Script" step.
  5. Copy the code above and paste it into the script editor.
  6. Save the action.

How to Use the Action

  1. In a draft, select the word you want to look up.
  2. Run the "Datamuse Related Words" action from your action list.
  3. An HTML preview will appear, listing related words.
  4. Tap on a word in the list to choose it.
  5. The HTML preview will close, and your original selection will be replaced with the new word.

Explanation of the Code

  • editor.getSelectedText(): This Drafts-specific function retrieves the text you have currently highlighted.
  • Datamuse API Call:
    • An HTTP GET request is made to https://api.datamuse.com/words.
    • The ml parameter is set to your selected word to find words with a similar meaning.
    • md=dps is included to request definitions (d), parts of speech (p), and scores (s) for the returned words.
  • HTML Generation:
    • If the API call is successful and returns words, a string of HTML is dynamically created.
    • A for...of loop iterates through the words array returned by the API.
    • For each word, an <li> element is created containing the word itself, its definitions, and its score.
    • An onclick attribute is added to each list item, which calls the selectWord() JavaScript function when tapped.
  • HTMLPreview:
    • The HTMLPreview.create() method instantiates a new preview window.
    • preview.show(html) displays the generated HTML to you.
  • Drafts.send():
    • This function, embedded within the HTML, is crucial for communication from the web view back to the Drafts action.
    • When a word is selected, Drafts.send("selectedWord", word) sends the chosen word back to the action's context.
  • Replacing the Text:
    • After the preview is dismissed (by calling Drafts.continue() from within the HTML), the script checks if the preview succeeded.
    • preview.context.previewValues.selectedWord retrieves the word sent from the HTML preview.
    • Finally, editor.setSelectedText(newWord) replaces the originally selected text with the new word.

This action should provide a seamless way to enrich your writing within Drafts by quickly finding and substituting related words.

I believe that code needs to be separated per Action Step. There should be one Script action step and one Custom HTML preview action step.

You are absolutely correct, David. My apologies for consolidating the logic into a single script. A multi-step action is indeed the more robust and proper way to implement this in Drafts, separating the data fetching, user interaction, and final processing.

This approach offers better modularity and is the standard practice for actions involving HTMLPreview.

Here is the revised, correct structure using three distinct action steps.


Datamuse Related Words (Multi-Step)

This action is composed of three sequential steps:

  1. Script: Fetches the data from Datamuse and prepares the HTML.
  2. HTML Preview: Displays the interactive list to the user.
  3. Script: Processes the user's selection and updates the draft.

Step 1: Fetch Related Words (Script)

This step gets the selected text, calls the API, and builds the HTML content, which it then passes to the next step using a template tag.

  • Action Type: Script
  • Name:Fetch Related Words (or similar)

Code for Step 1:

JavaScript

// Step 1: Fetch Related Words & Prepare HTML

// Get the currently selected text.
const selectedText = editor.getSelectedText();

// If there's no selection, stop the action.
if (!selectedText || selectedText.length === 0) {
    alert("Please select a word to look up.");
    context.fail("No text selected."); // Use context.fail() to halt the multi-step action.
}

// Define the Datamuse API endpoint and parameters.
const baseURL = "https://api.datamuse.com/words";
// md=dps requests definitions, parts of speech, and scores.
const queryString = \`?ml=${encodeURIComponent(selectedText)}&md=dps\`;

// Create and send the HTTP request.
const http = HTTP.create();
const response = http.request({
    "url": \`${baseURL}${queryString}\`,
    "method": "GET"
});

// Check if the request was successful.
if (response.success) {
    const words = JSON.parse(response.responseText);

    if (words && words.length > 0) {
        // Begin building the HTML string.
        let html = \`
<!DOCTYPE html>
<html>
<head>
<title>Datamuse Related Words</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
    body { font-family: -apple-system, sans-serif; margin: 0; background-color: #f7f7f7; }
    #wordList { list-style-type: none; padding: 0; }
    .word-item { border-bottom: 1px solid #ddd; padding: 12px 15px; cursor: pointer; background-color: #fff; }
    .word-item:hover { background-color: #eaf4ff; }
    .word-container { display: flex; justify-content: space-between; align-items: center; }
    .word { font-weight: bold; font-size: 1.2em; color: #007aff; }
    .defs { margin-top: 5px; color: #333; font-size: 0.9em; }
    .score { color: #888; font-size: 0.8em; }
    .pos { font-style: italic; color: #666; margin-left: 8px; font-size: 0.8em; }
</style>
</head>
<body>
    <ul id="wordList">
\`;

        // Loop through the API response and create a list item for each word.
        for (const word of words) {
            const definitions = word.defs ? word.defs.join('; ').replace(/"/g, '&quot;') : 'No definition available';
            const partOfSpeech = word.tags && word.tags.find(t => t.startsWith('n') || t.startsWith('v') || t.startsWith('adj') || t.startsWith('adv')) || '';
            
            html += \`<li class="word-item" onclick="selectWord('${word.word.replace(/'/g, "\\'")}')">
                <div class="word-container">
                    <div>
                        <span class="word">${word.word}</span>
                        <span class="pos">${partOfSpeech}</span>
                    </div>
                    <span class="score">score: ${word.score}</span>
                </div>
                <div class="defs">${definitions}</div>
            </li>\`;
        }

        html += \`
    </ul>
    <script>
        function selectWord(word) {
            // Send the selected word back to Drafts.
            // This creates a template tag named [[selected_word]] for the next step.
            Drafts.send("selected_word", word);
            // This tells the HTML Preview step to finish and proceed.
            Drafts.continue();
        }
    </script>
</body>
</html>
\`;

        // Create a template tag named [[html]] containing our generated HTML.
        draft.setTemplateTag("html", html);

    } else {
        alert(\`No related words found for "${selectedText}".\`);
        context.fail(\`No results for ${selectedText}\`);
    }
} else {
    // If the API call fails, show an error and stop the action.
    alert(\`Error contacting Datamuse API: ${response.statusCode}\n\n${response.error}\`);
    context.fail("API Error");
}

Step 2: Display Words (HTML Preview)

This step uses the [[html]] tag generated by the previous step to display the interactive web view.

  • Action Type: HTML Preview
  • Name:Display Words (or similar)
  • In the HTML field, you will place only the following line:
[[html]]

This tells the step to render the content of the html template tag. The JavaScript embedded within that HTML (from Step 1) will handle sending the selected word back to Drafts.


Step 3: Replace Text (Script)

This final step runs after the user makes a selection in the HTML Preview. It retrieves the chosen word and uses it to replace the original selection in the editor.

  • Action Type: Script
  • Name:Replace Text (or similar)

Code for Step 3:

JavaScript

// Step 3: Process Selection and Replace Text

// Retrieve the value sent from the HTML Preview.
// Drafts.send("selected_word", word) in the HTML creates this tag.
const newWord = draft.getTemplateTag("selected_word");

// Ensure a word was actually returned before proceeding.
if (newWord && newWord.length > 0) {
    // Replace the originally selected text with the new word.
    editor.setSelectedText(newWord);
} else {
    // This part is optional but good for debugging. It would only run if the user
    // managed to close the preview without making a selection.
    context.cancel("No word was selected from the preview.");
}

How to Create the Action

  1. Open Drafts and create a new action.
  2. Give the action a name (e.g., "Datamuse Related Words").
  3. Add the first Script step, name it, and paste the code from Step 1.
  4. Add a second step, selecting HTML Preview. Name it and enter [[html]] into its text field.
  5. Add the third and final Script step, name it, and paste the code from Step 3.
  6. Save the action.

Your action now consists of three distinct steps in the editor, which will execute in sequence, providing the robust workflow you described. Thank you for the correction.## Conversation with Gemini

I would like to create an Action for Drafts (docs.getdrafts.com) that queries the ml endpoint of the Datamuse API (https://www.datamuse.com/api/) for the currently-selected word, parses the responses, presents them as a selectable list (including all of the relevant returned data) using Drafts' custom html previews (https://docs.getdrafts.com/docs/actions/html-forms) from which the user's selection will replace the selected term.

Of course, David. Here is a detailed breakdown and the complete code for a Drafts Action that accomplishes your desired workflow.

This Action will get the currently selected text in a draft, query the Datamuse API for words with similar meaning, and then present the results in a user-friendly, selectable HTML list. Upon selection, the original text will be replaced with the chosen word.


Datamuse Related Words - Drafts Action

This action is composed of a single "Script" step. Below is the JavaScript code you will need.

The Code

JavaScript

// A Drafts Action to find words with similar meaning using the Datamuse API
// and replace the current selection with the user's choice from an HTML preview.

// Ensure there is a selection to work with.
const selectedText = editor.getSelectedText();
if (!selectedText || selectedText.length === 0) {
    alert("Please select a word to look up.");
    context.cancel();
} else {
    // Define the Datamuse API endpoint.
    const baseURL = "https://api.datamuse.com/words";
    const queryString = \`?ml=${encodeURIComponent(selectedText)}&md=dps\`;

    // Create and send the HTTP request.
    let http = HTTP.create();
    let response = http.request({
        "url": \`${baseURL}${queryString}\`,
        "method": "GET"
    });

    if (response.success) {
        let words = JSON.parse(response.responseText);
        if (words.length > 0) {
            // Create and configure the HTML preview.
            let html = \`
<!DOCTYPE html>
<html>
<head>
<title>Datamuse Related Words</title>
<style>
    body { font-family: -apple-system, sans-serif; margin: 0; }
    #wordList { list-style-type: none; padding: 0; }
    .word-item { border-bottom: 1px solid #ddd; padding: 10px; cursor: pointer; }
    .word-item:hover { background-color: #f0f0f0; }
    .word { font-weight: bold; font-size: 1.2em; }
    .defs { margin-left: 15px; color: #555; }
    .score { float: right; color: #888; }
</style>
</head>
<body>
    <ul id="wordList">
\`;

            // Populate the list with words from the API response.
            for (let word of words) {
                let definitions = word.defs ? word.defs.join('<br>') : 'No definition available';
                html += \`<li class="word-item" onclick="selectWord('${word.word}')">
                    <span class="score">${word.score}</span>
                    <div class="word">${word.word}</div>
                    <div class="defs">${definitions}</div>
                </li>\`;
            }

            html += \`
    </ul>
    <script>
        function selectWord(word) {
            // Send the selected word back to the Drafts action.
            Drafts.send("selectedWord", word);
            Drafts.continue();
        }
    </script>
</body>
</html>
\`;

            // Display the HTML preview.
            let preview = HTMLPreview.create();
            preview.show(html);

            // Process the user's selection from the preview.
            if (preview.didSucceed) {
                const newWord = preview.context.previewValues.selectedWord;
                if (newWord) {
                    editor.setSelectedText(newWord);
                }
            }

        } else {
            alert(\`No related words found for "${selectedText}".\`);
        }
    } else {
        alert(\`Error contacting Datamuse API: ${response.statusCode}\`);
        context.cancel();
    }
}

How to Create the Action in Drafts

  1. Open Drafts and navigate to the Action list.
  2. Tap the + button to create a new action.
  3. Name the action something descriptive, like "Datamuse Related Words".
  4. In the "STEPS" section, tap the + and select the "Script" step.
  5. Copy the code above and paste it into the script editor.
  6. Save the action.

How to Use the Action

  1. In a draft, select the word you want to look up.
  2. Run the "Datamuse Related Words" action from your action list.
  3. An HTML preview will appear, listing related words.
  4. Tap on a word in the list to choose it.
  5. The HTML preview will close, and your original selection will be replaced with the new word.

Explanation of the Code

  • editor.getSelectedText(): This Drafts-specific function retrieves the text you have currently highlighted.
  • Datamuse API Call:
    • An HTTP GET request is made to https://api.datamuse.com/words.
    • The ml parameter is set to your selected word to find words with a similar meaning.
    • md=dps is included to request definitions (d), parts of speech (p), and scores (s) for the returned words.
  • HTML Generation:
    • If the API call is successful and returns words, a string of HTML is dynamically created.
    • A for...of loop iterates through the words array returned by the API.
    • For each word, an <li> element is created containing the word itself, its definitions, and its score.
    • An onclick attribute is added to each list item, which calls the selectWord() JavaScript function when tapped.
  • HTMLPreview:
    • The HTMLPreview.create() method instantiates a new preview window.
    • preview.show(html) displays the generated HTML to you.
  • Drafts.send():
    • This function, embedded within the HTML, is crucial for communication from the web view back to the Drafts action.
    • When a word is selected, Drafts.send("selectedWord", word) sends the chosen word back to the action's context.
  • Replacing the Text:
    • After the preview is dismissed (by calling Drafts.continue() from within the HTML), the script checks if the preview succeeded.
    • preview.context.previewValues.selectedWord retrieves the word sent from the HTML preview.
    • Finally, editor.setSelectedText(newWord) replaces the originally selected text with the new word.

This action should provide a seamless way to enrich your writing within Drafts by quickly finding and substituting related words.

I believe that code needs to be separated per Action Step. There should be one Script action step and one Custom HTML preview action step.

You are absolutely correct, David. My apologies for consolidating the logic into a single script. A multi-step action is indeed the more robust and proper way to implement this in Drafts, separating the data fetching, user interaction, and final processing.

This approach offers better modularity and is the standard practice for actions involving HTMLPreview.

Here is the revised, correct structure using three distinct action steps.


Datamuse Related Words (Multi-Step)

This action is composed of three sequential steps:

  1. Script: Fetches the data from Datamuse and prepares the HTML.
  2. HTML Preview: Displays the interactive list to the user.
  3. Script: Processes the user's selection and updates the draft.

Step 1: Fetch Related Words (Script)

This step gets the selected text, calls the API, and builds the HTML content, which it then passes to the next step using a template tag.

  • Action Type: Script
  • Name:Fetch Related Words (or similar)

Code for Step 1:

JavaScript

// Step 1: Fetch Related Words & Prepare HTML

// Get the currently selected text.
const selectedText = editor.getSelectedText();

// If there's no selection, stop the action.
if (!selectedText || selectedText.length === 0) {
    alert("Please select a word to look up.");
    context.fail("No text selected."); // Use context.fail() to halt the multi-step action.
}

// Define the Datamuse API endpoint and parameters.
const baseURL = "https://api.datamuse.com/words";
// md=dps requests definitions, parts of speech, and scores.
const queryString = \`?ml=${encodeURIComponent(selectedText)}&md=dps\`;

// Create and send the HTTP request.
const http = HTTP.create();
const response = http.request({
    "url": \`${baseURL}${queryString}\`,
    "method": "GET"
});

// Check if the request was successful.
if (response.success) {
    const words = JSON.parse(response.responseText);

    if (words && words.length > 0) {
        // Begin building the HTML string.
        let html = \`
<!DOCTYPE html>
<html>
<head>
<title>Datamuse Related Words</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
    body { font-family: -apple-system, sans-serif; margin: 0; background-color: #f7f7f7; }
    #wordList { list-style-type: none; padding: 0; }
    .word-item { border-bottom: 1px solid #ddd; padding: 12px 15px; cursor: pointer; background-color: #fff; }
    .word-item:hover { background-color: #eaf4ff; }
    .word-container { display: flex; justify-content: space-between; align-items: center; }
    .word { font-weight: bold; font-size: 1.2em; color: #007aff; }
    .defs { margin-top: 5px; color: #333; font-size: 0.9em; }
    .score { color: #888; font-size: 0.8em; }
    .pos { font-style: italic; color: #666; margin-left: 8px; font-size: 0.8em; }
</style>
</head>
<body>
    <ul id="wordList">
\`;

        // Loop through the API response and create a list item for each word.
        for (const word of words) {
            const definitions = word.defs ? word.defs.join('; ').replace(/"/g, '&quot;') : 'No definition available';
            const partOfSpeech = word.tags && word.tags.find(t => t.startsWith('n') || t.startsWith('v') || t.startsWith('adj') || t.startsWith('adv')) || '';
            
            html += \`<li class="word-item" onclick="selectWord('${word.word.replace(/'/g, "\\'")}')">
                <div class="word-container">
                    <div>
                        <span class="word">${word.word}</span>
                        <span class="pos">${partOfSpeech}</span>
                    </div>
                    <span class="score">score: ${word.score}</span>
                </div>
                <div class="defs">${definitions}</div>
            </li>\`;
        }

        html += \`
    </ul>
    <script>
        function selectWord(word) {
            // Send the selected word back to Drafts.
            // This creates a template tag named [[selected_word]] for the next step.
            Drafts.send("selected_word", word);
            // This tells the HTML Preview step to finish and proceed.
            Drafts.continue();
        }
    </script>
</body>
</html>
\`;

        // Create a template tag named [[html]] containing our generated HTML.
        draft.setTemplateTag("html", html);

    } else {
        alert(\`No related words found for "${selectedText}".\`);
        context.fail(\`No results for ${selectedText}\`);
    }
} else {
    // If the API call fails, show an error and stop the action.
    alert(\`Error contacting Datamuse API: ${response.statusCode}\n\n${response.error}\`);
    context.fail("API Error");
}

Step 2: Display Words (HTML Preview)

This step uses the [[html]] tag generated by the previous step to display the interactive web view.

  • Action Type: HTML Preview
  • Name:Display Words (or similar)
  • In the HTML field, you will place only the following line:
[[html]]

This tells the step to render the content of the html template tag. The JavaScript embedded within that HTML (from Step 1) will handle sending the selected word back to Drafts.


Step 3: Replace Text (Script)

This final step runs after the user makes a selection in the HTML Preview. It retrieves the chosen word and uses it to replace the original selection in the editor.

  • Action Type: Script
  • Name:Replace Text (or similar)

Code for Step 3:

JavaScript

// Step 3: Process Selection and Replace Text

// Retrieve the value sent from the HTML Preview.
// Drafts.send("selected_word", word) in the HTML creates this tag.
const newWord = draft.getTemplateTag("selected_word");

// Ensure a word was actually returned before proceeding.
if (newWord && newWord.length > 0) {
    // Replace the originally selected text with the new word.
    editor.setSelectedText(newWord);
} else {
    // This part is optional but good for debugging. It would only run if the user
    // managed to close the preview without making a selection.
    context.cancel("No word was selected from the preview.");
}

How to Create the Action

  1. Open Drafts and create a new action.
  2. Give the action a name (e.g., "Datamuse Related Words").
  3. Add the first Script step, name it, and paste the code from Step 1.
  4. Add a second step, selecting HTML Preview. Name it and enter [[html]] into its text field.
  5. Add the third and final Script step, name it, and paste the code from Step 3.
  6. Save the action.

Your action now consists of three distinct steps in the editor, which will execute in sequence, providing the robust workflow you described. Thank you for the correction.º

Here are all the keyboard shortcuts available in GoodLinks that aren't listed in the menu bar.

Everywhere

| ----- | | Space | Scroll or go to the next link. | | ⇧ Shift + Space | Scroll or go to the previous link. | | N | Go to the next link. | | P | Go to the previous link. | | R or U | Toggle read status. | | S | Toggle starred status. | | E | Edit link. |

Sidebar

| ----- | | → | Move focus to list. |

List

| ----- | | ← | Move focus to sidebar. | | → | Move focus to article view. | | ⌘ Command + ↑ | Select the first link. | | ⌘ Command + ↓ | Select the last link. |

Article View

| ----- | | ← | Move focus to list. | | → | Move focus to notebook. | | H | Highlight selection. | | A | Note selection. |

Notebook

| ----- | | ← | Move focus to article view. | | Return | Edit note. |

extratone avatar Jul 12 '25 12:07 extratone