itemprop="text">
I have a small shiny app
            for annotating text
            files.
- The UI provides
            fileInput to select .txt files. One of
            the files is the default when the app is launched.
             
Next,
            Previous buttons allow user to display the contents of the
            file, one sentence at a time. 
- User may select any text
            within a sentence and click the Add Markup button to annotate
            the sentence. The Action Button triggers javascript function
            addMarkup().  
- The sentence is
            displayed after being marked
            up. 
I am only
            posting the shiny app code here. Complete code of the app is available on             href="https://github.com/beyond2013/dynamicContent" rel="nofollow noreferrer">github
            repository
            library(shiny)
 ui <- fluidPage(
            tags$head(tags$script(src="textselection.js")),
 titlePanel("Corpus Annotation
            Utility"),
 sidebarLayout(
 sidebarPanel(
            fileInput('fileInput', 'Select Corpus', accept = c('text', 'text','.txt')),
            actionButton("Previous", "Previous"),
 actionButton("Next",
            "Next"),
 actionButton("mark", "Add Markup")
 ),
            mainPanel(
 tags$h1("Sentence: "),
            htmlOutput("sentence"),
 tags$h1("Sentence marked up: "),
            htmlOutput("sentenceMarkedUp") 
 )
            )
)
server <- function(input, output) {
            sourceData <- reactive({
 corpusFile <- input$fileInput
            if(is.null(corpusFile)){
 return(readCorpus('data/news.txt'))
            }
 readCorpus(corpusFile$datapath)
            })
 corpus <- reactive({sourceData()}) 
            values <- reactiveValues(current = 1)
 observeEvent(input$Next,{
            if(values$current >=1 & values$current < length(corpus())){
            values$current <- values$current + 1
 }
 })
            observeEvent(input$Previous,{
 if(values$current > 1 & values$current
            <= length(corpus())){
 values$current <- values$current -
            1
 }
 })
 output$sentence <-
            renderText(corpus()[values$current])
}
shinyApp(ui = ui, server =
            server)
            
readCorpus()
            function looks like
            this:
readCorpus <-
            function(pathToFile){
 con <- file(pathToFile) 
            sentences <- readLines(con, encoding = "UTF-8")
 close(con)
            return(sentences)
}
My
            question is how can I persist the sentences to a file after they have been
            annotated? 
            href="https://i.stack.imgur.com/2hfTF.png" rel="nofollow noreferrer">
            src="https://i.stack.imgur.com/2hfTF.png" alt="screenshot of the
            app">
Update:
I
            have gone through             href="https://shiny.rstudio.com/articles/persistent-data-storage.html" rel="nofollow
            noreferrer">Persistent data storage in Shiny apps, and hope that I will be
            able to follow along the documentation regarding persistent storage. However I am still
            unsure how to capture the sentence after it has been marked up. 
              You have two issues here - persisting the
            changes, and then saving the output. I solved the problem using a bit of JS and a bit of
            R code. I'll do a pull request on Github to submit the broader code. However, here's the
            core of it.
In your Javascript that you use to
            select things, you can use Shiny.onInputChange() to update an
            element of the input vector. Doing this, you can create a
            reactiveValues item for the corpus, and then update it with
            inputs from your interface.
Below, you'll notice
            that I switched from using a textnode to using just the inner HTML. Using a node, and
            firstChild, as you had it before, you end up truncating the
            sentence after the first annotation (since it only picks the stuff before
            . Doing it this way seems to work
            better.
window.onload =
            function(){
 document.getElementById('mark').addEventListener('click',
            addMarkup);
}
function addMarkup(){
 var
            sentence = document.getElementById("sentence").innerHTML,
            selection="";
 if(window.getSelection){
 selection =
            window.getSelection().toString();
 }
 else
            if(document.selection && document.selection.type != "Control"){
            selection = document.selection.createRange().text;
 }
            if(selection.length === 0){
 return;
 }
 marked =
            "".concat(selection).concat("");
 result =
            sentence.replace(selection, marked);
            document.getElementById("sentence").innerHTML = result;
            Shiny.onInputChange("textresult",result);
}
Next,
            I've tried to simplify your server.R code. You were using a
            reactive context to pull from another reactive context
            (sourceData into corpus), which seemed
            unnecessary. So, I tried to refactor it a
            bit.
library(shiny)
source("MyUtils.R")
ui
            <- fluidPage(
 tags$head(tags$script(src="textselection.js")),
            titlePanel("Corpus Annotation Utility"),
 sidebarLayout(
            sidebarPanel(
 fileInput('fileInput', 'Select Corpus', accept = c('text',
            'text','.txt')),
 actionButton("Previous", "Previous"),
            actionButton("Next", "Next"),
 actionButton("mark", "Add Markup"),
            downloadButton(outputId = "save",label = "Download")),
 mainPanel(
            tags$h1("Sentence: "),
 htmlOutput("sentence"))
            )
)
server <- function(input, output) {
            corpus <- reactive({
 corpusFile <- input$fileInput
            if(is.null(corpusFile)) {
 return(readCorpus('data/news.txt'))
 }
            else {
 return(readCorpus(corpusFile$datapath))
            }
 })
 values <- reactiveValues(current =
            1)
 observe({
 values$corpus <- corpus()
 })
            output$sentence <- renderText(values$corpus[values$current])
            observeEvent(input$Next,{
 if(values$current >=1 &
            values$current < length(corpus())) {
 values$current <- values$current +
            1
 }
 })
 observeEvent(input$Previous,{
            if(values$current > 1 & values$current <= length(corpus())) {
            values$current <- values$current - 1
 }
 })
            observeEvent(input$mark,{
 values$corpus[values$current] <-
            input$textresult
 })
 output$save <- downloadHandler(filename =
            "marked_corpus.txt",
 content = function(file) {
            writeLines(text = values$corpus,
 con = file,
 sep =
            "\n")
            })
}
Now,
            the code has a few changes. The loading from file is basically the same. I was right
            about my skepticism on isolate - replacing it with an
            observe accomplishes what I wanted to do, whereas
            isolate would only give you the initial load. Anyway, we use
            observe to load the corpus values into the
            reactiveValues object you created - this is to give us a place
            to propagate changes to the data.
We keep the
            remaining logic for moving forward and backward. However, we change the way the output
            is rendered so that it looks at the reactiveValues object.
            Then, we create an observer that updates the reactiveValues
            object with the input from our updated Javascript. When this happens, the data gets
            stored permanently, and you can also mark more than one sequence in the string (though I
            have not done anything with nested marking or with removing marks). Finally, a save
            function is added - the resulting strings are saved out with
             used to show the marked
            areas.
If you load a previously marked file, the
            marks will show up again.
  
No comments:
Post a Comment