Integrating a Questionnaire DataFrame with PDF Viewer in Streamlit

Hello Streamlit Community,

I’m currently working on a project where we have developed a questionnaire that includes questions, their respective answers, and reference links to PDF documents. The objective is to display this questionnaire on a Streamlit page in an interactive format.

Here’s the layout we’re aiming for:

  • Left Side: A DataFrame representation of the questionnaire. This should include the question, the answer, and a button for each row. When a user clicks the button, it should open the associated PDF on the right side of the page.
  • Right Side: A PDF viewer that displays the content of a PDF when its corresponding button in the DataFrame is pressed. We have already implemented the PDF viewer component.

The challenge I’m facing is integrating the DataFrame buttons with the PDF viewer in such a way that clicking a button updates the PDF display on the right side of the page. Ideally, I’m looking for a solution that allows for seamless interaction between the DataFrame and the PDF viewer.

Specific Questions:

  1. How can I add buttons to a DataFrame in Streamlit that, when clicked, trigger an action (in this case, opening a PDF on the right side of the page)?
  2. What would be the best approach to update the PDF viewer on the right side of the page based on the button clicked in the DataFrame on the left?

I believe this could involve callbacks or session states, but I’m unsure how to implement this effectively in Streamlit. Any guidance, code snippets, or pointers to similar implementations would be greatly appreciated.

Thank you in advance for your assistance!

Hi @LucaVA

Yes, what you intend to create may be resolved via the use of callback function and session state.

Consider the following simple example along with in-line comments for explaining each step:

import streamlit as st

# Initialize the session state variable click for tracking the button click
if 'click' not in st.session_state:
    st.session_state['click'] = False

# Callback function that switches the session state variable 'click' to True upon button click via the `on_click` parameter and callback function
def callback_click():
   st.session_state['click'] = True

# Create the button that makes use of the callback function
st.button('Click Me', on_click=callback_click)

# if/else conditional to evaluate whether the button was clicked or not
if st.session_state['click']:
   st.write('Button has been clicked')
else:
   st.write('Button was not clicked')

Additional examples and code snippets are available in the following Docs pages:

Hope this helps!

2 Likes

Hello @LucaVA,

You can use Streamlit’s layout capabilities to create a custom layout that mimics a DataFrame, where each row is represented by a Streamlit container or column.

import streamlit as st
import pandas as pd

df = pd.DataFrame({
    'Question': ['What is Streamlit?', 'How to add a PDF viewer?'],
    'Answer': ['A framework for ML and data apps.', 'Use st.file_uploader and st.pdf_display.'],
    'PDF_Link': ['link_to_pdf_1.pdf', 'link_to_pdf_2.pdf']
})

# Layout
st.title('Questionnaire')

# Splitting the page into two columns
col1, col2 = st.columns([2, 3])  # Adjust the ratio based on your preference

with col1:
    for index, row in df.iterrows():
        st.text(f"Q: {row['Question']}")
        st.text(f"A: {row['Answer']}")
        if st.button('Open PDF', key=f'btn_{index}'):
            # You can use session state to store the PDF link to be displayed
            st.session_state['pdf_to_display'] = row['PDF_Link']

For the PDF viewer on the right side, you can check if the session state variable for the PDF link is set, and then display the PDF accordingly

with col2:
    if 'pdf_to_display' in st.session_state:
        # Adjust the src attribute to the actual method of accessing your PDF
        st.markdown(f"<iframe src='{st.session_state['pdf_to_display']}' width='100%' height='600'></iframe>", unsafe_allow_html=True)

Hope this helps!

Kind Regards,
Sahir Maharaj
Data Scientist | AI Engineer

P.S. Lets connect on LinkedIn!

➤ Want me to build your solution? Lets chat about how I can assist!
➤ Join my Medium community of 30k readers! Sharing my knowledge about data science and AI
➤ Website: https://sahirmaharaj.com
➤ Email: sahir@sahirmaharaj.com
➤ 100+ FREE Power BI Themes: Download Now

2 Likes

Another approach is to utilize the checkbox column of the data_editor’s column config. We use the data_editor because we allow the user to modify the checkbox.

Here is a sample app.

# Dataframe on the left side.
with cols[0]:
    edited_df = st.data_editor(
        df,
        column_config={
            'Question': st.column_config.TextColumn(
                disabled=True,  # do not allow the user to modify
            ),
            'View': st.column_config.CheckboxColumn(width='small'),
            'Pdf': None,  # hide the column
        },
        use_container_width=True,
        hide_index=False,
        key='pdf',
        on_change=change,
        args=(df,)
    )

Once the users selected a checkbox, it will trigger a pdf display in the right side.

This is done thru a callback in the editor.

def change(df):
    """A callback from data editor."""
    delta = ss.pdf
    
    # Save the index that has true value on the View column.
    # 'edited_rows': {0: {'View': False}, 1: {'View': True}}
    true_index = []
    for k, v in delta['edited_rows'].items():
        for k1, v1 in v.items():
            if k1 == 'View' and v1:
                true_index.append(k)

    # Save the pdf file based on the index.
    # If there are more than 1 true index, set the selected to None to
    # not display any pdf.
    if not true_index or len(true_index) > 1:
        ss.selected_file = None
        return

    ss.selected_file = df.loc[true_index[0], 'Pdf']

If users selected more than 1 checkbox, don’t display any pdf.

You can actually program this such that the last selected checkbox will be displayed and deselect the others. But I am not doing this. The current implementation could be enough.

Full code

Just revise the entries in the data to suit your needs.

import base64

import streamlit as st
from streamlit import session_state as ss
import pandas as pd


st.set_page_config(layout='wide')


if 'selected_file' not in ss:
    ss.selected_file = None


data = {
    'Question': [
        'Name an example of Bradley-Terry Model application?',
        'Describe Split Finding Algorithm',
        'What is solidjs?'
    ],
    'Answer': ['', '', ''],
    'Pdf': [
        './static/pdf/mm-bradley-terry-1079120141.pdf',
        './static/pdf/xgboost_arxiv.pdf',
        './static/pdf/solidjs_intro.pdf'
    ],
    'View': [False, False, False]
}


def display_pdf(file, width=600, height=600):
    try:
        with open(file, "rb") as f:
            base64_pdf = base64.b64encode(f.read()).decode('utf-8')
    except Exception as err:
        st.error(err)
    else:
        pdf_display = F'<embed src="data:application/pdf;base64,{base64_pdf}" width={width} height={height} type="application/pdf">'
        st.markdown(pdf_display, unsafe_allow_html=True)


def change(df):
    """A callback from data editor."""
    delta = ss.pdf
    
    # Save the index that has true value on the View column.
    # 'edited_rows': {0: {'View': False}, 1: {'View': True}}
    true_index = []
    for k, v in delta['edited_rows'].items():
        for k1, v1 in v.items():
            if k1 == 'View' and v1:
                true_index.append(k)

    # Save the pdf file based on the index.
    # If there are more than 1 true index, set the selected to None to
    # not display any pdf.
    if not true_index or len(true_index) > 1:
        ss.selected_file = None
        return

    ss.selected_file = df.loc[true_index[0], 'Pdf']


# Start
df = pd.DataFrame(data)

cols = st.columns([1, 1])

# Dataframe on the left side.
with cols[0]:
    edited_df = st.data_editor(
        df,
        column_config={
            'Question': st.column_config.TextColumn(
                disabled=True,  # do not allow the user to modify
            ),
            'View': st.column_config.CheckboxColumn(width='small'),
            'Pdf': None,  # hide the column
        },
        use_container_width=True,
        hide_index=False,
        key='pdf',
        on_change=change,
        args=(df,)
    )

# pdf view on the right side
with cols[1]:
    if ss.selected_file:
        display_pdf(ss.selected_file, width=700, height=700)
    else:
        st.info('Check a single checkbox under the View column on the dataframe in the left side.')
4 Likes

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