DashedSentence in a PennController trial

PennController for IBEX Forums FAQ / Tips DashedSentence in a PennController trial

This topic contains 31 replies, has 8 voices, and was last updated by Jeremy Jeremy 3 weeks, 1 day ago.

Viewing 15 posts - 1 through 15 (of 32 total)
  • Author
    Posts
  • #4220
    Jeremy
    Jeremy
    Keymaster

    EDIT: PennController includes Controller elements starting with version 1.7. The preferred method now is:

    Example:

    newTrial(
      newText("instructions", "Click on the button below to start reading")
        .print()
      ,
      newButton("Start reading")
        .print()
        .wait()
        .remove()
      ,
      getText("instructions")
        .remove()
      ,
      newController("DashedSentence", {s: "The mouse that the cat is chasing runs fast"})
        .print()
        .log()
        .wait()
        .remove()
      ,
      newButton("I'm done")
        .print()
        .wait()
    )

    Option 2 below is deprecated, unless you have specific needs that cannot be addressed by the native-Ibex DashedSentence controller.

    END OF EDIT

    I am working on a general solution to inject native-Ibex-format controllers inside PennController trials.

    In the meantime, this page gives you two options to present self-paced reading tasks along PennController trials.

    Option 1: use the native-Ibex DashedSentence controller

    Pros: you get all the features of the native controller
    Cons: you don’t get the features of PennController, ie. no .log command to store metadata from your table’s columns

    Example:

    // This line is required for PennController experiments
    PennController.ResetPrefix(null)
    
    // Sequence of randomly picked pairs of test + control trials
    PennController.Sequence( "practice" , rshuffle("test", "control") )
    
    // This is the standard way of defining items in Ibex
    var items = [
        ["practice", "DashedSentence", {s: "This is a practice item"}]
    ]
    
    // No need to use these if uploading CSV tables to Resources/chunks_includes
    PennController.AddTable( "testTable" , `Sentence
    Hello world
    Bye world
    "Yes, or, no"`) // Note the quotes when including commas
    
    PennController.AddTable( "controlTable" , `Equation,Result1,Result2,Result3
    7*7,49,53,32
    489+256,745,667,233
    81/9,9,7,8`)
    
    
    // Here we use Template to generate trials from testTable
    // Note that we use the Ibex-native DashedSentence controller
    PennController.Template( "testTable" ,
      row => ["test", "DashedSentence", {s: row.Sentence}]
    )
    
    
    // Here we use Template to generate trials from controlTable
    // Now we use a PennController trial
    PennController.Template( "controlTable" ,
      row => PennController( "control" ,
        newText("What is the result of "+row.Equation+"?")
            .print()
        ,
        newText("Negative feedback", "Are you sure?")
        ,
        // This is a dirty javascript trick to randomize the answers
        newScale("result", ...[row.Result1, row.Result2, row.Result3].sort(()=>0.5-Math.random()) )
            .settings.labelsPosition("right")
            .print()
            // We wait until the participant selects the right answer
            .wait(
                getScale("result").test.selected(row.Result1)
                    .failure( getText("Negative feedback").print() )
            )
      )
    )

    Option 2: code the task’s logic yourself
    EDIT: this method is deprecated since PennController 1.7—

    Pros: you get all the features and flexibility of PennContrller
    Cons: you have to recode everything from scratch…

    Example reproducing the default behavior of DashedSentence (self-paced reading mode and dashed display)

    // This function generates an array of PennController commands
    // It's basically a sequence of text.settings.text() and key.wait()
    // The first parameter is the sentence to display (words should be separated by space characters)
    // The second (optional) parameter determines whether the sentence should be removed at the end
    //
    dashed = (sentence, remove) => {
      let words = sentence.split(' '),  blanks = words.map(w=>w.split('').map(c=>'_').join(''));
      let textName = 'dashed'+words.join('');
      // We'll return cmds: the first command consists in creating (and printing) a Text element with dashes
      let cmds = [ newText(textName, blanks.join(' ')).print() ];
      // We'll go through each word, and add two command blocks per word
      for (let i = 0; i < = words.length; i++)
        cmds = cmds.concat([ newKey('dashed'+i+words[i], " ").settings.log().wait() , // Wait for (and log) a press on Space
                             getText(textName).settings.text(blanks.map((w,n)=>(n==i?words[n]:w)).join(' ')) ]); // Show word
      if (remove)  // Remove the text after the last key.wait() is parameter specified
        cmds.push(getText(textName).remove());
      return cmds;
    }
    
    // Illustration
    //
    PennController(
      newText("instructions", "Click on the button below to start reading")
        .print()
      ,
      newButton("Start reading")
        .print()
        .wait()
        .remove()
      ,
      getText("instructions")
        .remove()
      ,
      // We call the function here
      // Note the ... before dashed because it returns an array, which we want to inject
      //
      ...dashed("The mouse that the cat is chasing runs fast", 'remove')
      ,
      newButton("I'm done")
        .print()
        .wait()
    )
    • This topic was modified 5 months, 2 weeks ago by Jeremy Jeremy. Reason: Added an example using Ibex-native DashedSentence
    #4885
    Avatar
    daniela
    Participant

    Hi Jeremy,

    I’m having trouble implementing newController() inside a template. Here’s what I’ve got (I just added newController() and its commands to an already functioning script):

    PennController.Template( PennController.GetTable(“fictional.csv”),
    variable => [“critical”,
    newController(“DashedSentence”, {s: [variable.bio], display: “in place”,blankText: “…”})
    .print()
    .log()
    .wait()
    .remove()
    ,
    newController(“DashedSentence”, {s: [variable.pronoun,variable.verb,variable.adj,variable.objnp,variable.time,variable.spillover]})
    .print()
    .log()
    .wait()
    .remove()
    )

    I get the following error when I try to run it: Command ‘length’ unknown on Controller element ‘DashedSentence’. (PennController: 22). I’ve updated to PC 1.7, and have also tried to figure it out in the PC trial tester with no luck.

    Did I do something wrong, or does newController() not work inside a template (or both)?

    Best,
    Daniela

    • This reply was modified 3 months, 4 weeks ago by Avatar daniela.
    • This reply was modified 3 months, 4 weeks ago by Avatar daniela.
    #4888
    Avatar
    daniela
    Participant

    *Edit: I’m missing the closing ] bracket, but this was just a mistake in shortening it for the forum

    #4889
    Jeremy
    Jeremy
    Keymaster

    Hi Daniela,

    The Controller element, as such, is supposed to go inside a trial created with newTrial (previously PennController). In your script, you are trying to use it like a regular native-Ibex definition, which won’t work because your newController commands appear outside newTrial/PennController. If you want to use the old Ibex definition, just build on Option 1 in my first message. If you want to use the Controller element, build on the example I gave in my Edit instead.

    Also, I’m not sure what you’re trying to do with your arrays of variable. in the s parameter: what should happen to all those values?

    #5030
    Avatar
    daniela
    Participant

    Hi Jeremy,

    thanks for your reply. What I didn’t like about DashedSentence was the format of the results file output (it printed new column header labels for each trial–I know that would be fixed once I get my data into R, but I wanted to tidy things up for my test runs). I figured out newController based on your comment, but decided to just use Option 2 (recreated DashedSentence based on your code), and it worked great.

    I had arrays of variable in the s parameter because I wanted to present the sentence in chunks, rather than word-by-word (each variable contained one chunk). In case anybody else stumbles upon this thread wanting to do something similar, I was able to adapt your reproduction of DashedSentence (from Option 2) by changing let words = sentence.split(' ') to let words = sentence.split('*'). Then I added an asterisk (*) to my .csv file to indicate where the chunk boundaries should be.

    Here’s my adaptation (including setting the font to Courier):

    dashed = (sentence, remove) => {
        let words = sentence.split('*'),  blanks = words.map(w=>w.split('').map(c=>'_').join('') );
        let textName = 'dashed'+words.join('');
        // We'll return cmds: the first command consists in creating (and printing) a Text element with dashes
        let cmds = [ newText(textName, blanks.join(' ')).print() .settings.css("font-family","courier")]; // COURIER as font
        // We'll go through each word, and add two command blocks per word
        for (let i = 0; i <= words.length; i++)
        cmds = cmds.concat([ newKey('dashed'+i+words[i], " ").log().wait() , // Wait for (and log) a press on Space
        getText(textName).text(blanks.map((w,n)=>(n==i?words[n]:w)).join(' ')) ]); // Show word
        if (remove)  // Remove the text after the last key.wait() is parameter specified
        cmds.push(getText(textName).remove());
        return cmds;
    }

    and then later inside a PennController.Tempalte() I called on ‘dashed’:

    ...dashed(variable.critical, "remove")

    Worked great, thanks!

    Best,
    Daniela

    #5231
    Avatar
    aurnha
    Participant

    Hi Jeremy,

    I created an SPR experiment following the approach outlined in your example. The words are being displayed in-place.

    I realised that when the space bar is kept pressed, the all the words of the current trial are running through at max speed. It appears, the DashedSentence Controller doesn’t actually wait for a key release before registering the next keypress.

    Interestingly, this also clutters the result file with many repetitions of the data from the entire sentence, but each time starting at the beginning of the sentence again, not just repeating the current word. But this might only be a tangential issue.

    Is this a known problem and are you aware of any way of resolving this, e.g. by making key releases obligatory?

    Thanks a lot!
    Christoph

    #5232
    Jeremy
    Jeremy
    Keymaster

    Hi Christoph,

    Thank you for the feedback. Yes these are known problems:

    – I don’t know if it makes sense to modify the native-Ibex DashedSentence controller to prevent jumping to the next word by keeping the spacebar pressed down. Or maybe I could just add a parameter that you have to set, so as to maintain retro-compatibility

    – The cluttering of the results file will be fixed in the next release of PennController

    Jeremy

    #5235
    Avatar
    mschrumpf
    Participant

    Sorry for barging into this discussion. But is there any way to keep punctuation and line breaks visible at all times in a dashed sentence trial?
    Basically, an example text like this:

    Nico and Lotte fought together for a fair and equal world. Their collaboration proved to be extremely successful.
    Nico inspired Lotte because he always wanted to take immediate action when he saw injustice.

    Should turn into a dashed sentence text like this:

    —- — —– —— ——– — – —- — —– —–. —– ————- —— — — ——— ———-.
    —- — —– —— ——– — – —- — —– —–.

    I could not yet figure out how to put elements into the text which would be treated as exceptions by the dashed function – nor how to concatenate several dashed sentence and plain text elements on a single trial screen.
    Any help here would be greatly appreciated.
    Best regards,
    Matthias

    #5236
    Jeremy
    Jeremy
    Keymaster

    Hello Matthias,

    I don’t think the native-Ibex DashedSentence controller offers that option. However following option 2 from my first message, I came up with two short javascript functions that reasonably reproduce DashedSentence’s behavior:

    showWord = (s,i) => '<p>'+s.map((w,n)=>`
            <span${(i===n?"":' style=\'border-bottom:solid 1px black;\'><span style=\'visibility:hidden;\'')}'>
            ${w.replace(/^\s*(\w+).*$/,"$1")}${(i===n?"":'</span>')}</span>${w.replace(/^\s*\w+/,'')}`).join(' ')+'</p>'
    
    dashed = (name, sentence) => {
        let words = sentence.split(' ');
        return [
            [newText(name, showWord(words)).print()], 
            ...words.map( (w,i) => [newKey(`${name}-${i}-${w}`," ").log().wait() , getText(name).text(showWord(words,i))] ),
            [newKey(`${name}-last`," ").log().wait()]
        ].flat(1);
    }

    Once you’ve added that bit of code at the top of your script, you can then use it like this:

    newTrial(
        dashed("test", "Nico and Lotte fought together for a fair and equal world. Their collaboration proved to be extremely successful.
    Nico inspired Lotte because he always wanted to take immediate action when he saw injustice.") , getText("test").remove() , newButton("next").print().wait() )

    Just make sure you don’t insert a space before <br> otherwise it will be considered a word.

    As you can see, the script creates a Text element whose name you provide as the first argument of dashed (here, test) so you can play with that element later (I decided to take it off the screen once complete, using remove).

    You’ll get keypress events reported in your results file (along with a timestamp), with names like test-0-Nico, test-1-and, etc.

    Incidentally, it also take care of the key-release problem that Christoph reported.

    Let me know if you have any questions.

    Jeremy

    #5247
    Avatar
    mschrumpf
    Participant

    Hello Jeremy,
    thank you very much for your help. Your solution worked great. I just need to work out two more things.
    Firstly, how to replace each character of each word with a hyphen “-” instead of a blank space with a border on the bottom. I’m sorry this didn’t become clearer in my other post. It looked much clearer in the editor than in the finished post.
    Secondly, and perhaps more important, how to call the function in a template and supply the argument “sentence” from a row in a .csv file.
    Best regards,
    Matthias

    #5252
    Jeremy
    Jeremy
    Keymaster

    Hello,

    You can replace the first lines from the code above (ie. until dashed = ..., non-included) with:

    showWord = (s,i) => '<p style="font-family: monospace;">'+s.map((w,n)=>`
            <span>${w.replace(/^\s*(\w+).*$/,"$1").replace(/(.)/g,(i===n?"$1":"-"))}</span>${w.replace(/^\s*\w+/,'')}`).join(' ')+'</p>'

    I used a monospace font to clearly mark each dash, but feel free to change that if you don’t like the rendering.

    You can use the dashed function from within the Template command as usual:

    Template( "myTable.csv" , row =>
      newTrial(
        dashed("sentence" , row.sentence )
        ,
        getText("sentence").remove()
        ,
        newButton("Next").print().wait()
      )
    )

    Jeremy

    #5291
    Avatar
    matheus
    Participant

    Hello,
    I’ve been trying to use this in a similar way to what daniela did, which is present chunks instead of single words.
    However, I can’t get it to work. Where exactly should I put the code Jeremy wrote as option 2 (considering the changes daniela did to split the sentences not when there’s a space but a *)?
    Matheus

    #5292
    Jeremy
    Jeremy
    Keymaster

    Hello Matheus,

    You can copy the code defining the showWord and dashed functions from my message above, just replace " " with "*" in the line starting with ...words. Then use showWord as illustrated in that message, inserting a * instead of a space character in the text wherever you want to mark a chunk separation.

    Jeremy

    #5293
    Avatar
    matheus
    Participant

    Hello Jeremy,
    Where exactly should I paste this code? In the main.js file?
    I am a 0 when it comes to programming and all that, sorry.

    #5294
    Avatar
    matheus
    Participant

    I think I got it, now. There was a small mistake I was doing and couldn’t understand why it was not working, but it is now. Thank you very much, Jeremy!

Viewing 15 posts - 1 through 15 (of 32 total)

You must be logged in to reply to this topic.