Skip to content

Commit 34c1285

Browse files
Release 1.31.0 (#951)
* Docs for chat improvements in 1.31.0 (#948) * Update ChatGPT-like exampel with write_stream * Update streamlit.json * Add write_stream page * Use write_stream in chat tutorial * Improve readability of chat tutorial code * Refcard and cheat sheet for chat improvements * Update chat.input-inline.py * Update streamlit.json * Add st.page_link (#942) * Add st.page_link example source * Add st.page_link page * Update streamlit.json * Update st.page_link docstring * Rename files * Update streamlit.json * RefCards for st.page_link * Update api-cheat-sheet.md * What's new, changelog, embedded apps version for 1.31.0 * Nit for clarity Co-authored-by: Snehan Kekre <[email protected]> * Add page description for st.page_link Co-authored-by: Snehan Kekre <[email protected]> * Add custom navigation tutorial (#952) * Move and rename files to create new category * Add custom navigation tutorial * Edit for clarity * Add tip with link to tutorial * Changelog video and legal edit --------- Co-authored-by: Snehan Kekre <[email protected]>
1 parent 5d6e0b2 commit 34c1285

30 files changed

+8798
-175
lines changed

content/kb/tutorials/chat.md

+49-52
Original file line numberDiff line numberDiff line change
@@ -223,29 +223,31 @@ if prompt := st.chat_input("What is up?"):
223223

224224
The only difference so far is we've changed the title of our app and added imports for `random` and `time`. We'll use `random` to randomly select a response from a list of responses and `time` to add a delay to simulate the chatbot "thinking" before responding.
225225

226-
All that's left to do is add the chatbot's responses within the `if` block. We'll use a list of responses and randomly select one to display. We'll also add a delay to simulate the chatbot "thinking" before responding (or stream its response).
226+
All that's left to do is add the chatbot's responses within the `if` block. We'll use a list of responses and randomly select one to display. We'll also add a delay to simulate the chatbot "thinking" before responding (or stream its response). Let's make a helper function for this and insert it at the top of our app.
227227

228228
```python
229-
# Display assistant response in chat message container
230-
with st.chat_message("assistant"):
231-
message_placeholder = st.empty()
232-
full_response = ""
233-
assistant_response = random.choice(
229+
# Streamed response emulator
230+
def response_generator():
231+
response = random.choice(
234232
[
235233
"Hello there! How can I assist you today?",
236234
"Hi, human! Is there anything I can help you with?",
237235
"Do you need help?",
238236
]
239237
)
240-
# Simulate stream of response with milliseconds delay
241-
for chunk in assistant_response.split():
242-
full_response += chunk + " "
238+
for word in response.split():
239+
yield word + " "
243240
time.sleep(0.05)
244-
# Add a blinking cursor to simulate typing
245-
message_placeholder.markdown(full_response + "")
246-
message_placeholder.markdown(full_response)
241+
```
242+
243+
Back to writing the response in our chat interface, we'll use `st.write_stream` to write out the streamed response with a typewriter effect.
244+
245+
```python
246+
# Display assistant response in chat message container
247+
with st.chat_message("assistant"):
248+
response = st.write_stream(response_generator())
247249
# Add assistant response to chat history
248-
st.session_state.messages.append({"role": "assistant", "content": full_response})
250+
st.session_state.messages.append({"role": "assistant", "content": response})
249251
```
250252

251253
Above, we've added a placeholder to display the chatbot's response. We've also added a for loop to iterate through the response and display it one word at a time. We've added a delay of 0.05 seconds between each word to simulate the chatbot "thinking" before responding. Finally, we append the chatbot's response to the chat history. As you've probably guessed, this is a naive implementation of streaming. We'll see how to implement streaming with OpenAI in the [next section](#build-a-chatgpt-like-app).
@@ -259,6 +261,21 @@ import streamlit as st
259261
import random
260262
import time
261263

264+
265+
# Streamed response emulator
266+
def response_generator():
267+
response = random.choice(
268+
[
269+
"Hello there! How can I assist you today?",
270+
"Hi, human! Is there anything I can help you with?",
271+
"Do you need help?",
272+
]
273+
)
274+
for word in response.split():
275+
yield word + " "
276+
time.sleep(0.05)
277+
278+
262279
st.title("Simple chat")
263280

264281
# Initialize chat history
@@ -280,24 +297,9 @@ if prompt := st.chat_input("What is up?"):
280297

281298
# Display assistant response in chat message container
282299
with st.chat_message("assistant"):
283-
message_placeholder = st.empty()
284-
full_response = ""
285-
assistant_response = random.choice(
286-
[
287-
"Hello there! How can I assist you today?",
288-
"Hi, human! Is there anything I can help you with?",
289-
"Do you need help?",
290-
]
291-
)
292-
# Simulate stream of response with milliseconds delay
293-
for chunk in assistant_response.split():
294-
full_response += chunk + " "
295-
time.sleep(0.05)
296-
# Add a blinking cursor to simulate typing
297-
message_placeholder.markdown(full_response + "")
298-
message_placeholder.markdown(full_response)
300+
response = st.write_stream(response_generator())
299301
# Add assistant response to chat history
300-
st.session_state.messages.append({"role": "assistant", "content": full_response})
302+
st.session_state.messages.append({"role": "assistant", "content": response})
301303
```
302304

303305
</Collapse>
@@ -360,24 +362,23 @@ if prompt := st.chat_input("What is up?"):
360362
# Display user message in chat message container
361363
with st.chat_message("user"):
362364
st.markdown(prompt)
363-
# Display assistant response in chat message container
364-
with st.chat_message("assistant"):
365-
message_placeholder = st.empty()
366-
full_response = ""
367365
```
368366

369-
All that's changed is that we've added a default model to `st.session_state` and set our OpenAI API key from Streamlit secrets. Here's where it gets interesting. We can replace our logic from earlier to emulate streaming predetermined responses with the model's responses from OpenAI:
367+
All that's changed is that we've added a default model to `st.session_state` and set our OpenAI API key from Streamlit secrets. Here's where it gets interesting. We can replace our emulated stream with the model's responses from OpenAI:
370368

371369
```python
372-
for response in client.chat.completions.create(
373-
model=st.session_state["openai_model"],
374-
messages=[{"role": m["role"], "content": m["content"]} for m in st.session_state.messages],
375-
stream=True,
376-
):
377-
full_response += (response.choices[0].delta.content or "")
378-
message_placeholder.markdown(full_response + "")
379-
message_placeholder.markdown(full_response)
380-
st.session_state.messages.append({"role": "assistant", "content": full_response})
370+
# Display assistant response in chat message container
371+
with st.chat_message("assistant"):
372+
stream = client.chat.completions.create(
373+
model=st.session_state["openai_model"],
374+
messages=[
375+
{"role": m["role"], "content": m["content"]}
376+
for m in st.session_state.messages
377+
],
378+
stream=True,
379+
)
380+
response = st.write_stream(stream)
381+
st.session_state.messages.append({"role": "assistant", "content": response})
381382
```
382383

383384
Above, we've replaced the list of responses with a call to [`OpenAI().chat.completions.create`](https://platform.openai.com/docs/guides/text-generation/chat-completions-api). We've set `stream=True` to stream the responses to the frontend. In the API call, we pass the model name we hardcoded in session state and pass the chat history as a list of messages. We also pass the `role` and `content` of each message in the chat history. Finally, OpenAI returns a stream of responses (split into chunks of tokens), which we iterate through and display each chunk.
@@ -410,20 +411,16 @@ if prompt := st.chat_input("What is up?"):
410411
st.markdown(prompt)
411412

412413
with st.chat_message("assistant"):
413-
message_placeholder = st.empty()
414-
full_response = ""
415-
for response in client.chat.completions.create(
414+
stream = client.chat.completions.create(
416415
model=st.session_state["openai_model"],
417416
messages=[
418417
{"role": m["role"], "content": m["content"]}
419418
for m in st.session_state.messages
420419
],
421420
stream=True,
422-
):
423-
full_response += (response.choices[0].delta.content or "")
424-
message_placeholder.markdown(full_response + "")
425-
message_placeholder.markdown(full_response)
426-
st.session_state.messages.append({"role": "assistant", "content": full_response})
421+
)
422+
response = st.write_stream(stream)
423+
st.session_state.messages.append({"role": "assistant", "content": response})
427424
```
428425

429426
<Image src="/images/knowledge-base/chatgpt-clone.gif" clean />

content/library/advanced-features/multipage-apps.md content/library/advanced-features/multipage-apps/_multipage-apps.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: Multipage apps
33
slug: /library/advanced-features/multipage-apps
4+
description: Streamlit provides a simple way to create multipage apps.
45
---
56

67
# Multipage apps
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
title: Create custom navigation menus
3+
slug: /library/advanced-features/multipage-apps/custom-navigation
4+
description: Streamlit makes it easy to build a custom navigation menu in your multipage app.
5+
---
6+
7+
# Create custom navigation menus
8+
9+
Streamlit lets you build custom navigation menus and elements with `st.page_link`. Introduced in Streamlit version 1.31.0, `st.page_link` can link to other pages in your multipage app or to external sites. When linked to another page in your app, `st.page_link` will show a highlight effect to indicate the current page. When combined with the [`client.showSidebarNavigation`](/library/advanced-features/configuration#client) configuration option, you can build sleek, dynamic navigation in your app.
10+
11+
## Prerequisites
12+
13+
Create a new working directory in your development environment. We'll call this directory `your-repository`.
14+
15+
## Summary
16+
17+
In this example, we'll build a dynamic navigation menu for a multipage app that depends on the current user's role. We've abstracted away the use of username and creditials to simplify the example. Instead, we'll use a selectbox on the main page of the app to switch between roles. Session State will carry this selection between pages. The app will have a main page (`app.py`) which serves as the abstracted log-in page. There will be three additional pages which will be hidden or accessible, depending on the current role. The file structure will be as follows:
18+
19+
```
20+
your-repository/
21+
├── .streamlit/
22+
│ └── config.toml
23+
├── pages/
24+
│ ├── admin.py
25+
│ ├── super-admin.py
26+
│ └── user.py
27+
├── menu.py
28+
└── app.py
29+
```
30+
31+
Here's a look at what we'll build:
32+
33+
<Cloud src="https://doc-custom-navigation.streamlit.app/?embed=true" height="400" />
34+
35+
## Build the example
36+
37+
### Hide the default sidebar navigation
38+
39+
When creating a custom navigation menu, you need to hide the default sidebar navigation using `client.showSidebarNavigation`. Add the following `.streamlit/config.toml` file to your working directory:
40+
41+
```toml
42+
[client]
43+
showSidebarNavigation = false
44+
```
45+
46+
### Create a menu function
47+
48+
You can write different menu logic for different pages or you can create a single menu function to call on multiple pages. In this example, we'll use the same menu logic on all pages, including a redirect to the main page when a user isn't logged in. We'll build a few helper functions to do this.
49+
50+
- `menu_with_redirect()` checks if a user is logged in, then either redirects them to the main page or renders the menu.
51+
- `menu()` will call the correct helper function to render the menu based on whether the user is logged in or not.
52+
- `authenticated_menu()` will display a menu based on an authenticated user's role.
53+
- `unauthenticated_menu()` will display a menu for unauthenticated users.
54+
55+
We'll call `menu()` on the main page and call `menu_with_redirect()` on the other pages. `st.session_state.role` will store the current selected role. If this value does not exist or is set to `None`, then the user is not logged in. Otherwise, it will hold the user's role as a string: `"user"`, `"admin"`, or `"super-admin"`.
56+
57+
Add the following `menu.py` file to your working directory. (We'll describe the functions in more detail below.)
58+
59+
```python
60+
import streamlit as st
61+
62+
63+
def authenticated_menu():
64+
# Show a navigation menu for authenticated users
65+
st.sidebar.page_link("app.py", label="Switch accounts")
66+
st.sidebar.page_link("pages/user.py", label="Your profile")
67+
if st.session_state.role in ["admin", "super-admin"]:
68+
st.sidebar.page_link("pages/admin.py", label="Manage users")
69+
st.sidebar.page_link(
70+
"pages/super-admin.py",
71+
label="Manage admin access",
72+
disabled=st.session_state.role != "super-admin",
73+
)
74+
75+
76+
def unauthenticated_menu():
77+
# Show a navigation menu for unauthenticated users
78+
st.sidebar.page_link("app.py", label="Log in")
79+
80+
81+
def menu():
82+
# Determine if a user is logged in or not, then show the correct
83+
# navigation menu
84+
if "role" not in st.session_state or st.session_state.role is None:
85+
unauthenticated_menu()
86+
return
87+
authenticated_menu()
88+
89+
90+
def menu_with_redirect():
91+
# Redirect users to the main page if not logged in, otherwise continue to
92+
# render the navigation menu
93+
if "role" not in st.session_state or st.session_state.role is None:
94+
st.switch_page("app.py")
95+
menu()
96+
```
97+
98+
Let's take a closer look at `authenticated_menu()`. When this function is called, `st.session_state.role` exists and has a value other than `None`.
99+
100+
```python
101+
def authenticated_menu():
102+
# Show a navigation menu for authenticated users
103+
```
104+
105+
The first two pages in the navigation menu are available to all users. Since we know a user is logged in when this function is called, we'll use the label "Switch accounts" for the main page. (If you don't use the `label` parameter, the page name will be derived from the file name like it is with the default sidebar navigation.)
106+
107+
```python
108+
st.sidebar.page_link("app.py", label="Switch accounts")
109+
st.sidebar.page_link("pages/user.py", label="Your profile")
110+
```
111+
112+
We only want to show the next two pages to admins. Furthermore, we've chosen to disable&mdash;but not hide&mdash;the super-admin page when the admin user is not a super-admin. We do this using the `disabled` parameter. (`disabled=True` when the role is not `"super-admin"`.)
113+
114+
```
115+
if st.session_state.role in ["admin", "super-admin"]:
116+
st.sidebar.page_link("pages/admin.py", label="Manage users")
117+
st.sidebar.page_link(
118+
"pages/super-admin.py",
119+
label="Manage admin access",
120+
disabled=st.session_state.role != "super-admin",
121+
)
122+
```
123+
124+
It's that simple! `unauthenticated_menu()` will only show a link to the main page of the app with the label "Log in." `menu()` does a simple inspection of `st.session_state.role` to switch between the two menu-rendering functions. Finally, `menu_with_redirect()` extends `menu()` to redirect users to `app.py` if they aren't logged in.
125+
126+
<Tip>
127+
128+
If you want to include emojis in your page labels, you can use the `icon` parameter. There's no need to include emojis in your file name or the `label` parameter.
129+
130+
</Tip>
131+
132+
### Create the main file of your app
133+
134+
The main `app.py` file will serve as a pseudo-login page. The user can choose a role from the `st.selectbox` widget. A few bits of logic will save that role into Session State to preserve it while navigating between pages&mdash;even when returning to `app.py`.
135+
136+
Add the following `app.py` file to your working directory:
137+
138+
```python
139+
import streamlit as st
140+
from menu import menu
141+
142+
# Initialize st.session_state.role to None
143+
if "role" not in st.session_state:
144+
st.session_state.role = None
145+
146+
# Retrieve the role from Session State to initialize the widget
147+
st.session_state._role = st.session_state.role
148+
149+
def set_role():
150+
# Callback function to save the role selection to Session State
151+
st.session_state.role = st.session_state._role
152+
153+
154+
# Selectbox to choose role
155+
st.selectbox(
156+
"Select your role:",
157+
[None, "user", "admin", "super-admin"],
158+
key="_role",
159+
on_change=set_role,
160+
)
161+
menu() # Render the dynamic menu!
162+
```
163+
164+
### Add other pages to your app
165+
166+
Add the following `pages/user.py` file:
167+
168+
```python
169+
import streamlit as st
170+
from menu import menu_with_redirect
171+
172+
# Redirect to app.py if not logged in, otherwise show the navigation menu
173+
menu_with_redirect()
174+
175+
st.title("This page is available to all users")
176+
st.markdown(f"You are currently logged with the role of {st.session_state.role}.")
177+
```
178+
179+
Session State resets if a user manually navigates to a page by URL. Therefore, if a user tries to access an admin page in this example, Session State will be cleared, and they will be redirected to the main page as an unauthenicated user. However, it's still good practice to include a check of the role at the top of each restricted page. You can use `st.stop` to halt an app if a role is not whitelisted.
180+
181+
`pages/admin.py`:
182+
183+
```python
184+
import streamlit as st
185+
from menu import menu_with_redirect
186+
187+
# Redirect to app.py if not logged in, otherwise show the navigation menu
188+
menu_with_redirect()
189+
190+
# Verify the user's role
191+
if st.session_state.role not in ["admin", "super-admin"]:
192+
st.warning("You do not have permission to view this page.")
193+
st.stop()
194+
195+
st.title("This page is available to all admins")
196+
st.markdown(f"You are currently logged with the role of {st.session_state.role}.")
197+
```
198+
199+
`pages/super-admin.py`:
200+
201+
```python
202+
import streamlit as st
203+
from menu import menu_with_redirect
204+
205+
# Redirect to app.py if not logged in, otherwise show the navigation menu
206+
menu_with_redirect()
207+
208+
# Verify the user's role
209+
if st.session_state.role not in ["super-admin"]:
210+
st.warning("You do not have permission to view this page.")
211+
st.stop()
212+
213+
st.title("This page is available to super-admins")
214+
st.markdown(f"You are currently logged with the role of {st.session_state.role}.")
215+
```
216+
217+
As noted above, the redirect in `menu_with_redirect()` will prevent a user from ever seeing the warning messages on the admin pages. If you want to see the warning, just add another `st.page_link("pages/admin.py")` button at the bottom of `app.py` so you can navigate to the admin page after selecting the "user" role. 😉

0 commit comments

Comments
 (0)