streamlit icon indicating copy to clipboard operation
streamlit copied to clipboard

Impossible to disable st.chat_input while writing the model's response

Open Napuh opened this issue 1 year ago • 12 comments

Checklist

  • [X] I have searched the existing issues for similar issues.
  • [X] I added a very descriptive title to this issue.
  • [X] I have provided sufficient information below to help reproduce this issue.

Summary

When a user enters a prompt in st.chat_input, it is not possible to disable the input while an LLM is generating the response to that input.

This leads to the possibility of the user 'interrupting' the model output and messing up the conversation structure.

I've tried binding the disable argument to a variable inside the st.session_state, but it does not work.

This happens in the official 'chat-GPT like' implementation on streamlit forums.

Reproducible Code Example

import streamlit as st
import time

st.title("ChatGPT-like clone")

def stream_example_response():
    response = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

    # yield 5 characters every 0.1 seconds
    for i in range(0, len(response), 5):
        time.sleep(0.1)
        yield response[i:i+5]

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

if "disable_chat_input" not in st.session_state:
    st.session_state.disable_chat_input = False

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

if prompt := st.chat_input("What is up?", disabled=st.session_state.disable_chat_input):
    
    st.session_state['disable_chat_input'] = True

    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        response = st.write_stream(stream_example_response())
    st.session_state.messages.append({"role": "assistant", "content": response})

    st.session_state['disable_chat_input'] = False

Steps To Reproduce

  1. Run the minimal example provided (streamlit run minimal.py)
  2. Send a prompt
  3. While the response is streaming, send another prompt
  4. The response gets interrupted, doesn't get registered on the history and the messages become messed up.

Expected Behavior

With the disabled=st.session_state.disable_chat_input I was expecting that the user input would be disabled until the response stops streaming. Perhaps would be useful to have an argument in st.chat_input or something so that it automatically disables until all the code inside the if prompt:= ... statement ends, whatever works best.

Current Behavior

No response

Is this a regression?

  • [ ] Yes, this used to work in a previous version.

Debug info

  • Streamlit version: 1.31.1
  • Python version: 3.10.13
  • Operating System: Windows
  • Browser: Firefox

Additional Information

Video of the current (wrong) behavior:

st_issue

Napuh avatar Mar 18 '24 15:03 Napuh

To help Streamlit prioritize this feature, react with a 👍 (thumbs up emoji) to the initial post.

Your vote helps us identify which enhancements matter most to our users.

Views

github-actions[bot] avatar Mar 18 '24 18:03 github-actions[bot]

@Napuh Thanks for reporting this issue. This is currently expected, but we definitely could improve this behaviour.

lukasmasuch avatar Mar 18 '24 18:03 lukasmasuch

So in the current state of streamlit and st.chat_input, there is no way to disable the input while the response is generating?

How could this be implemented? In the case of an hipothetical PR

Napuh avatar Mar 18 '24 19:03 Napuh

@Napuh There might be a workaround by using st.rerun, but that could get a bit tricky. You would need to store the current text that's returned with chat_input in the session state, triggered a rerun, and check if this is in session state, set the chat_input to disabled and react to the text.

We could solve it in Streamlit by adding some functionality that auto-disables chat input after the users submits a text for the remaining session. But that would require some discussions with the product team.

lukasmasuch avatar Mar 18 '24 20:03 lukasmasuch

This would be a desired functionality to be able to to disable chat input during model's processing! :)

KRoszyk avatar Mar 29 '24 14:03 KRoszyk

This would also be a desirable functionality to disable chat while prompting for streamlit-feedback, making feedback mandatory after each request.

Right now saving chat state and performing st.rerun() makes the feedback box disappear.

bram49 avatar Jul 09 '24 13:07 bram49

Following is my workaround; it works well for my simple chatbot:

  1. use session state to control the input state, and get the input value
  2. move everything into the on_submit callback, DO NOT use st.chat_message("user").write(...) in it, or will write dup items.

if "disabled" not in st.session_state:
    st.session_state.disabled = False

if "messages" not in st.session_state or 0 == len(st.session_state.messages):
    st.session_state.messages = [{"role": "assistant", "content": "Hi, what can I do for you?"}]
    

for msg in st.session_state.messages:
    st.chat_message(msg["role"]).write(msg["content"])

def on_submit():
    st.session_state.disabled = True

    st.session_state.messages.append({"role": "user", "content": st.session_state.prompt})
    # st.chat_message("user").write(st.session_state.prompt)

    response = llm.chat(st.session_state.prompt)

    st.session_state.messages.append({"role": "assistant", "content": response})
    # st.chat_message("assistant").write(response)

    st.session_state.disabled = False


if prompt := st.chat_input("You question here", on_submit=on_submit, disabled=st.session_state.disabled, key="prompt"):
    pass

chaosddp avatar Jul 15 '24 10:07 chaosddp

@Napuh There might be a workaround by using st.rerun, but that could get a bit tricky. You would need to store the current text that's returned with chat_input in the session state, triggered a rerun, and check if this is in session state, set the chat_input to disabled and react to the text.

We could solve it in Streamlit by adding some functionality that auto-disables chat input after the users submits a text for the remaining session. But that would require some discussions with the product team.

Hi @LukasMasuch , any update on this ?

iamgauravpant avatar Jul 24 '24 11:07 iamgauravpant

EDIT: added css selector for streamlit 1.39.0

This was was a 'must be fixed'. Callback was not working for me. I ended up using following css hack:

import time
import streamlit as st

# .stApp[data-teststate=running] is present when bot answer is streaming
st.markdown("""
<style>
    .stApp[data-teststate=running] .stChatInput textarea,
    .stApp[data-test-script-state=running] .stChatInput textarea {
        display: none;
    }
</style>
""", unsafe_allow_html=True)

def stream_text(text):
    for char in text:
        time.sleep(0.1)
        yield char

if prompt := st.chat_input("Say something. (This is disabled while assistant is streaming)"):
    with st.chat_message("user"):
        st.write(prompt)
    with st.chat_message("assistant"):
        st.write_stream(stream_text(f"user typed: {prompt}"))

Unfortunately, focus is lost from chat input field because of this hack. Works at least with Streamlit 1.37.1 and 1.39.0, but may break after any update...

zeppafi avatar Sep 02 '24 13:09 zeppafi

Any updates on this? I think this would be a much-needed feature for almost all chatbots

aflah02 avatar Oct 11 '24 06:10 aflah02

@chaosddp Thanks this look like an elegant way. Did you use this more? Any issues that might arise down the line? Also could you get this working with text streaming? An important part is to not do any rendering in the callback but then you can't stream the response

aflah02 avatar Oct 11 '24 06:10 aflah02

@chaosddp Thanks this look like an elegant way. Did you use this more? Any issues that might arise down the line? Also could you get this working with text streaming? An important part is to not do any rendering in the callback but then you can't stream the response

No, I changed to another framework (chainlit) for my frontend, it is much easy to use for me. :)

chaosddp avatar Oct 17 '24 07:10 chaosddp

For future readers, following this worked for me - https://discuss.streamlit.io/t/disable-st-input-chat-during-conversation/50258/2?u=aflah1

aflah02 avatar Oct 30 '24 06:10 aflah02

For future readers, here is my approach (it works for me on streamlit version 1.39.0):

import time
import streamlit as st

# beginning of your code here

def _handle_user_interaction():
    st.session_state["waiting_for_answer"] = True

if "waiting_for_answer" not in st.session_state:
    st.session_state["waiting_for_answer"] = False

# some more code here

if user_prompt := st.chat_input(
    "Ask a question",
    on_submit=_handle_user_interaction,
    disabled=st.session_state["waiting_for_answer"]
):
    with st.chat_message("user"):
        st.write(user_prompt)

    with st.chat_message("assistant"):
        with st.spinner():
            time.sleep(5)
        # stream the answer here

if st.session_state["waiting_for_answer"]:
    st.session_state["waiting_for_answer"] = False
    st.rerun()

The callback is needed for disabling the st.chat_input component and any other component you'd like to disable when the user submits a prompt, through setting the waiting_for_answer flag. Then, after printing the response, the flag is set to False again. Unfortunately, streamlit needs another user interaction to re-render the app with the flag disabled, so a st.rerun is needed

vrtornisiello avatar Nov 04 '24 19:11 vrtornisiello

The problem with this last solution is that apparently , the chat text box loses focus every time...

emanjavacas avatar Nov 05 '24 10:11 emanjavacas