DuoChat Codelab Part 5: Wrapping it up¶
In this section, we'll create a multi-column layout for different model responses, implement user and model message components, add auto-scroll functionality, and finalize the chat experience with multiple models.
Creating a New Conversation Page¶
First, let's create a new page for our conversations. Update the main.py
file to include a new route and function for the conversation page:
import mesop as me
from data_model import State, Models, ModelDialogState, Conversation, ChatMessage
from dialog import dialog, dialog_actions
import claude
import gemini
# ... (keep the existing imports and styles)
STYLESHEETS = [
"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
]
@me.page(
path="/",
stylesheets=STYLESHEETS,
)
def home_page():
model_picker_dialog()
with me.box(style=ROOT_BOX_STYLE):
header()
with me.box(
style=me.Style(
width="min(680px, 100%)",
margin=me.Margin.symmetric(horizontal="auto", vertical=36),
)
):
me.text(
"Chat with multiple models at once",
style=me.Style(font_size=20, margin=me.Margin(bottom=24)),
)
# Uncomment this in the next step:
# examples_row()
chat_input()
@me.page(path="/conversation", stylesheets=STYLESHEETS)
def conversation_page():
state = me.state(State)
model_picker_dialog()
with me.box(style=ROOT_BOX_STYLE):
header()
models = len(state.conversations)
models_px = models * 680
with me.box(
style=me.Style(
width=f"min({models_px}px, calc(100% - 32px))",
display="grid",
gap=16,
grid_template_columns=f"repeat({models}, 1fr)",
flex_grow=1,
overflow_y="hidden",
margin=me.Margin.symmetric(horizontal="auto"),
padding=me.Padding.symmetric(horizontal=16),
)
):
for conversation in state.conversations:
model = conversation.model
messages = conversation.messages
with me.box(
style=me.Style(
overflow_y="auto",
)
):
me.text("Model: " + model, style=me.Style(font_weight=500))
for message in messages:
if message.role == "user":
user_message(message.content)
else:
model_message(message)
if messages and model == state.conversations[-1].model:
me.box(
key="end_of_messages",
style=me.Style(
margin=me.Margin(
bottom="50vh" if messages[-1].in_progress else 0
)
),
)
with me.box(
style=me.Style(
display="flex",
justify_content="center",
)
):
with me.box(
style=me.Style(
width="min(680px, 100%)",
padding=me.Padding(top=24, bottom=24),
)
):
chat_input()
def user_message(content: str):
with me.box(
style=me.Style(
background="#e7f2ff",
padding=me.Padding.all(16),
margin=me.Margin.symmetric(vertical=16),
border_radius=16,
)
):
me.text(content)
def model_message(message: ChatMessage):
with me.box(
style=me.Style(
background="#fff",
padding=me.Padding.all(16),
border_radius=16,
margin=me.Margin.symmetric(vertical=16),
)
):
me.markdown(message.content)
if message.in_progress:
me.progress_spinner()
# ... (keep the existing helper functions)
def send_prompt(e: me.ClickEvent):
state = me.state(State)
if not state.conversations:
me.navigate("/conversation")
for model in state.models:
state.conversations.append(Conversation(model=model, messages=[]))
input = state.input
state.input = ""
for conversation in state.conversations:
model = conversation.model
messages = conversation.messages
history = messages[:]
messages.append(ChatMessage(role="user", content=input))
messages.append(ChatMessage(role="model", in_progress=True))
yield
me.scroll_into_view(key="end_of_messages")
if model == Models.GEMINI_1_5_FLASH.value:
llm_response = gemini.send_prompt_flash(input, history)
elif model == Models.GEMINI_1_5_PRO.value:
llm_response = gemini.send_prompt_pro(input, history)
elif model == Models.CLAUDE_3_5_SONNET.value:
llm_response = claude.call_claude_sonnet(input, history)
else:
raise Exception("Unhandled model", model)
for chunk in llm_response:
messages[-1].content += chunk
yield
messages[-1].in_progress = False
yield
Try running the app: mesop main.py
and now you should navigate to the conversation page once you click the send button.
Adding Example Prompts¶
Let's add some example prompts to the home page to help users get started. Add the following functions to main.py
:
EXAMPLES = [
"Create a file-lock in Python",
"Write an email to Congress to have free milk for all",
"Make a nice box shadow in CSS",
]
def examples_row():
with me.box(
style=me.Style(
display="flex", flex_direction="row", gap=16, margin=me.Margin(bottom=24)
)
):
for i in EXAMPLES:
example(i)
def example(text: str):
with me.box(
key=text,
on_click=click_example,
style=me.Style(
cursor="pointer",
background="#b9e1ff",
width="215px",
height=160,
font_weight=500,
line_height="1.5",
padding=me.Padding.all(16),
border_radius=16,
border=me.Border.all(me.BorderSide(width=1, color="blue", style="none")),
),
):
me.text(text)
def click_example(e: me.ClickEvent):
state = me.state(State)
state.input = e.key
And then uncomment the callsite for examples_row
in home_page
.
Updating the Header¶
Let's update header
to allow users to return to the home page when they click on the header box:
def header():
def navigate_home(e: me.ClickEvent):
me.navigate("/")
state = me.state(State)
state.conversations = []
with me.box(
on_click=navigate_home,
style=me.Style(
cursor="pointer",
padding=me.Padding.all(16),
),
):
me.text(
"DuoChat",
style=me.Style(
font_weight=500,
font_size=24,
color="#3D3929",
letter_spacing="0.3px",
),
)
Finalizing the Chat Experience¶
Now that we have a dedicated conversation page with a multi-column layout, let's make some final improvements to enhance the user experience:
- Navigate to the conversations page at the start of a conversation.
- Implement auto-scrolling to keep the latest messages in view.
Update the send_prompt
function in main.py
:
def send_prompt(e: me.ClickEvent):
state = me.state(State)
if not state.conversations:
me.navigate("/conversation")
for model in state.models:
state.conversations.append(Conversation(model=model, messages=[]))
input = state.input
state.input = ""
for conversation in state.conversations:
model = conversation.model
messages = conversation.messages
history = messages[:]
messages.append(ChatMessage(role="user", content=input))
messages.append(ChatMessage(role="model", in_progress=True))
yield
me.scroll_into_view(key="end_of_messages")
if model == Models.GEMINI_1_5_FLASH.value:
llm_response = gemini.send_prompt_flash(input, history)
elif model == Models.GEMINI_1_5_PRO.value:
llm_response = gemini.send_prompt_pro(input, history)
elif model == Models.CLAUDE_3_5_SONNET.value:
llm_response = claude.call_claude_sonnet(input, history)
else:
raise Exception("Unhandled model", model)
for chunk in llm_response:
messages[-1].content += chunk
yield
messages[-1].in_progress = False
yield
Running the Final Application¶
Run the application with mesop main.py
and navigate to http://localhost:32123
. You should now have a fully functional DuoChat application with the following features:
- A home page with example prompts and a model picker.
- A conversation page with a multi-column layout for different model responses.
- Real-time streaming of model responses with loading indicators.
- Auto-scrolling to keep the latest message in view.
Troubleshooting¶
If you're having trouble, compare your code to the solution.
Conclusion¶
Congratulations! You've successfully built DuoChat, a Mesop application that allows users to interact with multiple AI models simultaneously. This project demonstrates Mesop's capabilities for creating responsive UIs, managing complex state, and integrating with external APIs.
Some potential next steps to further improve the application:
- Deploy your app.
- Add the ability to save and load conversations.
- Add support for additional AI models or services.
Feel free to experiment with these ideas or come up with your own improvements to enhance the DuoChat experience!