Repository: deepset-ai/haystack
Pull request: Public demo
Tasks:

- [x] Create a document store with a different dataset (not GoT)
- [x] Disable file upload in the UI
- [x] Make the UI submit requests only when the button is clicked
- [x] Make the UI display a message when all workers are busy
- [x] Adjust eval mode
- [ ] Deploy the modified demo on a suitable EC instance
- [x] A line of text saying that the data comes from Wikipedia and a link to the license + timestamp of the crawled dataset

Problem statement:
Deploy public demo
Let's deploy a simple, public demo based on our docker-compose setup with elasticsearch + haystack API + streamlit UI. 

We should particularly :
- streamline the UI again (disable file upload, only submit requests when clicking on run button, adjusting eval mode...)
- display an info message if all workers are busy (i.e. too many users at the same time)

Hints from issue discussion:
Tasks:
- [ ] Create a document store with a different dataset (not GoT)
- [ ] Disable file upload in the UI
- [ ] Make the UI submit requests only when the button is clicked
- [ ] Make the UI display a message when all workers are busy
- [ ] Adjust eval mode ?
- [ ] Deploy the modified demo on a suitable EC instance

Commit message: Trigger the ci

Patch to review:
diff --git a/docs/_src/api/api/document_classifier.md b/docs/_src/api/api/document_classifier.md
index a55a1e2a1d..8e89683f75 100644
--- a/docs/_src/api/api/document_classifier.md
+++ b/docs/_src/api/api/document_classifier.md
@@ -2,7 +2,7 @@
 # Module base
 
 <a name="base.BaseDocumentClassifier"></a>
-## BaseDocumentClassifier Objects
+## BaseDocumentClassifier
 
 ```python
 class BaseDocumentClassifier(BaseComponent)
@@ -21,7 +21,7 @@ Wrapper method used to time functions.
 # Module transformers
 
 <a name="transformers.TransformersDocumentClassifier"></a>
-## TransformersDocumentClassifier Objects
+## TransformersDocumentClassifier
 
 ```python
 class TransformersDocumentClassifier(BaseDocumentClassifier)
diff --git a/docs/_src/api/api/evaluation.md b/docs/_src/api/api/evaluation.md
index fbaa084db6..0ccd2dc289 100644
--- a/docs/_src/api/api/evaluation.md
+++ b/docs/_src/api/api/evaluation.md
@@ -2,7 +2,7 @@
 # Module evaluator
 
 <a name="evaluator.EvalDocuments"></a>
-## EvalDocuments Objects
+## EvalDocuments
 
 ```python
 class EvalDocuments(BaseComponent)
@@ -48,7 +48,7 @@ Run this node on one sample and its labels
 Print the evaluation results
 
 <a name="evaluator.EvalAnswers"></a>
-## EvalAnswers Objects
+## EvalAnswers
 
 ```python
 class EvalAnswers(BaseComponent)
diff --git a/docs/_src/api/api/pipelines.md b/docs/_src/api/api/pipelines.md
index 3faa60f55f..b8f3269087 100644
--- a/docs/_src/api/api/pipelines.md
+++ b/docs/_src/api/api/pipelines.md
@@ -2,7 +2,7 @@
 # Module base
 
 <a name="base.RootNode"></a>
-## RootNode Objects
+## RootNode
 
 ```python
 class RootNode(BaseComponent)
@@ -11,7 +11,7 @@ class RootNode(BaseComponent)
 RootNode feeds inputs together with corresponding params to a Pipeline.
 
 <a name="base.BasePipeline"></a>
-## BasePipeline Objects
+## BasePipeline
 
 ```python
 class BasePipeline()
@@ -72,7 +72,7 @@ Here's a sample configuration:
                                      `_` sign must be used to specify nested hierarchical properties.
 
 <a name="base.Pipeline"></a>
-## Pipeline Objects
+## Pipeline
 
 ```python
 class Pipeline(BasePipeline)
@@ -268,7 +268,7 @@ Save a YAML configuration for the Pipeline that can be used with `Pipeline.load_
 - `return_defaults`: whether to output parameters that have the default values.
 
 <a name="base.RayPipeline"></a>
-## RayPipeline Objects
+## RayPipeline
 
 ```python
 class RayPipeline(Pipeline)
@@ -369,7 +369,7 @@ Here's a sample configuration:
 - `address`: The IP address for the Ray cluster. If set to None, a local Ray instance is started.
 
 <a name="base._RayDeploymentWrapper"></a>
-## \_RayDeploymentWrapper Objects
+## \_RayDeploymentWrapper
 
 ```python
 class _RayDeploymentWrapper()
@@ -410,7 +410,7 @@ Ray calls this method which is then re-directed to the corresponding component's
 # Module standard\_pipelines
 
 <a name="standard_pipelines.BaseStandardPipeline"></a>
-## BaseStandardPipeline Objects
+## BaseStandardPipeline
 
 ```python
 class BaseStandardPipeline(ABC)
@@ -482,7 +482,7 @@ Create a Graphviz visualization of the pipeline.
 - `path`: the path to save the image.
 
 <a name="standard_pipelines.ExtractiveQAPipeline"></a>
-## ExtractiveQAPipeline Objects
+## ExtractiveQAPipeline
 
 ```python
 class ExtractiveQAPipeline(BaseStandardPipeline)
@@ -521,7 +521,7 @@ Pipeline for Extractive Question Answering.
               by this method under the key "_debug"
 
 <a name="standard_pipelines.DocumentSearchPipeline"></a>
-## DocumentSearchPipeline Objects
+## DocumentSearchPipeline
 
 ```python
 class DocumentSearchPipeline(BaseStandardPipeline)
@@ -558,7 +558,7 @@ Pipeline for semantic document search.
       by this method under the key "_debug"
 
 <a name="standard_pipelines.GenerativeQAPipeline"></a>
-## GenerativeQAPipeline Objects
+## GenerativeQAPipeline
 
 ```python
 class GenerativeQAPipeline(BaseStandardPipeline)
@@ -597,7 +597,7 @@ Pipeline for Generative Question Answering.
       by this method under the key "_debug"
 
 <a name="standard_pipelines.SearchSummarizationPipeline"></a>
-## SearchSummarizationPipeline Objects
+## SearchSummarizationPipeline
 
 ```python
 class SearchSummarizationPipeline(BaseStandardPipeline)
@@ -639,7 +639,7 @@ Pipeline that retrieves documents for a query and then summarizes those document
       by this method under the key "_debug"
 
 <a name="standard_pipelines.FAQPipeline"></a>
-## FAQPipeline Objects
+## FAQPipeline
 
 ```python
 class FAQPipeline(BaseStandardPipeline)
@@ -676,7 +676,7 @@ Pipeline for finding similar FAQs using semantic document search.
       by this method under the key "_debug"
 
 <a name="standard_pipelines.TranslationWrapperPipeline"></a>
-## TranslationWrapperPipeline Objects
+## TranslationWrapperPipeline
 
 ```python
 class TranslationWrapperPipeline(BaseStandardPipeline)
@@ -702,7 +702,7 @@ Wrap a given `pipeline` with the `input_translator` and `output_translator`.
                  Note that pipelines with split or merge nodes are currently not supported.
 
 <a name="standard_pipelines.QuestionGenerationPipeline"></a>
-## QuestionGenerationPipeline Objects
+## QuestionGenerationPipeline
 
 ```python
 class QuestionGenerationPipeline(BaseStandardPipeline)
@@ -712,7 +712,7 @@ A simple pipeline that takes documents as input and generates
 questions that it thinks can be answered by the documents.
 
 <a name="standard_pipelines.RetrieverQuestionGenerationPipeline"></a>
-## RetrieverQuestionGenerationPipeline Objects
+## RetrieverQuestionGenerationPipeline
 
 ```python
 class RetrieverQuestionGenerationPipeline(BaseStandardPipeline)
@@ -722,7 +722,7 @@ A simple pipeline that takes a query as input, performs retrieval, and then gene
 questions that it thinks can be answered by the retrieved documents.
 
 <a name="standard_pipelines.QuestionAnswerGenerationPipeline"></a>
-## QuestionAnswerGenerationPipeline Objects
+## QuestionAnswerGenerationPipeline
 
 ```python
 class QuestionAnswerGenerationPipeline(BaseStandardPipeline)
@@ -732,7 +732,7 @@ This is a pipeline which takes a document as input, generates questions that the
 this document, and then performs question answering of this questions using that single document.
 
 <a name="standard_pipelines.MostSimilarDocumentsPipeline"></a>
-## MostSimilarDocumentsPipeline Objects
+## MostSimilarDocumentsPipeline
 
 ```python
 class MostSimilarDocumentsPipeline(BaseStandardPipeline)
diff --git a/docs/_src/api/api/query_classifier.md b/docs/_src/api/api/query_classifier.md
index d1ce774642..0e06db13cc 100644
--- a/docs/_src/api/api/query_classifier.md
+++ b/docs/_src/api/api/query_classifier.md
@@ -2,7 +2,7 @@
 # Module base
 
 <a name="base.BaseQueryClassifier"></a>
-## BaseQueryClassifier Objects
+## BaseQueryClassifier
 
 ```python
 class BaseQueryClassifier(BaseComponent)
@@ -14,7 +14,7 @@ Abstract class for Query Classifiers
 # Module sklearn
 
 <a name="sklearn.SklearnQueryClassifier"></a>
-## SklearnQueryClassifier Objects
+## SklearnQueryClassifier
 
 ```python
 class SklearnQueryClassifier(BaseQueryClassifier)
@@ -81,7 +81,7 @@ queries or statement vs question queries.
 # Module transformers
 
 <a name="transformers.TransformersQueryClassifier"></a>
-## TransformersQueryClassifier Objects
+## TransformersQueryClassifier
 
 ```python
 class TransformersQueryClassifier(BaseQueryClassifier)
diff --git a/docs/_src/api/api/question_generator.md b/docs/_src/api/api/question_generator.md
index d132c9a32a..4b3681709f 100644
--- a/docs/_src/api/api/question_generator.md
+++ b/docs/_src/api/api/question_generator.md
@@ -2,7 +2,7 @@
 # Module question\_generator
 
 <a name="question_generator.QuestionGenerator"></a>
-## QuestionGenerator Objects
+## QuestionGenerator
 
 ```python
 class QuestionGenerator(BaseComponent)
diff --git a/docs/_src/api/api/ranker.md b/docs/_src/api/api/ranker.md
index 33e2d0524f..6a84f1cdd1 100644
--- a/docs/_src/api/api/ranker.md
+++ b/docs/_src/api/api/ranker.md
@@ -2,7 +2,7 @@
 # Module base
 
 <a name="base.BaseRanker"></a>
-## BaseRanker Objects
+## BaseRanker
 
 ```python
 class BaseRanker(BaseComponent)
@@ -55,7 +55,7 @@ position in the ranking of documents the correct document is.
 # Module sentence\_transformers
 
 <a name="sentence_transformers.SentenceTransformersRanker"></a>
-## SentenceTransformersRanker Objects
+## SentenceTransformersRanker
 
 ```python
 class SentenceTransformersRanker(BaseRanker)
diff --git a/ui/eval_labels_example.csv b/ui/eval_labels_example.csv
index 65db88d557..fb4e0411e6 100644
--- a/ui/eval_labels_example.csv
+++ b/ui/eval_labels_example.csv
@@ -1,4 +1,15 @@
 "Question Text";"Answer"
-"Who is the father of Arya Stark?";"Ned Stark"
-"Who is the mother of Arya Stark?";"Catelyn Stark"
-"Who created the dothraki language?";"David J. Peterson"
\ No newline at end of file
+"Does the Danube cross Austria?";"Yes"
+"Where is the deepset lake in the world?";"Lake Bajkal"
+"What is the name of the highest mountain of Chile?";"Ojos del Salado"
+"What is the largest city of Camerun?";"Douala"
+"In which country is the highest airport in the world?";"China"
+"How many national parks are in Sweden?";"30"
+"How many regions has France?";"18"
+"Which country is the biggest producer of peanuts in 2021?";"China"
+"How many barrels of oil did Noway export in 2019?";"1.7 million barrels a day"
+"Which countries surround the Caspian Sea?";"Kazakhstan, Iran, Azerbaijan, Russia and Turkmenistan"
+"In which continent is the Gobi desert?";"Asia"
+"What is Bolivia's official capital city?";"Sucre"
+"What is the most remote island in the world?";"Bouvet Island"
+"How do you become a citizen of Vatican City?";"By ius officii"
\ No newline at end of file
diff --git a/ui/requirements.txt b/ui/requirements.txt
index 14d9d9fa3c..76c407b5ca 100644
--- a/ui/requirements.txt
+++ b/ui/requirements.txt
@@ -1,2 +1,2 @@
-streamlit>=0.84.0
-st-annotated-text==1.1.0
+streamlit>=1.2.0
+st-annotated-text==2.0.0
diff --git a/ui/utils.py b/ui/utils.py
index 7cc18ad473..ff8391e6f5 100644
--- a/ui/utils.py
+++ b/ui/utils.py
@@ -21,7 +21,6 @@ def haystack_is_ready():
     return False
 
 
-@st.cache(show_spinner=False)
 def retrieve_doc(query, filters=None, top_k_reader=5, top_k_retriever=5):
     # Query Haystack API
     url = f"{API_ENDPOINT}/{DOC_REQUEST}"
@@ -31,6 +30,10 @@ def retrieve_doc(query, filters=None, top_k_reader=5, top_k_retriever=5):
 
     # Format response
     result = []
+
+    if "errors" in response_raw:
+        raise Exception(", ".join(response_raw["errors"]))
+        
     answers = response_raw["answers"]
     for i in range(len(answers)):
         answer = answers[i]
diff --git a/ui/webapp.py b/ui/webapp.py
index 68e16f3f74..0cb60531e6 100644
--- a/ui/webapp.py
+++ b/ui/webapp.py
@@ -3,6 +3,7 @@
 
 import logging
 import pandas as pd
+from pathlib import Path
 import streamlit as st
 from annotated_text import annotated_text
 
@@ -12,175 +13,197 @@
 import SessionState
 from utils import feedback_doc, haystack_is_ready, retrieve_doc, upload_doc
 
+
 # Adjust to a question that you would like users to see in the search bar when they load the UI:
 DEFAULT_QUESTION_AT_STARTUP = "Who is the father of Arya Stark?"
 
+# Labels for the evaluation
+EVAL_LABELS = os.getenv("EVAL_FILE", Path(__file__).parent / "eval_labels_example.csv")
 
-def annotate_answer(answer, context):
-    """ If we are using an extractive QA pipeline, we'll get answers
-    from the API that we highlight in the given context"""
-    start_idx = context.find(answer)
-    end_idx = start_idx + len(answer)
-    # calculate dynamic height depending on context length
-    height = int(len(context) * 0.50) + 5
-    annotated_text(context[:start_idx], (answer, "ANSWER", "#8ef"), context[end_idx:], height=height)
-
-
-def show_plain_documents(text):
-    """ If we are using a plain document search pipeline, i.e. only retriever, we'll get plain documents
-    from the API that we just show without any highlighting"""
-    st.markdown(text)
-
-
-def random_questions(df):
-    """
-    Helper to get one random question + gold random_answer from the user's CSV 'eval_labels_example'.
-    This can then be shown in the UI when the evaluation mode is selected. Users can easily give feedback on the
-    model's results and "enrich" the eval dataset with more acceptable labels
-    """
-    random_row = df.sample(1)
-    random_question = random_row["Question Text"].values[0]
-    random_answer = random_row["Answer"].values[0]
-    return random_question, random_answer
+# Whether the file upload should be enabled or not
+DISABLE_FILE_UPLOAD = os.getenv("HAYSTACK_UI_DISABLE_FILE_UPLOAD")
 
 
 def main():
-    # Define state
-    state_question = SessionState.get(
-        random_question=DEFAULT_QUESTION_AT_STARTUP, random_answer="", next_question="false", run_query="false"
+
+    # Persistent state
+    state = SessionState.get(
+        random_question=DEFAULT_QUESTION_AT_STARTUP, 
+        random_answer="",
+        results=None,
+        raw_json=None,
+        get_next_question=True
     )
 
-    # Initialize variables
-    eval_mode = False
-    random_question = DEFAULT_QUESTION_AT_STARTUP
-    eval_labels = os.getenv("EVAL_FILE", "eval_labels_example.csv")
+    # Small callback to reset the interface in case the text of the question changes
+    def reset_results(*args):
+        state.results = None
+        state.raw_json = None
+
+    # Title
+    st.write("# Countries of the World")
 
-    # UI search bar and sidebar
-    st.write("# Haystack Demo")
+    # Sidebar
     st.sidebar.header("Options")
     top_k_reader = st.sidebar.slider("Max. number of answers", min_value=1, max_value=10, value=3, step=1)
-    top_k_retriever = st.sidebar.slider(
-        "Max. number of documents from retriever", min_value=1, max_value=10, value=3, step=1
-    )
+    top_k_retriever = st.sidebar.slider("Max. number of documents from retriever", min_value=1, max_value=10, value=3, step=1)
     eval_mode = st.sidebar.checkbox("Evaluation mode")
     debug = st.sidebar.checkbox("Show debug info")
 
-    st.sidebar.write("## File Upload:")
-    data_files = st.sidebar.file_uploader("", type=["pdf", "txt", "docx"], accept_multiple_files=True)
-    for data_file in data_files:
-        # Upload file
-        if data_file:
-            raw_json = upload_doc(data_file)
-            st.sidebar.write(raw_json)
-            if debug:
-                st.subheader("REST API JSON response")
-                st.sidebar.write(raw_json)
-
-    # load csv into pandas dataframe
+    # File upload block
+    if not DISABLE_FILE_UPLOAD:
+        st.sidebar.write("## File Upload:")
+        data_files = st.sidebar.file_uploader("", type=["pdf", "txt", "docx"], accept_multiple_files=True)
+        for data_file in data_files:
+            # Upload file
+            if data_file:
+                raw_json = upload_doc(data_file)
+                st.sidebar.write(str(data_file.name) + " &nbsp;&nbsp; ✅ ")
+                if debug:
+                    st.subheader("REST API JSON response")
+                    st.sidebar.write(raw_json)
+
+    st.sidebar.markdown("""
+    <style>
+        a {
+            text-decoration: none;
+        }
+        .haystack-footer {
+            text-align: center;
+        }
+        .haystack-footer h4 {
+            margin: 0.1rem; 
+            padding:0;
+        }
+        footer {
+            opacity: 0;
+        }
+    </style>
+    <div class="haystack-footer">
+        <hr />
+        <h4>Built with <a href="https://www.deepset.ai/haystack">Haystack 1.0</a></h4>
+        <p>Get it on <a href="https://github.com/deepset-ai/haystack/">GitHub</a> &nbsp;&nbsp; - &nbsp;&nbsp; Read the <a href="https://haystack.deepset.ai/overview/intro">Docs</a></p>
+        <small>Data crawled from <a href="https://en.wikipedia.org/wiki/Category:Lists_of_countries_by_continent">Wikipedia</a> in November 2021.<br />See the <a href="https://creativecommons.org/licenses/by-sa/3.0/">License</a> (CC BY-SA 3.0).</small>
+    </div>
+    """, unsafe_allow_html=True)
+
+    # Load csv into pandas dataframe
     if eval_mode:
         try:
-            df = pd.read_csv(eval_labels, sep=";")
+            df = pd.read_csv(EVAL_LABELS, sep=";")
         except Exception:
-            sys.exit("The eval file was not found. Please check the README for more information.")
-        if (
-            state_question
-            and hasattr(state_question, "next_question")
-            and hasattr(state_question, "random_question")
-            and state_question.next_question
-        ):
-            random_question = state_question.random_question
-            random_answer = state_question.random_answer
-        else:
-            random_question, random_answer = random_questions(df)
-            state_question.random_question = random_question
-            state_question.random_answer = random_answer
-
-    # Get next random question from the CSV
-    if eval_mode:
-        next_question = st.button("Load new question")
-        if next_question:
-            random_question, random_answer = random_questions(df)
-            state_question.random_question = random_question
-            state_question.random_answer = random_answer
-            state_question.next_question = True
-            state_question.run_query = False
-        else:
-            state_question.next_question = False
+            sys.exit(f"The eval file was not found under `{EVAL_LABELS}`. Please check the README for more information.")
+
+        # Get next random question from the CSV
+        state.get_next_question = st.button("Load new question")
+        if state.get_next_question:
+            reset_results()
+            new_row = df.sample(1)   
+            while new_row["Question Text"].values[0] == state.random_question:  # Avoid picking the same question twice (the change is not visible on the UI)
+                new_row = df.sample(1)
+            state.random_question = new_row["Question Text"].values[0]
+            state.random_answer = new_row["Answer"].values[0]
 
     # Search bar
-    question = st.text_input("Please provide your query:", value=random_question)
-    if state_question and state_question.run_query:
-        run_query = state_question.run_query
-        st.button("Run")
-    else:
-        run_query = st.button("Run")
-        state_question.run_query = run_query
-
-    raw_json_feedback = ""
+    question = st.text_input(
+        "Please provide your query:", 
+        value=state.random_question, 
+        max_chars=100, 
+        on_change=reset_results
+    )
+    
+    # In evaluation mode I should not re-assign the value of the Run button click
+    run_query = st.button("Run")
 
+    # Check the connection
     with st.spinner("⌛️ &nbsp;&nbsp; Haystack is starting..."):
         if not haystack_is_ready():
             st.error("🚫 &nbsp;&nbsp; Connection Error. Is Haystack running?")
             run_query = False
+            reset_results()
 
     # Get results for query
-    if run_query:
+    if run_query and question:
+        reset_results()
         with st.spinner(
             "🧠 &nbsp;&nbsp; Performing neural search on documents... \n "
             "Do you want to optimize speed or accuracy? \n"
             "Check out the docs: https://haystack.deepset.ai/usage/optimization "
         ):
             try:
-                results, raw_json = retrieve_doc(question, top_k_reader=top_k_reader, top_k_retriever=top_k_retriever)
+                state.results, state.raw_json = retrieve_doc(question, top_k_reader=top_k_reader, top_k_retriever=top_k_retriever)
             except Exception as e:
                 logging.exception(e)
-                st.error("🐞 &nbsp;&nbsp; An error occurred during the request. Check the logs in the console to know more.")
+                if "The server is busy processing requests" in str(e):
+                    st.error("🧑‍🌾 &nbsp;&nbsp; All our workers are busy! Try again later.")
+                else:
+                    st.error("🐞 &nbsp;&nbsp; An error occurred during the request. Check the logs in the console to know more.")
                 return
 
-        # Show if we use a question of the given set
-        if question == random_question and eval_mode:
+    if state.results:
+
+        # Show the gold answer if we use a question of the given set
+        if question == state.random_question and eval_mode:
             st.write("## Correct answers:")
-            random_answer
+            st.write(state.random_answer)
 
         st.write("## Results:")
+        count = 0  # Make every button key unique
 
-        # Make every button key unique
-        count = 0
-
-        for result in results:
+        for result in state.results:
             if result["answer"]:
-                annotate_answer(result["answer"], result["context"])
+                answer, context = result["answer"], result["context"]
+                start_idx = context.find(answer)
+                end_idx = start_idx + len(answer)
+                annotated_text(context[:start_idx], (answer, "ANSWER", "#8ef"), context[end_idx:])
             else:
-                show_plain_documents(result["context"])
+                st.markdown(result["context"])
+
             st.write("**Relevance:** ", result["relevance"], "**Source:** ", result["source"])
             if eval_mode:
                 # Define columns for buttons
-                button_col1, button_col2, button_col3, button_col4 = st.columns([1, 1, 1, 6])
-                if button_col1.button("👍", key=(result["context"] + str(count) + "1"), help="Correct answer"):
-                    raw_json_feedback = feedback_doc(
-                        question, "true", result["document_id"], 1, "true", result["answer"], result["offset_start_in_doc"]
+                button_col1, button_col2, button_col3, _ = st.columns([1, 1, 1, 6])
+                if button_col1.button("👍", key=f"{result['context']}{count}1", help="Correct answer"):
+                    feedback_doc(
+                        question=question, 
+                        is_correct_answer="true", 
+                        document_id=result["document_id"], 
+                        model_id=1, 
+                        is_correct_document="true",
+                        answer=result["answer"], 
+                        offset_start_in_doc=result["offset_start_in_doc"]
                     )
-                    st.success("Thanks for your feedback")
-                if button_col2.button("👎", key=(result["context"] + str(count) + "2"), help="Wrong answer and wrong passage"):
-                    raw_json_feedback = feedback_doc(
-                        question,
-                        "false",
-                        result["document_id"],
-                        1,
-                        "false",
-                        result["answer"],
-                        result["offset_start_in_doc"],
+                    st.success("✨ &nbsp;&nbsp; Thanks for your feedback! &nbsp;&nbsp; ✨")
+
+                if button_col2.button("👎", key=f"{result['context']}{count}2", help="Wrong answer and wrong passage"):
+                    feedback_doc(
+                        question=question, 
+                        is_correct_answer="false", 
+                        document_id=result["document_id"], 
+                        model_id=1, 
+                        is_correct_document="false",
+                        answer=result["answer"], 
+                        offset_start_in_doc=result["offset_start_in_doc"]
                     )
-                    st.success("Thanks for your feedback!")
-                if button_col3.button("👎👍", key=(result["context"] + str(count) + "3"), help="Wrong answer, but correct passage"):
-                    raw_json_feedback = feedback_doc(
-                        question, "false", result["document_id"], 1, "true", result["answer"], result["offset_start_in_doc"]
+                    st.success("✨ &nbsp;&nbsp; Thanks for your feedback! &nbsp;&nbsp; ✨")
+
+                if button_col3.button("👎👍", key=f"{result['context']}{count}3", help="Wrong answer, but correct passage"):
+                    feedback_doc(
+                        question=question, 
+                        is_correct_answer="false", 
+                        document_id=result["document_id"], 
+                        model_id=1, 
+                        is_correct_document="true",
+                        answer=result["answer"], 
+                        offset_start_in_doc=result["offset_start_in_doc"]
                     )
-                    st.success("Thanks for your feedback!")
+                    st.success("✨ &nbsp;&nbsp; Thanks for your feedback! &nbsp;&nbsp; ✨")
                 count += 1
             st.write("___")
+
         if debug:
             st.subheader("REST API JSON response")
-            st.write(raw_json)
+            st.write(state.raw_json)
+
 
 main()


Code review comments:
diff_hunk: @@ -12,175 +13,197 @@
 import SessionState
 from utils import feedback_doc, haystack_is_ready, retrieve_doc, upload_doc
 
+
 # Adjust to a question that you would like users to see in the search bar when they load the UI:
 DEFAULT_QUESTION_AT_STARTUP = "Who is the father of Arya Stark?"
path: ui/webapp.py
text: @user1:
I see that you changed the `eval_labels_example.csv` according to our new dataset already. In that case, we should also change the DEFAULT_QUESTION_AT_STARTUP.

diff_hunk: @@ -12,175 +13,197 @@
 import SessionState
 from utils import feedback_doc, haystack_is_ready, retrieve_doc, upload_doc
 
+
 # Adjust to a question that you would like users to see in the search bar when they load the UI:
 DEFAULT_QUESTION_AT_STARTUP = "Who is the father of Arya Stark?"
 
+# Labels for the evaluation
+EVAL_LABELS = os.getenv("EVAL_FILE", Path(__file__).parent / "eval_labels_example.csv")
 
-def annotate_answer(answer, context):
-    """ If we are using an extractive QA pipeline, we'll get answers
-    from the API that we highlight in the given context"""
-    start_idx = context.find(answer)
-    end_idx = start_idx + len(answer)
-    # calculate dynamic height depending on context length
-    height = int(len(context) * 0.50) + 5
-    annotated_text(context[:start_idx], (answer, "ANSWER", "#8ef"), context[end_idx:], height=height)
-
-
-def show_plain_documents(text):
-    """ If we are using a plain document search pipeline, i.e. only retriever, we'll get plain documents
-    from the API that we just show without any highlighting"""
-    st.markdown(text)
-
-
-def random_questions(df):
-    """
-    Helper to get one random question + gold random_answer from the user's CSV 'eval_labels_example'.
-    This can then be shown in the UI when the evaluation mode is selected. Users can easily give feedback on the
-    model's results and "enrich" the eval dataset with more acceptable labels
-    """
-    random_row = df.sample(1)
-    random_question = random_row["Question Text"].values[0]
-    random_answer = random_row["Answer"].values[0]
-    return random_question, random_answer
+# Whether the file upload should be enabled or not
+DISABLE_FILE_UPLOAD = os.getenv("HAYSTACK_UI_DISABLE_FILE_UPLOAD")
 
 
 def main():
-    # Define state
-    state_question = SessionState.get(
-        random_question=DEFAULT_QUESTION_AT_STARTUP, random_answer="", next_question="false", run_query="false"
+
+    # Persistent state
+    state = SessionState.get(
+        random_question=DEFAULT_QUESTION_AT_STARTUP, 
+        random_answer="",
+        results=None,
+        raw_json=None,
+        get_next_question=True
     )
 
-    # Initialize variables
-    eval_mode = False
-    random_question = DEFAULT_QUESTION_AT_STARTUP
-    eval_labels = os.getenv("EVAL_FILE", "eval_labels_example.csv")
+    # Small callback to reset the interface in case the text of the question changes
+    def reset_results(*args):
+        state.results = None
+        state.raw_json = None
+
+    # Title
+    st.write("# Countries of the World")
 
-    # UI search bar and sidebar
-    st.write("# Haystack Demo")
+    # Sidebar
     st.sidebar.header("Options")
     top_k_reader = st.sidebar.slider("Max. number of answers", min_value=1, max_value=10, value=3, step=1)
-    top_k_retriever = st.sidebar.slider(
-        "Max. number of documents from retriever", min_value=1, max_value=10, value=3, step=1
-    )
+    top_k_retriever = st.sidebar.slider("Max. number of documents from retriever", min_value=1, max_value=10, value=3, step=1)
     eval_mode = st.sidebar.checkbox("Evaluation mode")
     debug = st.sidebar.checkbox("Show debug info")
 
-    st.sidebar.write("## File Upload:")
-    data_files = st.sidebar.file_uploader("", type=["pdf", "txt", "docx"], accept_multiple_files=True)
-    for data_file in data_files:
-        # Upload file
-        if data_file:
-            raw_json = upload_doc(data_file)
-            st.sidebar.write(raw_json)
-            if debug:
-                st.subheader("REST API JSON response")
-                st.sidebar.write(raw_json)
-
-    # load csv into pandas dataframe
+    # File upload block
+    if not DISABLE_FILE_UPLOAD:
+        st.sidebar.write("## File Upload:")
+        data_files = st.sidebar.file_uploader("", type=["pdf", "txt", "docx"], accept_multiple_files=True)
+        for data_file in data_files:
+            # Upload file
+            if data_file:
+                raw_json = upload_doc(data_file)
+                st.sidebar.write(str(data_file.name) + " &nbsp;&nbsp; ✅ ")
+                if debug:
+                    st.subheader("REST API JSON response")
+                    st.sidebar.write(raw_json)
+
+    st.sidebar.markdown("""
+    <style>
+        a {
+            text-decoration: none;
+        }
+        .haystack-footer {
+            text-align: center;
+        }
+        .haystack-footer h4 {
+            margin: 0.1rem; 
+            padding:0;
+        }
+        footer {
+            opacity: 0;
+        }
+    </style>
+    <div class="haystack-footer">
+        <hr />
+        <h4>Built with <a href="https://www.deepset.ai/haystack">Haystack 1.0</a></h4>
path: ui/webapp.py
text: @user1:
I wonder whether we could reuse our `versionfromfile()` method from https://github.com/deepset-ai/haystack/blob/31e22012da1dfe298ee539a7344874f2f1013925/setup.py#L51
`_version: str = versionfromfile(here, "haystack", "_version.py")`

@user1:
As an alternative, maybe let's not mention the version number in the demo at all.

@author:
Good idea actually! I will use `versionfromfile`

@author:
Ok actually I'm not sure I can, because the UI and haystack are not necessarily installed in the same container... :sweat_smile: I'm gonna implement a real small endpoint on the rest api and use that, that's cleaner

@user1:
Or leave the version number out for now. It's not important from my point of view.

diff_hunk: @@ -1,4 +1,15 @@
 "Question Text";"Answer"
-"Who is the father of Arya Stark?";"Ned Stark"
-"Who is the mother of Arya Stark?";"Catelyn Stark"
-"Who created the dothraki language?";"David J. Peterson"
\ No newline at end of file
+"Does the Danube cross Austria?";"Yes"
+"Where is the deepset lake in the world?";"Lake Bajkal"
+"What is the name of the highest mountain of Chile?";"Ojos del Salado"
+"What is the largest city of Camerun?";"Douala"
+"In which country is the highest airport in the world?";"China"
+"How many national parks are in Sweden?";"30"
+"How many regions has France?";"18"
+"Which country is the biggest producer of peanuts in 2021?";"China"
+"How many barrels of oil did Noway export in 2019?";"1.7 million barrels a day"
path: ui/eval_labels_example.csv
text: @user1:
Noway should be Norway. While haystack should be able to deal with typos like this, we don't need to make it extra difficult. ;) 

@user1:
For the evaluation of extractive question answering, we need the answer string to be contained in the Wikipedia page of Norway or Oslo but I couldn't find it. Let's maybe try to reuse questions about countries or cities from the SQuAD dataset?

@author:
Hi sure! These are only some random questions for me to test how it looked on the interface, we should replace them completely if we have a better source of question/answer pairs ready to go. Do you think there is a good way to automatically filter pairs from SQuAD or shall I take some time to pick a few by hand?

diff_hunk: @@ -12,175 +13,197 @@
 import SessionState
 from utils import feedback_doc, haystack_is_ready, retrieve_doc, upload_doc
 
+
 # Adjust to a question that you would like users to see in the search bar when they load the UI:
 DEFAULT_QUESTION_AT_STARTUP = "Who is the father of Arya Stark?"
 
+# Labels for the evaluation
+EVAL_LABELS = os.getenv("EVAL_FILE", Path(__file__).parent / "eval_labels_example.csv")
 
-def annotate_answer(answer, context):
-    """ If we are using an extractive QA pipeline, we'll get answers
-    from the API that we highlight in the given context"""
-    start_idx = context.find(answer)
-    end_idx = start_idx + len(answer)
-    # calculate dynamic height depending on context length
-    height = int(len(context) * 0.50) + 5
-    annotated_text(context[:start_idx], (answer, "ANSWER", "#8ef"), context[end_idx:], height=height)
-
-
-def show_plain_documents(text):
-    """ If we are using a plain document search pipeline, i.e. only retriever, we'll get plain documents
-    from the API that we just show without any highlighting"""
-    st.markdown(text)
-
-
-def random_questions(df):
-    """
-    Helper to get one random question + gold random_answer from the user's CSV 'eval_labels_example'.
-    This can then be shown in the UI when the evaluation mode is selected. Users can easily give feedback on the
-    model's results and "enrich" the eval dataset with more acceptable labels
-    """
-    random_row = df.sample(1)
-    random_question = random_row["Question Text"].values[0]
-    random_answer = random_row["Answer"].values[0]
-    return random_question, random_answer
+# Whether the file upload should be enabled or not
+DISABLE_FILE_UPLOAD = os.getenv("HAYSTACK_UI_DISABLE_FILE_UPLOAD")
 
 
 def main():
-    # Define state
-    state_question = SessionState.get(
-        random_question=DEFAULT_QUESTION_AT_STARTUP, random_answer="", next_question="false", run_query="false"
+
+    # Persistent state
+    state = SessionState.get(
+        random_question=DEFAULT_QUESTION_AT_STARTUP, 
+        random_answer="",
+        results=None,
+        raw_json=None,
+        get_next_question=True
     )
 
-    # Initialize variables
-    eval_mode = False
-    random_question = DEFAULT_QUESTION_AT_STARTUP
-    eval_labels = os.getenv("EVAL_FILE", "eval_labels_example.csv")
+    # Small callback to reset the interface in case the text of the question changes
+    def reset_results(*args):
+        state.results = None
+        state.raw_json = None
+
+    # Title
+    st.write("# Countries of the World")
path: ui/webapp.py
text: @user1:
I thought "Haystack Demo" (or "haystack Demo") is a good title. Maybe check with Andrey regarding SEO?

@author:
Yep also this shouldn't have made it to this PR, sorry :sweat_smile: Rolling back :+1:

diff_hunk: @@ -1,4 +1,15 @@
 "Question Text";"Answer"
-"Who is the father of Arya Stark?";"Ned Stark"
-"Who is the mother of Arya Stark?";"Catelyn Stark"
-"Who created the dothraki language?";"David J. Peterson"
\ No newline at end of file
+"Does the Danube cross Austria?";"Yes"
path: ui/eval_labels_example.csv
text: @user1:
Yes/No questions won't work out-of-the-box with haystack at the moment as far as I know. Let's not include that kind of questions (yet).

diff_hunk: @@ -1,4 +1,15 @@
 "Question Text";"Answer"
-"Who is the father of Arya Stark?";"Ned Stark"
-"Who is the mother of Arya Stark?";"Catelyn Stark"
-"Who created the dothraki language?";"David J. Peterson"
\ No newline at end of file
+"Does the Danube cross Austria?";"Yes"
+"Where is the deepset lake in the world?";"Lake Bajkal"
path: ui/eval_labels_example.csv
text: @user1:
It could well be that the models return a country or even continent as an answer. We should make the questions more precise.