How to use radio buttons in chat_input?

I have the following app:

import streamlit as st
import time

# ------------------------------------------------------------------------------
# Functions
# ------------------------------------------------------------------------------

def stream_response(response, message_placeholder, stream = True):
	if stream:
		full_response = ''
		for chunk in (response or "").split(' '):
			full_response += chunk + " "
			time.sleep(0.05)
			message_placeholder.markdown(full_response + '▌')
		message_placeholder.markdown(response)
	else:
		message_placeholder.markdown(response)

# ------------------------------------------------------------------------------
# Session state
# ------------------------------------------------------------------------------

if 'messages' not in st.session_state:
	st.session_state.messages = []

for message in st.session_state.messages:
	with st.chat_message(message['role']):
		st.markdown(message['content'])

if 'choice' not in st.session_state:
	st.session_state.choice = ''

# ------------------------------------------------------------------------------
# Chatbot
# ------------------------------------------------------------------------------

if prompt := st.chat_input("Whats up?"):
	if st.session_state.choice:
		print('hi')
else:
	if not st.session_state.choice:
		with st.chat_message("assistant"):
			print('* Assistant responding (hardcoded introduction)')
			stream_response(response='hey there! pick an option below', message_placeholder=st.empty())
			with st.form('form'):
				choice = st.radio(
					label='Available Options',
					options=[
						'red',
						'blue'
					],
					index=None
				)

				submitted = st.form_submit_button('Submit')

				if submitted:
					st.session_state.choice = choice
	else:
		with st.chat_message("assistant"):
			if st.session_state.choice == 'red':
				response = f'* User choice defined: {st.session_state.choice}'
				print(response)
				stream_response(response=response, message_placeholder=st.empty())
				st.session_state.messages.append({'role': 'assistant', 'content': response})

			elif st.session_state.choice == 'blue':
				response = f'* User choice defined: {st.session_state.choice}'
				print(response)
				stream_response(response=response, message_placeholder=st.empty())
				st.session_state.messages.append({'role': 'assistant', 'content': response})

			else:
				print(f'* User choice defined but not expected: {st.session_state.choice}')

When I run this, I have to click the forms “submit” button twice to get the result I want. How can I make it so that only 1 click is required? I am new to python and have a feeling my understanding of control flow is lacking here.


Edit: Playing with the idea of st.rerun() to avoid the second click and seems to be doing what I want, is this the recommended approach?

Try this:

if st.form_submit_button('Submit'):
    st.session_state.choice = choice

instead of:

submitted = st.form_submit_button('Submit')

if submitted:
    st.session_state.choice = choice

Thanks but no luck, if I add a st.rerun() tho I get something close to what I’m looking for:

if st.form_submit_button('Submit'):
	st.session_state.choice = choice
	st.rerun() # <-- added this

I see the issue, you don’t need to use an else block to link the code under it to whether st.session_state.choice is defined or not. Instead, make it it’s own if statement that will get checked each rerun (after button is clicked):

if prompt := st.chat_input("Whats up?"):
    if st.session_state.choice:
        st.write('hi')
else:
    if not st.session_state.choice:
        with st.chat_message("assistant"):
            st.write('* Assistant responding (hardcoded introduction)')
            with st.form('form'):
                choice = st.radio(
                    label='Available Options',
                    options=[
                        'red',
                        'blue'
                    ]
                )

                submitted = st.form_submit_button('Submit')

                if submitted:
                    st.session_state.choice = choice

    if st.session_state.choice:
        with st.chat_message("assistant"):
            if st.session_state.choice == 'red':
                response = f'* User choice defined: {st.session_state.choice}'
                st.write(response)

            elif st.session_state.choice == 'blue':
                response = f'* User choice defined: {st.session_state.choice}'
                st.write(response)
            else:
                st.write(f'* User choice defined but not expected: {st.session_state.choice}')
1 Like

Why exactly do you use a form here?

If you really want to force the submit button after a selection is made, you can just set the key of the radio and that value will be in Session State as soon as the form submit button is clicked and be available from the beginning of the rerun that results.

			with st.form('form'):
				choice = st.radio(
					label='Available Options',
					options=[
						'red',
						'blue'
					],
					index=None,
					key="choice"
				)

				st.form_submit_button('Submit')

If maybe you don’t really need that form, you can simplify it further. (Just making the selection on the radio will trigger the rerun.

			choice = st.radio(
				label='Available Options',
				options=[
					'red',
					'blue'
				],
				index=None,
				key="choice"
			)

Thanks a bunch, this does solve the problem in my example.

If I understand correctly, this is working because the logic doesn’t have to go backward, it continues to flow down the script and the session state works as expected. Is that right?

If my understanding is right, I am starting to realize that my question is more, how can I achieve going backward in the logic? For example, if I initialize a chat input that is disabled by default, then I enable it downstream using a session state variable, how would I force the chat input to update and enable itself?

import streamlit as st

# ------------------------------------------------------------------------------
# Session state
# ------------------------------------------------------------------------------

if 'messages' not in st.session_state:
	st.session_state.messages = []

for message in st.session_state.messages:
	with st.chat_message(message['role']):
		st.markdown(message['content'])

if 'choice' not in st.session_state:
	st.session_state.choice = ''

if 'disable_chat' not in st.session_state:
	st.session_state.disable_chat = True

# ------------------------------------------------------------------------------
# Chatbot
# ------------------------------------------------------------------------------

if prompt := st.chat_input("Whats up?", disabled=st.session_state.disable_chat): 
    if st.session_state.choice:
        st.write('hi')
        
else:
    if not st.session_state.choice:
        with st.chat_message("assistant"):
            st.write('* Assistant responding (hardcoded introduction)')
            with st.form('form'):
                choice = st.radio('Available Options', options=['red', 'blue'])
                submitted = st.form_submit_button('Submit')
                if submitted:
                    st.session_state.choice = choice
                    st.session_state.disable_chat = False # <------ enable the chat!

    if st.session_state.choice:
        with st.chat_message("assistant"):
            if st.session_state.choice == 'red':
                response = f'* User choice defined: {st.session_state.choice}'
                st.write(response)

            elif st.session_state.choice == 'blue':
                response = f'* User choice defined: {st.session_state.choice}'
                st.write(response)
                
            else:
                st.write(f'* User choice defined but not expected: {st.session_state.choice}')

The key thing sounds interesting, let me give it a go. In regards to why I want a form, it’s to avoid the chat input from re-rendering over and over as the user clicks different options in the radio button. I want the user to be sure what their choice is, click submit, and then move on.

If you need some more persistence of the radio choice, you can also set the value in a callback instead of through key (as I wrote) or through the output (as in your original code). This sort of “double submit” issue happens frequently when using the output of functions since it has to rerun once to get the new output, then rerun again to use that output (somewhere before the widget). Callbacks and widget keys are my go-to.

Thanks, the key parameter seems to avoid the need for a st.rerun() however, I’m still stumped on how I can dynamically disable/enable the chat input using session state. Sorry to keep switching gears here, I think I’m doing a poor job of explaining the fundamental challenge I am facing.

I come from R and use the shiny framework. Does streamlit have anything like reactivity in shiny? I have a feeling this is the center of my confusion. Additional info here: Shiny for Python - Streamlit

In shiny I would have a reactive value called disable and any time that value gets modified it updates only the inputs that depend on that reactive value disable().

Streamlit works by rerunning the whole script. So generally a script runs all the way to end, then a user interacts with a widget, and the whole thing runs top to bottom with that widget holding the new value. I think it’s common for new Streamlit developers to (implicitly or subconsciuosly) think the script is somehow picking up from the point of interaction or “waiting” at a widget in some sense, but it’s not.

We’re working on some additional control elements to be able to optimize how and where an app reruns, but it’s still in development.

Try using the “choice” key in the radio widget and defining a callback function for the form submit button to handle the chat enabling.

def enable_chat():
    st.session_state.disable_chat = False

Then you can get rid of submitted = ... and if submitted:.

You can just have

st.form_submit_button("Submit", on_click=enable_chat)

I think callbacks are going to give you the closest thing to your intuition because a callback executes instantly, before the rerun.

The Button behavior guide and Widget behavior guide both contain some good information and examples.

1 Like

Thanks for working through this with me, super helpful. What I’ve taken from this is that I should be using callback functions for my use case. This seems to be working as I hoped in a hypothetical workflow where:

  1. Chat input is disabled by default, requiring radio button selection first
  2. Once radio button choice is select and form is submitted, callback to enable chat
  3. Collect radio button choice and write it to chat

This should enable me to let the user first pick a direction they want to go in the chat first and foremost. Example I ended up with below:

import streamlit as st

# callbacks
def enable_chat():
    st.session_state.disable_chat = False

def disable_chat():
    st.session_state.disable_chat = True
    
# session state
if 'disable_chat' not in st.session_state:
    st.session_state.disable_chat = True

if 'choice' not in st.session_state:
    st.session_state.choice = ''

# chat input
if prompt := st.chat_input("Whats up?", disabled=st.session_state.disable_chat):
    with st.chat_message('assistant'):
        st.write(st.session_state.choice)
else:
    with st.chat_message('assistant'):
        if st.session_state.choice:
            st.write(st.session_state.choice)
        else:
            with st.form('form'):
                choice = st.radio(label='options', options=['red', 'blue'])
                st.session_state.choice = choice
                st.form_submit_button(on_click=enable_chat)

I think you may get some unexpected behavior around your “choice” value. If you don’t set a key for the radio widget, then the output won’t go into Session State until near the end of the rerun for the form submission. Your chat will be enabled but I don’t think you’ll ever get out of

st.session_state.choice = 'red'

As written, ‘red’ is the default and will never have an opportunity to change.

If you use a key, it will make ‘choice’ immediately available:

import streamlit as st

# callbacks
def enable_chat():
    st.session_state.disable_chat = False

def disable_chat():
    st.session_state.disable_chat = True
    
# session state
if 'disable_chat' not in st.session_state:
    st.session_state.disable_chat = True

if 'choice' not in st.session_state:
    st.session_state.choice = None

# chat input
if prompt := st.chat_input("Whats up?", disabled=st.session_state.disable_chat):
    with st.chat_message('assistant'):
        st.write(st.session_state.choice)
else:
    with st.chat_message('assistant'):
        if st.session_state.choice:
            st.write(st.session_state.choice)
        else:
            with st.form('form'):
                st.radio(label='options', options=['red', 'blue'], key='choice')
                st.form_submit_button(on_click=enable_chat)

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.