Save Custom Html form data to dataset !

The Recipe ain't saving dropdown values from custom html to dataset output or export. db-out !

import prodigy
from prodigy.components.loaders import Images

@prodigy.recipe('image-caption')
def image_caption(dataset, image_path):
	stream = Images(image_path)

	îndurările = ['milă', 'îndurare', 'milostenie', 'compasiune', 'iertare', 'caritate', 'compătimire', 'graţie', 'eleganţă', 'bunăvoinţă', 'farmec','iertare','compătimire','toleranţă', 'răbdare','abţinere', 'clemenţă', 'blândeţe', 'indulgenţă', 'îngăduinţă']
	complimentele = ['frumoasă', 'luminoasă', 'sclipitoare', 'orbitoare', 'elegantă', 'sclipitoare', 'superbă', 'mare', 'eleganţă', 'impozantă', 'impresionantă','generoasă','lucioasă','magnifică', 'minunată','împodobită', 'bogată', 'plină-de-noroi', 'somptuosă', 'radiantă', 'costisitoare', 'fabuloasă', 'strălucitoare', 'grandioasă', 'nebună', 'magnifică', 'aur-solid', 'splendidă', 'elegantă']
	
	îndurările_ = ''.join([f"<option value='{w}'>{w}</option>" for w in îndurările])
	complimentele_ = ''.join([f"<option value='{w}>{w}</option>" for w in complimentele])

	print(îndurările_)
	print(complimentele_)

	blocks = [
		{'view_id': 'image'},
		{'view_id': 'html', 'html_template': f"<div style='opacity: 0.5'><select name='complimentele' id='complimentele'> <option value='frumoasă'>frumoasă</option><option value='luminoasă'>luminoasă</option><option value='sclipitoare'>sclipitoare</option><option value='orbitoare'>orbitoare</option><option value='elegantă'>elegantă</option><option value='sclipitoare'>sclipitoare</option><option value='superbă'>superbă</option><option value='mare'>mare</option><option value='eleganţă'>eleganţă</option><option value='impozantă'>impozantă</option><option value='impresionantă'>impresionantă</option><option value='generoasă'>generoasă</option><option value='lucioasă'>lucioasă</option><option value='magnifică'>magnifică</option><option value='minunată'>minunată</option><option value='împodobită'>împodobită</option><option value='bogată'>bogată</option><option value='plină-de-noroi'>plină-de-noroi</option><option value='somptuosă'>somptuosă</option><option value='radiantă'>radiantă</option><option value='costisitoare'>costisitoare</option><option value='fabuloasă'>fabuloasă</option><option value='strălucitoare'>strălucitoare</option><option value='grandioasă'>grandioasă</option><option value='nebună'>nebună</option><option value='magnifică'>magnifică</option><option value='aur-solid'>aur-solid</option><option value='splendidă'>splendidă</option><option value='elegantă'>elegantă</option></select></div>", 'field_id': 'text', 'field_autofocus': True},
		{'view_id': 'html', 'html_template': "<div style='opacity: 0.5'><strong> Cerere de </strong></div>", 'field_id': 'text', 'field_autofocus': True},
		{'view_id': 'html', 'html_template': f"<div style='opacity: 0.5'><select name='îndurările' id='indurarile'><option value='milă'>milă</option><option value='îndurare'>îndurare</option><option value='milostenie'>milostenie</option><option value='compasiune'>compasiune</option><option value='iertare'>iertare</option><option value='caritate'>caritate</option><option value='compătimire'>compătimire</option><option value='graţie'>graţie</option><option value='eleganţă'>eleganţă</option><option value='bunăvoinţă'>bunăvoinţă</option><option value='farmec'>farmec</option><option value='iertare'>iertare</option><option value='compătimire'>compătimire</option><option value='toleranţă'>toleranţă</option><option value='răbdare'>răbdare</option><option value='abţinere'>abţinere</option><option value='clemenţă'>clemenţă</option><option value='blândeţe'>blândeţe</option><option value='indulgenţă'>indulgenţă</option><option value='îngăduinţă'>îngăduinţă</option> </select></div>", 'field_id': 'text', 'field_autofocus': True}
	 ]

	return {
		'dataset': dataset,
		'stream': stream,
		'view_id': 'blocks',
		'config': {'blocks': blocks}
	}

Welcome to the forum @calinjovrea! :wave:

Prodigy on its own won't be able to know how to process custom UI fields which is why you still need to provide javascript functions that handle the user input and store it in the DB.

For example, you could add an update callback to your html dropdown that gets triggered on change

function update() {
    // Update the default values of `selected_complimentele` & `selected_indurarile` keys with the selected options
    
    const complimenteleElement = document.getElementById("complimentele");
    const indurarileElement = document.getElementById("indurarile");

    if (complimenteleElement && indurarileElement) {
        const complimentele = complimenteleElement.value;
        const indurarile = indurarileElement.value;
        
        prodigy.update({
            selected_complimentele: complimentele,
            selected_indurarile: indurarile
        });
    } else {
        if (!complimenteleElement) {
            console.error('Element with id "complimentele" not found.');
        }
        if (!indurarileElement) {
            console.error('Element with id "indurarile" not found.');
        }
    }
}

This function callsprodigy.update function that is responsible for updating the current state of the task. Other similar functions that you can interact with via javascript are documented here.

You probably want to restore the options to the default value once the user has answered the question. For that you could specify the reset function and listen to prodigyanswer event:


function reset(elementId, firstOption) {
    const element = document.getElementById(elementId);
    if (element) {
        element.value = firstOption;
    } else {
        console.error(`Element with id "${elementId}" not found.`);
    }
}
document.addEventListener('prodigyanswer', (event) => {
    reset("complimentele", "frumoasă");
    reset("indurarile", "milă");
});

Other events that you can listen to are documented in the same section linked above. You can even specify custom events but it won't be necessary for this particular case.

For simplicity, I'm hardcoding the default values to restore but you could as well access the array of options and choose the first one.

One case that I have not covered so far is when the user accepts the options without modifying anything. Our update will only be triggered when the user changes the field, so I prepopulated the target fields (selected_complimentele and selected_indurarile) with the default value at the recipe level (see full code below).

In summary, the modified recipe would look like this:

import prodigy
from prodigy.components.loaders import Images
from pathlib import Path

def add_default_options(stream, field: str, default_selection: str):
    for eg in stream:
        eg[field] = default_selection
        yield eg

@prodigy.recipe('image-caption')
def image_caption(dataset, image_path):
    stream = Images(image_path)

    îndurările = ['milă', 'îndurare', 'milostenie', 'compasiune', 'iertare', 'caritate', 'compătimire', 'graţie', 'eleganţă', 'bunăvoinţă', 'farmec','iertare','compătimire','toleranţă', 'răbdare','abţinere', 'clemenţă', 'blândeţe', 'indulgenţă', 'îngăduinţă']
    complimentele = ['frumoasă', 'luminoasă', 'sclipitoare', 'orbitoare', 'elegantă', 'sclipitoare', 'superbă', 'mare', 'eleganţă', 'impozantă', 'impresionantă','generoasă','lucioasă','magnifică', 'minunată','împodobită', 'bogată', 'plină-de-noroi', 'somptuosă', 'radiantă', 'costisitoare', 'fabuloasă', 'strălucitoare', 'grandioasă', 'nebună', 'magnifică', 'aur-solid', 'splendidă', 'elegantă']
    
    îndurările_ = ''.join([f"<option value='{w}'>{w}</option>" for w in îndurările])
    complimentele_ = ''.join([f"<option value='{w}>{w}</option>" for w in complimentele])
    stream = add_default_options(stream, 'selected_complimentele', complimentele[0])
    stream = add_default_options(stream, 'selected_indurarile', îndurările[0])
    custom_js = Path("custom.js").read_text()

    blocks = [
        {'view_id': 'image'},
        {'view_id': 'html', 'html_template': f"<div style='opacity: 0.5'><select name='complimentele' id='complimentele' onchange='update()'> <option value='frumoasă'>frumoasă</option><option value='luminoasă'>luminoasă</option><option value='sclipitoare'>sclipitoare</option><option value='orbitoare'>orbitoare</option><option value='elegantă'>elegantă</option><option value='sclipitoare'>sclipitoare</option><option value='superbă'>superbă</option><option value='mare'>mare</option><option value='eleganţă'>eleganţă</option><option value='impozantă'>impozantă</option><option value='impresionantă'>impresionantă</option><option value='generoasă'>generoasă</option><option value='lucioasă'>lucioasă</option><option value='magnifică'>magnifică</option><option value='minunată'>minunată</option><option value='împodobită'>împodobită</option><option value='bogată'>bogată</option><option value='plină-de-noroi'>plină-de-noroi</option><option value='somptuosă'>somptuosă</option><option value='radiantă'>radiantă</option><option value='costisitoare'>costisitoare</option><option value='fabuloasă'>fabuloasă</option><option value='strălucitoare'>strălucitoare</option><option value='grandioasă'>grandioasă</option><option value='nebună'>nebună</option><option value='magnifică'>magnifică</option><option value='aur-solid'>aur-solid</option><option value='splendidă'>splendidă</option><option value='elegantă'>elegantă</option></select></div>", 'field_id': 'text', 'field_autofocus': True},
        {'view_id': 'html', 'html_template': "<div style='opacity: 0.5'><strong> Cerere de </strong></div>", 'field_id': 'text', 'field_autofocus': True},
        {'view_id': 'html', 'html_template': f"<div style='opacity: 0.5'><select name='îndurările' id='indurarile' onchange='update()'><option value='milă'>milă</option><option value='îndurare'>îndurare</option><option value='milostenie'>milostenie</option><option value='compasiune'>compasiune</option><option value='iertare'>iertare</option><option value='caritate'>caritate</option><option value='compătimire'>compătimire</option><option value='graţie'>graţie</option><option value='eleganţă'>eleganţă</option><option value='bunăvoinţă'>bunăvoinţă</option><option value='farmec'>farmec</option><option value='iertare'>iertare</option><option value='compătimire'>compătimire</option><option value='toleranţă'>toleranţă</option><option value='răbdare'>răbdare</option><option value='abţinere'>abţinere</option><option value='clemenţă'>clemenţă</option><option value='blândeţe'>blândeţe</option><option value='indulgenţă'>indulgenţă</option><option value='îngăduinţă'>îngăduinţă</option> </select></div>", 'field_id': 'text', 'field_autofocus': True}
     ]

    return {
        'dataset': dataset,
        'stream': stream,
        'view_id': 'blocks',
        'config': {
            'blocks': blocks,
            'javascript': custom_js,
        }
    }

Note the update callback in the HTML definition, the add_default_options python function for prepopulating the target fields with defaults and the loading of the custom.js file to custom_js string, which is then passed as value to the javascript key of config in the return statement.

The entire custom.js would look like this

function update() {
    // Update the default values of `selected_complimentele` & `selected_indurarile` keys with the selected options
    
    const complimenteleElement = document.getElementById("complimentele");
    const indurarileElement = document.getElementById("indurarile");

    if (complimenteleElement && indurarileElement) {
        const complimentele = complimenteleElement.value;
        const indurarile = indurarileElement.value;
        
        prodigy.update({
            selected_complimentele: complimentele,
            selected_indurarile: indurarile
        });
    } else {
        if (!complimenteleElement) {
            console.error('Element with id "complimentele" not found.');
        }
        if (!indurarileElement) {
            console.error('Element with id "indurarile" not found.');
        }
    }
}

function reset(elementId, firstOption) {
    const element = document.getElementById(elementId);
    if (element) {
        element.value = firstOption;
    } else {
        console.error(`Element with id "${elementId}" not found.`);
    }
}
document.addEventListener('prodigyanswer', (event) => {
    reset("complimentele", "frumoasă");
    reset("indurarile", "milă");
});

This should result in the options stored like this in the DB:

{
"image": ...
"text": "373",
  "meta": {
    "file": "373.jpeg"
  },
  "path": "../images/373.jpeg",
  "selected_complimentele": "frumoasă",
  "selected_indurarile": "milă",
  "_input_hash": 1363510692,
  "_task_hash": 1656897643,
  "_view_id": "blocks",
  "answer": "accept",
  "_timestamp": 1717074014,
  "_annotator_id": "2024-05-30_14-59-49",
  "_session_id": "2024-05-30_14-59-49"
}