Skip to content

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:

main.py
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:

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:

main.py
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:

  1. Navigate to the conversations page at the start of a conversation.
  2. Implement auto-scrolling to keep the latest messages in view.

Update the send_prompt function in main.py:

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:

  1. A home page with example prompts and a model picker.
  2. A conversation page with a multi-column layout for different model responses.
  3. Real-time streaming of model responses with loading indicators.
  4. 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:

  1. Deploy your app.
  2. Add the ability to save and load conversations.
  3. 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!