Search code examples
pythonstreamlit

Why is my streamlit app's second radio disappearing when I change the default answer?


I am designing an small annotation interface which will present users with a series of examples. An example will be presented to a user, they will make a mutually exclusive selection using a radio button, then more information will be presented in a second button where they can make a new selection with the same options; after submitting this, they move to the next example. I have the following code:

#!/usr/bin/env python3

import streamlit as st

columns = ["question", "answerA", "answerB", "answerC", "explanationA", "explanationB", "explanationC", "answer"]

with open('fake_data.tsv') as f:
    examples = [dict(zip(columns, line.strip().split("\t"))) for line in f]

def show_example(examples, i, answers):
    example = examples[i]
    st.header("context: TODO, wire this up")
    answer_before = st.radio(example["question"], [example["answerA"], example["answerB"], example["answerC"], "multiple options"], key=f"q1-{i}")
    if st.button("submit", key=f"b1-{i}"):
        answer_after = st.radio(example["question"], [
            ": ".join((example["answerA"], example["explanationA"])),
            ": ".join((example["answerB"], example["explanationB"])),
            ": ".join((example["answerC"], example["explanationC"])),
            "multiple options"
        ], key=f"q2-{i}")
        if i + 1 == len(examples):
            print("Saving answers")
        else:
            if st.button("next question", key=f"b2-{i}"):
                answers.append((answer_before, answer_after))
                return show_example(examples, i+1, answers)

show_example(examples, 0, [])

which when run with a sample data file:

  1. Presents the first example's first part
  2. Allows a user to submit their answer
  3. Presents the first example's second part

but when a different selection is made in (3), the second radio button disappears completely.

My data file is here:

fake_data.tsv:

question0   answerA0    answerB0    answerC0    explanationA0   explanationB0   explanationC0   acceptedAnswer0
question1   answerA1    answerB1    answerC1    explanationA1   explanationB1   explanationC1   acceptedAnswer1
question2   answerA2    answerB2    answerC2    explanationA2   explanationB2   explanationC2   acceptedAnswer2
question3   answerA3    answerB3    answerC3    explanationA3   explanationB3   explanationC3   acceptedAnswer3

How can I achieve my desired interaction? I'm using streamlit 1.22.0.


Solution

  • You have nested buttons. A button will return True only on the page load resulting from its click, then go back to False. If you nest something inside a button, it will disappear as soon as the user makes their next move. Therefore, anything conditioned directly on a button should be transient--meant to execute once and go away.

    When you need something to display conditioned on a button having been pressed any time in the past, you would need the button to toggle some flag in session state, then use that for your conditional. (See point one in this Streamlit blog post.)

    For your example, here's some edits:

    You basically have two stages for each question (before and after). I created a flag in session state using the key after. It is initially False, becomes True when someone submits a "before" answer, then goes back to False when someone submits their "after" answer and proceeds to the next question. The buttons affect this value in session state via callbacks.

    As a little extra, I set the first button and radio widget to disable when it's clicked so the user can't go back and change their "before" answer.

    I left the keys in place, but they aren't needed as it currently runs with each question just being displayed by itself. If you further change it so the questions are displayed simultaneously, then you'd need keys to keep your submit keys distinct, for instance.

    import streamlit as st
    import pandas as pd
    
    columns = ["question", "answerA", "answerB", "answerC", "explanationA", "explanationB", "explanationC", "answer"]
    
    #Changed to inline executable example
    examples = pd.DataFrame(
        [[f'q{i}',f'A{i}',f'B{i}',f'C{i}',f'exA{i}',f'exB{i}',f'exC{i}',f'a{i}',] for i in range(10)], 
        columns=columns
    )
    
    # Initialize Values in Session State
    if 'answers' not in st.session_state:
        st.session_state.after = False # To track whether "before" vs "after"
        st.session_state.done = False # To track the last submission when there is no next
        st.session_state.i = 0 # To track which question the user is on
        st.session_state.answers = [] # To save the answers
    
    def after():
        '''Change 'after' key in session state to True
        '''
        st.session_state.after = True
    
    def next(answers, answer_before, answer_after):
        '''Change 'after' key back to false, increment question tracker, save answers
        '''
        st.session_state.after = False
        st.session_state.i += 1
        answers.append((answer_before, answer_after))
    
    def done(answers, answer_before, answer_after):
        '''Change 'done' key to True and save answers to final question
        '''
        st.session_state.done = True
        answers.append((answer_before, answer_after))
    
    def show_example(examples, i, answers):
        #Changed to match example
        #example = examples[i]
        example = examples.iloc[i]
        st.header("context: TODO, wire this up")
        answer_before = st.radio(
            example["question"], 
            [example["answerA"], example["answerB"], example["answerC"], "multiple options"], 
            key=f"q1-{i}",
            disabled=st.session_state.after)
        st.button("submit", key=f"b1-{i}", disabled=st.session_state.after, on_click=after)
        if st.session_state.after:
            answer_after = st.radio(example["question"], [
                ": ".join((example["answerA"], example["explanationA"])),
                ": ".join((example["answerB"], example["explanationB"])),
                ": ".join((example["answerC"], example["explanationC"])),
                "multiple options"
            ], key=f"q2-{i}", disabled=st.session_state.done)
            if i + 1 == len(examples):
                st.button('Done', on_click=done, disabled=st.session_state.done, args=[answers,answer_before,answer_after])
                if st.session_state.done:
                    st.write(st.session_state.answers)
            else:
                st.button("next question", key=f"b2-{i}", on_click=next, args=[answers,answer_before,answer_after])
    
    show_example(examples, st.session_state.i, st.session_state.answers)