Custom view templates with scripts

Another small update: The theme settings are now also exposed as window.prodigy.theme and the {{theme}} variable within the HTML template. This includes the default colours and settings, and/or any modifications made by the user via the customTheme config.

I thought this could be pretty nice, because it’ll let you style custom interfaces more consistently, and keep all theming variables in one place. You can also add your own properties to the custom theme, e.g. "customTheme": {"customColor": "blue"}.

Example use case 1: in HTML

<mark style="background: {{theme.bgHighlight}}">{{label}}</mark>

Example use case 2: in CSS in <style> tag

<input required type="text" class="input" />

<style>
.input:invalid {
    border-color: {{theme.reject}};
}
</style>

Example use case 3: in JavaScript

function updateText() {
    const input = document.querySelector('.input');
    input.style.borderColor = window.prodigy.theme.accept;
    window.prodigy.update({ text: input.value });
}
1 Like

I want to implement something to help me be quicker for text classification. Simply, I just want to highlight certain keywords so I can visually jump to them for the classification of a long sentence or multiple sentences.

I tried generating the HTML in my data file, but the data is shown as-is (no rendering). I then added a column for the word to highlight, so I could modify it after the page loaded using a little Javascript, but turns out templates can’t be used inside the javascript config.

Any suggestions?

(I realize however that maybe I can chop my data records up a little more, that might be the way I’ll go, but curious about what else could be done here without changing the data processing part.)

@hadsed If you just want to highlight certain words, you might not even need a custom template. You could simply add a "spans" property to the annotation tasks (just like when you’re annotating named entities). For example:

{
    "text": "Super long text",
    "label": "SOME_LABEL",
    "spans": [
        {"start": 20, "end": 30},
        {"start": 100, "end": 110}
    ]
}

The only thing that’s important here is that you need to know the character offsets of your keywords – but those should be pretty easy to generate. If a text classification task has spans assigned, Prodigy will highlight those inline. You could even add a "label" to the span.

That said, if you do want to use a custom template for more flexibility, make sure to set 'view_id': 'html' in your (custom) recipe, and add the HTML as the task’s "html" key. You can still add a "text" key that contains the plain text – this makes sense if you’re annotating with a model in the loop, or later want to use the annotations to train a model. The text classifier for example will then be trained on the "text" (not the HTML containing your custom markup).

{
    "text": "some text",
    "html": "<strong>some text</strong>"
}

Btw, the custom JavaScript examples I outlined in this thread already work! They’re just not documented yet, because they’re very experimental. So if you have a look at my examples above, you can see how to add custom JavaScript (via the recipe’s "javascript" config setting) and how to access the current task data and UI actions from within your script.

A post was merged into an existing topic: Custom HTML

fyi: I’m using text boxes like this and while a and space work, delete is not working for me (it still acts as going to the previous annotation).

Thanks – I'll double check that. I might have already fixed this internally, actually. In the meantime, stopping the key event from propagating in the textbox should probably work. Kinda like this:

Btw, quick update on the thread topic: Custom JS for all interfaces and a global CSS setting is already implemented and will be shipped with the next version :smiley:

1 Like

Hi I am trying to implement your solution to user inputs for a machine translation annotation task. While I was able to successfully create my own user input button while following your template, I am struggling to get Prodigy to save the user inputted text into the final exported annotations. Any advice would be appreciated.

@jlanday Could you share your current script? The update method is exposed as window.prodigy.update. It takes an object of properties and will update the current task by performing a shallow update. So when you want to update the task (e.g. on the input’s onchange event or when the user clicks a button), you can do something like this:

const input = document.querySelector('.input') // or whatevr
window.prodigy.update({ user_input: input.value })

This will overwrite/add the task property 'user_input' and set it to the value of the input.

Hi,

Sorry for the delay…

First, I have textcat_eval.js which is the following:

function updateFromInput() {
    const text = document.querySelector('.input').value;
    window.prodigy.update({ user_text: text });
}

Second, I have textcat.html which is the following:

<h2>{{text}}</h2>

<input type="text" class="input" placeholder="User text here..." />
<button onClick="updateFromInput()">Update</button>
<br />
{{user_text}}

Finally, my recipe is the following:


import prodigy
from prodigy.components.loaders import JSONL

with open('textcat_eval.html') as txt:
    template_text = txt.read()
with open('textcat_eval.js') as txt:
    script_text = txt.read()

@prodigy.recipe('sentiment',dataset=prodigy.recipe_args['dataset'],file_path=("Path to texts", "positional", None, str))
def sentiment(dataset, file_path):
    """Annotate the sentiment of texts using different mood options."""
    stream = JSONL(file_path)     # load in the JSONL file
    return {
        'dataset':dataset,
        'stream':stream,
        'view_id': 'html',
        'config': {
            'html_template': template_text,
            'html_script': script_text,
        }
    }

Thanks again !

This should be 'javascript'! But aside from that your recipe looks good :+1:

This is cool stuff and works really well :slight_smile: I guess as soon as you use a html_template, you can only use the html interface? Like is there an obvious way to combine this with presets like the choice interface or NER annotations in the text?

@SofieVL Yay, that's nice to hear! And yes, the html_template is currently limited to the html – although I think you can kinda trick the app into using it to render the content in interfaces like choice or classification by passing it an empty "html" property in the task.

Custom CSS and JavaScript are now supported for all interfaces, so you can always do something like this and append elements to the annotation card:

const content = document.querySelector('.prodigy-content')
const button = document.createElement('button')
button.textContent = 'Click me!'
// add event listener / onclick handler etc.
contend.appendChild(button)
1 Like

@ines: you're right, it does kind of work when specifying the html tags in the choice interface properly:

However it looks like you can't actually type in the box. Because as soon as your mouse goes there and selects the box, the radio button is selected instead, and focus moves aways from the text box.

Ah yeah, the choice options weren't really designed to have interactive content in them. So you probbly want to just add the field at the bottom below the options (instead of actually making it an option itself).

Hi @ines,
my window.prodigy.content is pointing to the wrong content when using view_id blocks.
My html_template consist of buttons and the js_templates holds the onClick functions. The OnClick are supposed to highlight some words. I planned on doing this by appending Spans to window.prodigy.content.spans and then updating it. However, it is not showing on the text visualization of the ner_manual block. May I know where I was wrong?
Thanks!

recipe.py
    "config": {
      "block": [
       { "view_id":"ner_manual", "labels": ["PERSON","ORG"] },
       { "view_id": "html", "html_template": get_html_template()}
      ],
      "javascript": get_js_template()
      }

Hi! The ner_manual interface currently keeps its own copy of the spans – calling update does update the task content, but it doesn't necessarily trigger a re-render of a built-in component. I do want to change this, though, since use cases like the one you describe are pretty cool. It's just not implemented yet.

You can read more about it here:

Hi @ines,
Thanks! I understand!
As a temporary workaround, may I ask if there is any way for me to trigger React to re-render? Perhaps using a pseudo state change?

Ah, good idea! It's hacky, but you could make it think that it's receiving a new task by updating the _task_hash of the current task. Just tested it and the following works for me:

const taskHash = window.prodigy.content._task_hash
window.prodigy.update({ _task_hash: 0 })
window.prodigy.update({ _task_hash: taskHash })

Just make sure to reset it to its original value again – otherwise, you'll end up with bad task hashes in your dataset (which impacts what Prodigy considers duplicates etc.). Maybe also try it with a test dataset first to check that there are no unintended side-effects on the client.

Hello @ines, do you know how I can update the selected label using custom view templates?

Here's what I have so far:

Custom dropdown HTML:

<div class="custom-dropdown" onchange="updateLabel()">
  <select id="labels">
    {label_options_str}
  </select>
</div>

Custom JS:

function updateLabel() {
    const selectedLabel = document.querySelector('select').value;
    console.log(selectedLabel)
    window.prodigy.update({ label: selectedLabel });
}

I am able to confirm that the custom javascript is correctly logging the selected label, but I'm not sure how to actually set the selected label in prodigy, since I don't see any examples of updating the selected label in the window.prodigy interface.

The .update() call in my custom JS was a wild guess, and sure enough, it didn't work (it's just using the first item in the dropdown, which is NO_LABEL in the below screenshot):

So, in summary, my question is: Is it possible to update the label from a custom HTML interface like this?

Thanks in advance!

-Luc

P.S. The reason I'm using a custom dropdown is that the default dropdown for a list of labels provided by the --dropdown prodigy option is not flexible enough for our image_manual use case, which requires using several <optgroup>'s in the select bar.

1 Like

Glad you got it to work so far :smiley:

The window.prodigy.update function will update the current task dictionary with whatever data you pass in. So in your code, you're adding the top-level property "label" to the dictionary – but I think that's not what you want, right? You'd want the label to be added to the entry in the "spans" that was just added.