DashedSentence in a PennController trial

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

Viewing 15 posts - 16 through 30 (of 44 total)
  • Author
    Posts
  • #5436
    mrhelfrich
    Participant

    Hello,

    I am trying to adapt the code from some of the replies in this thread for an experiment but I don’t have any coding experience.

    I am currently using the code in response #5236 to display sentences but I do not want to have the punctuation marks be displayed like that code specifically asks for. I tried using the original code in the opening post but when I use that code it doesn’t work with the Template command mentioned in response #5252.

    For clarity the important part of my code is this:

    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);
    }

    I would like it function like the original code in the first post, i.e. not displaying punctuation marks outside of the masked words.

    While I suspect I will have many more questions, one other related thing that I would like to do if possible would be to make the masking that appears (the series of underscores) appear to be the same length for all words so that a participant cannot look at the entirely masked sentence and guess how long each word is. The experiment I am setting up will use two sentence passages that are up to 30 words in length and it is very obvious currently where words like "a" and "to" are.

    Example: instead of a sentence that says "A local man is in a great amount of debt due to gambling." appearing as "_ _____ ___ __ __ _ _____ ______ __ ____ ___ __ _________" it would appear uniform until the words were being unmasked such as "_____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____"

    Thank you very much for your time as well as this very useful tool.
    Max

    #5437
    mrhelfrich
    Participant

    I’m not sure why but I’m unable to edit my original post and it looks like the copied code has some formatting problem so here is the code copied again:

    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);
    }
    #5439
    Jeremy
    Keymaster

    Hello Max,

    The special characters in forums posts mess with some characters in javascript code, it’s really frustrating I must say. I tried editing your last message so it displays the javascript code correctly.

    In order to make all words appear the same length when masked, would you be willing to use the longest-word’s length for all of them? Because that would make life easier:

    showWord = (s,i,m) => '<p>'+s.map( (w,n) =>
                `<span style='display: inline-block; text-align: center; width: ${m}ch; 
                ${(i===n?'':'border-bottom:solid 1px black;\'><span style=\'visibility:hidden;\'')}'>
                ${w}${(i===n?"":'</span>')}</span>`
            ).join(' ')+'</p>'
            
    dashed = (name, sentence) => {
        let words = sentence.split(' '), maxwidth = 0;
        words.map(w=>maxwidth=Math.max(w.length,maxwidth));
        return [
            [newText(name, showWord(words,null,maxwidth)).print()], 
            ...words.map( (w,i) => [newKey(`${name}-${i}-${w}`," ").log().wait() , getText(name).text(showWord(words,i,maxwidth))] ),
            [newKey(`${name}-last`," ").log().wait()]
        ].flat(1);
    }
    
    newTrial(
        ...dashed("test","A local man is in a great amount of debt due to gambling.")
        ,
        newButton("Finish").print().wait()
    )

    Jeremy

    #5440
    mrhelfrich
    Participant

    Thank you for such a fast response,

    Looking at the potential items we plan on using, using the longest word length would possibly be a problem. The longest string we may need to display is 30 words long and the largest word allowed would be 12 characters. Testing the code you just posted, 30 blanks at 12 characters each causes the blanks to wrap across the display. I assume there is a way to clean it up but since I will not have control over the size of the screen participants use to complete the task I think it would be best to limit the amount of characters masked.

    If it is too much effort to have the masks be a uniform size despite how large the actual word is that the original way (underscores match character length) is probably fine.

    Thanks again for the quick reply.

    #5441
    Jeremy
    Keymaster

    I don’t know that it’s too much effort, it’s more that there are decisions to be made. Using the longest word length is just the foolproof method, as it makes sure that all the blanks will definitely have the same width. You can try using a shorter constant width, but the question becomes “what to do with longer words?”

    No matter how you fine-tune this parameter though, as you say you won’t have control over the page resolution when your participants take your experiment, so there’s no way to guarantee 100% that no wrap will be introduced.

    Jeremy

    #5447
    mrhelfrich
    Participant

    The stimuli I will be using have set limitations of 30 words total and 12 characters per word (not including punctuation marks). I think having each blank be a set length, possibly 4 or 5 underscores long, would allow all of the information to appear on the page relatively cleanly (29 blanks x 4 underscores, one word <13 characters, 29 spaces, 2 punctuation marks would be 159 characters). The one concern with having the blanks be potentially shorter than the words is when a word gets displayed the entire string will shift around to display the characters which might be disorienting for a participant to follow along with. I’ll need to think about that and weigh that against being able to predict the length of a word before it gets displayed.

    I’ll probably have more questions for you tomorrow but I’ll see what I can do in the mean time with the code you have provided. Thanks again for all of the help, I really appreciate it.
    Max

    #5466
    snederve
    Participant

    Hi Jeremy,

    Thanks for posting this! I’m working with the native Ibex calling in a CSV from elsewhere. Getting the sentences works great, but I have one problem:
    I would like to include comprehension questions that relate to the sentence the participants just saw. In your message, the comprehension questions are independent, so they are randomized as separate elements. I can’t seem to figure out how I can stay within the same environment and have the comprehension questions show up.

    So I copied your code, and I only randomize the item “Sentence” (a column label in my csv). So I understand why the questions don’t show up. However, if I add “comprehension” to the shuffle sequence, they just show up randomly and are not associated with the previous sentence. Do you know how I can make the comprehension questions correspond to the previous sentence? Should I call in a specific column from a row?

    PennController.ResetPrefix(null)
    // Sequence of randomly picked pairs of test + control trials
    PennController.Sequence( "practice" , rshuffle("Sentence") )
    // This is the standard way of defining items in Ibex
    var items = [
        ["practice", "DashedSentence", {s: "This is a practice item so that you know what the task looks like"}]
    ]
    
     PennController.Template( "CSV_Template.csv", 
      row => ["Sentence", "DashedSentence", {s: row.Sentence}] 
     )
     
     PennController.Template( "CSV_Template.csv",  // So I'm sure I shouldn't put this here, but I can't make it work as part of the PennController.Template() above. CSV column labels correspond with code.
        row => PennController( "comprehension" ,
        newText("CompQ", row.CompQ)
           .print()
        ,
        newText("Negative feedback", "Not correct!")
       ,
       // This is a dirty javascript trick to randomize the answers
       newScale("result", ...[row.Results1, row.Results2].sort(()=>0.5-Math.random()) )
           .labelsPosition("right")
            .print()
            // We wait until the participant selects the right answer
           .wait(
             getScale("result").test.selected(row.Results1)
                    .failure( getText("Negative feedback").print() )
            )
      )
    )

    Again, thanks a lot for building this. I’m happy to do SPRs in PC Ibex and have all my experiments done on the same platform.

    Thanks.
    Sander

    #5504
    Jeremy
    Keymaster

    Hi Sander,

    If you go back to my first message, you will see that I edited it after introducing the Controller element in PennController 1.7. So you should probably try this:

    Sequence( "practice" , rshuffle("trial") )
    
    newTrial("practice",
      newController("DashedSentence", {s: "This is a practice item so that you know what the task looks like"})
        .print()
        .wait()
    )
    
    Template( "CSV_Template.csv", row =>
      newTrial( "trial" ,
         newController("DashedSentence", {s: row.Sentence})
            .print()
            .wait()
            .remove()
         ,
         newText("CompQ", row.CompQ)
           .print()
         ,
         newText("Negative feedback", "Not correct!")
        ,
        // This is a dirty javascript trick to randomize the answers
        newScale("result", ...[row.Results1, row.Results2].sort(()=>0.5-Math.random()) )
            .labelsPosition("right")
            .print()
            // We wait until the participant selects the right answer
            .wait(
              getScale("result").test.selected(row.Results1)
                    .failure( getText("Negative feedback").print() )
            )
      )
    )

    Jeremy

    #5505
    snederve
    Participant

    Amazing, it works perfectly! Thanks a lot!

    #5525
    snederve
    Participant

    Hi!
    Just one more question: when the results come out, I do not get the reading time for each word, but only the total time for one item. Is there a way to record the time per word that you know of? Or can I only do this with the native ibex script (not not with an external CSV file? Thanks 🙂
    Sander

    #5535
    snederve
    Participant

    I fixed it. The .log(“all”) did the trick.

    #5540
    mrhelfrich
    Participant

    Hello again,

    I am trying to learn something about the code that has been posted here and would like to try and understand how the code you have previously posted that forces special characters to always appear outside of the masks works so that I can remove it or make changes to it in the future. I have two pieces of code that may be useful for my experiment except they have they don’t mask special characters and I have no idea why. The first one should be the same as the code posted previously:

    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);
    }

    (Sorry about the formatting, whenever I try using the tilde key to put things into code format it just makes it worse and it appears I have a limited time to edit my replies before the system locks me out)

    The other piece of code I ended up changing was previously for changing the masking character from underscores to hyphens. I was seeing if I could make no mask appear at all, so that participants wouldn’t be able to guess at how long each word was until it appeared on screen but whatever I did ended up breaking everything in a way that could potentially be useful for me in the future since it makes only one word appear on screen at a time, like in a word-by-word style experiment. However it still has the same issue of not masking special characters which looks odd.

    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>’

    dashed = (name, sentence) => {
    let words = sentence.split(‘ ‘), maxwidth = 0;
    words.map(w=>maxwidth=Math.max(w.length,maxwidth));
    return [
    [newText(name, showWord(words,null,maxwidth)).print()],
    …words.map( (w,i) => [newKey(${name}-${i}-${w},” “).log().wait() , getText(name).text(showWord(words,i,maxwidth))] ),
    [newKey(${name}-last,” “).log().wait()]
    ].flat(1);
    }

    I would like to at least understand what portion of these two pieces of code causes the special characters to be ignored by the masking.

    Thanks again for all of your help, I really appreciate it,
    Max

    #5543
    Jeremy
    Keymaster

    Hello Max,

    Keep in mind that the original Ibex DashedSentence controller already comes with a mode that lets you show one word at a time. See the Ibex documentation.

    I apologize for the functions, I wrote them to be as compact as possible, but it’s not great for readability. Let me rewrite them more clearly:

    showWord = (arrayOfWords,showIndex) => "<p style='font-family: monospace;'>" + 
      arrayOfWords.map( (word,n) => {
        const letters = word.replace(/^\s*(\w+).*$/,"$1");  // this only keeps letters (and numbers)
        const punctuation = word.replace(/^\s*\w+/,"");  // this only keeps the punctuation characters
        if (n==showIndex) return "<span>"+letters+"</span>"+punctuation;
        else return "<span style='visibility: hidden;'>"+letters+"</span>"+punctuation;
      } ).join(" ") + "</p>";
    dashed = (name, sentence) => {
        const words = sentence.split(" ");  // Use space to break string into array
        return [  // Return an array of key.wait + text.print commands
            [ newText(name,showWord(words)).print() ], // First reveal no word
            ...words.map( (word,index) => [
                newKey(name+"-"+index+"-"+word," ").log().wait()
                , 
                getText(name).text( showWord(words,index) ) // reveal INDEXth word
            ]),
            [ newKey(name+"-last"," ").log().wait() ]
        ].flat(1);
    }

    As you can see, it’s the regular experessions in showWord that take care of separating the punctuation characters from the rest.

    Jeremy

    #5604
    mrhelfrich
    Participant

    Hello again,

    I’ve got another question about the code you have written to display words one at a time behind an underscore mask. The items I plan on displaying have various lengths and therefore will probably wrap at least once on a participant’s screen, especially since we don’t know what resolution they will have. To try and make the text wrapping uniform across participants, I was hoping to restrict the area where the text can display to a set size in the hopes that the text would always wrap at the same position for all participants.

    My attempted solution was to use a canvas element to set the dimensions for where the text is displayed, but since I know next to nothing about the code you have written, adding the dashed function as you have written it doesn’t work properly within a canvas.

    Here’s a simplified version of the code I was trying.

    PennController.ResetPrefix(null); 
    showWord = (arrayOfWords,showIndex) => "<p>" + 
      arrayOfWords.map( (word,n) => {
        const letters = word;  
        if (n==showIndex) return "<span>"+letters+"</span>";
        else return "<span style=\'border-bottom:solid 1px black;\'><span style='visibility: hidden;'>"+letters+"</span></span>";
      } ).join(" ") + "</p>"; 
    
    dashed = (name, sentence) => {
        const words = sentence.split(" ");  
        return [  
            [ newText(name,showWord(words)).print() ], 
            ...words.map( (word,index) => [
                newKey(name+"-"+index+"-"+word," ").log().wait(), 
                getText(name).text( showWord(words,index) ) 
            ]),
            [ newKey(name+"-last"," ").log().wait() ]
        ].flat(1);
    }
    newTrial("experiment",
        newCanvas("ItemDisp",700,300)
            .css("border","solid 1px black").css("font-size","24px") 
            .add(0,0,...dashed("SampleItem","Test item to verify that the text wraps within the border, doesn't break during a word, still looks readable." +
            "The actual border is temporary and will not appear during the actual experiment so text that touches it is fine.")),
        
        getCanvas("ItemDisp")
            .print().wait()
    )

    If using a canvas doesn’t work, is there some way to modify the dashed function to limit the length of the displayed text before it wraps? Right now it wraps when it runs out of space in the window so that will vary between displays and window sizes.

    The basic effect I am trying to end up with is this:

    newTrial("experiment",
        newCanvas("ItemDisp",700,300)
            .css("border","solid 1px black").css("font-size","24px")
            .add(0,0,newText("SampleItem","This text needs to be masked and displayed one " +
            "word at a time like it is when using the dashed function.  Also the black border " +
            "is just temporary to see what the dimensions are, it will be removed after testing.")),
        
        getCanvas("ItemDisp")
            .print().wait()
    )

    Thanks again for all of your help,
    Max

    #5605
    Jeremy
    Keymaster

    Hi Max,

    One solution to control text wrapping would be to manually insert linebreaks in the text, which was (one of) the original goal(s) when I posted this code.

    Assuming you will always name your element SampleItem across all your trials, another solution would be to add something like this to a file PennController.css in Aesthetics:

    .SampleItem {
      max-width: 40vw;
    }

    Or you could modify the code of the function to directly play with the Text element’s aesthetics there:

            [ newText(name,showWord(words)).css("max-width","40vw").print() ], 

    Jeremy

Viewing 15 posts - 16 through 30 (of 44 total)
  • You must be logged in to reply to this topic.