Day 75 of 80

UI Polish & User Experience

Phase 8: Deployment & UI

What You'll Build Today

You have spent the last 74 days building incredible logic. You can scrape websites, call LLMs, and manage databases. But let's be honest: if you showed your app to a friend right now, they might be confused. They might click a button and think the app crashed because it takes five seconds to respond. They might upload the wrong file type and see a terrifying wall of red code text.

Today, we stop thinking like engineers and start thinking like Product Designers. We are going to take a raw, functional script and turn it into a polished, professional application.

Here is what you will master today:

* Loading States & Spinners: Because a frozen screen makes users think your app is broken. We need to tell them, "Hold on, I'm working on it."

* Graceful Error Handling: Because seeing KeyError: 'choices' is scary. Seeing "Please enter a valid API key" is helpful.

* Layout & Responsiveness: Because 50% of web traffic is mobile. We need your app to look good on a phone.

* User Feedback Loops: Because you cannot fix what you don't know is broken. We will build a system for users to tell you when the AI hallucinates.

The Problem

Let's look at a typical "Day 40" app. It works, but using it is a stressful experience.

Imagine you have an app that summarizes long PDFs. The user uploads a file and clicks "Summarize." The AI takes about 10 seconds to process this.

Here is the code, and here is the pain:

import streamlit as st

import time

st.title("The Unfriendly PDF Summarizer")

# Simulate a backend function that takes time and might fail

def heavy_computation(user_input):

time.sleep(5) # Simulates LLM processing time

if user_input == "crash":

# Simulates an API error or bad input

raise ValueError("Connection reset by peer")

return f"Here is the summary for: {user_input}"

user_input = st.text_input("Enter text to summarize (type 'crash' to break it)")

if st.button("Summarize"):

# PAIN POINT 1: The UI freezes completely here. # The user clicks the button, it stays depressed, and nothing moves. # They usually click it 5 more times thinking they missed.

result = heavy_computation(user_input)

st.write(result)

Why this fails:
  • The "Is it working?" Anxiety: When time.sleep(5) runs, the browser tab freezes. The user has zero feedback. In the modern web, 5 seconds of silence feels like an eternity.
  • The Red Wall of Death: If you type "crash" into that box, Streamlit spits out a massive "Traceback" error message with file paths and line numbers. This reveals your internal code structure (a security risk) and makes the user feel like they broke the computer.
  • Visual Clutter: Everything is stacked vertically. On a wide monitor, your text input stretches across the whole screen, looking empty and unbalanced.
  • There has to be a way to make the app communicate with the user, rather than just processing at them.

    Let's Build It

    We are going to refactor this app step-by-step. We will add a sidebar, a loading spinner, specific error handling, and a feedback mechanism.

    Step 1: The Layout (Mobile Friendly)

    First, let's organize the screen. Streamlit processes code from top to bottom. If we want controls (like inputs and buttons) to be distinct from outputs (results), we should use the Sidebar and Columns.

    The Sidebar is excellent for mobile because it collapses into a "hamburger" menu (the three lines icon) automatically.

    import streamlit as st
    

    import time

    st.set_page_config(page_title="Polished AI App", page_icon="✨")

    st.title("✨ Professional AI Assistant")

    # Step 1: Move inputs to the sidebar # This keeps the main area clean for results

    with st.sidebar:

    st.header("Configuration")

    user_input = st.text_input("Enter your topic:")

    mode = st.selectbox("Complexity", ["Simple", "Advanced"])

    # A primary button stands out more

    generate_btn = st.button("Generate Content", type="primary")

    # Main area placeholder

    st.info("πŸ‘ˆ Configure your settings in the sidebar to get started.")

    Step 2: Managing the Wait (Spinners)

    Now, let's handle that 5-second freeze. Streamlit provides st.spinner(). This is a context manager (using with) that shows a running animation while the code inside the block executes.

    We will also use a "Status Container" for multi-step processes.

    # ... (Keep imports and config from Step 1)
    
    

    def simulate_ai_work():

    """Simulates a slow AI process with stages."""

    time.sleep(1) # Simulating network connection

    time.sleep(2) # Simulating generation

    return "The AI has successfully generated your content based on the parameters."

    if generate_btn:

    # Clear the main area info message

    st.empty()

    # Create two columns for the result area # On mobile, these will stack vertically automatically

    col1, col2 = st.columns([2, 1])

    with col1:

    # The Spinner

    with st.spinner('Contacting AI Brain...'):

    response = simulate_ai_work()

    st.success("Generation Complete!")

    st.write(response)

    with col2:

    st.caption("Metadata")

    st.metric(label="Processing Time", value="3.2s")

    Step 3: Catching Errors Gracefully

    Users will inevitably enter bad data or your API will time out. You must catch these errors. Instead of letting Python crash, we use try and except blocks to catch the crash and display a friendly st.error message.

    Let's modify the logic to force a crash and handle it.

    def risky_ai_work(text):
    

    time.sleep(2)

    if not text:

    raise ValueError("Input cannot be empty")

    if text.lower() == "error":

    raise ConnectionError("API Connection Failed")

    return f"Analysis of '{text}' is complete."

    if generate_btn:

    try:

    with st.spinner("Analyzing..."):

    # This is where the code might break

    result = risky_ai_work(user_input)

    # If we get here, no error happened

    st.subheader("Result")

    st.write(result)

    except ValueError as e:

    # Handle specific "bad input" errors

    st.warning(f"⚠️ Please check your input: {e}")

    except ConnectionError as e:

    # Handle "system broken" errors

    st.error(f"🚨 Network Error: {e}. Please try again later.")

    except Exception as e:

    # Catch-all for anything else we didn't predict

    st.error("An unexpected error occurred. Support has been notified.")

    # In a real app, you would log 'e' to a file here

    Step 4: The Feedback Loop

    You need to know if your users are happy. Let's add a simple feedback mechanism that appears only after a result is generated. We will store this in a simple session state list for now (in a real app, this goes to a database).

    # Initialize session state for feedback if it doesn't exist
    

    if "feedback_log" not in st.session_state:

    st.session_state.feedback_log = []

    # ... inside the 'try' block, after displaying results ...

    st.divider() # Adds a visual separator

    st.write("Was this helpful?")

    # We use columns for small buttons side-by-side

    f_col1, f_col2, f_col3 = st.columns([1,1,4])

    with f_col1:

    if st.button("πŸ‘"):

    st.session_state.feedback_log.append({"input": user_input, "rating": "up"})

    st.toast("Thanks for the positive feedback!")

    with f_col2:

    if st.button("πŸ‘Ž"):

    st.session_state.feedback_log.append({"input": user_input, "rating": "down"})

    st.toast("Sorry about that! We'll improve.")

    # Debug section (usually hidden, but useful for you to see)

    with st.expander("View Feedback Log (Admin)"):

    st.write(st.session_state.feedback_log)

    The Final Polished Code

    Here is the complete, runnable application combining all these concepts.

    import streamlit as st
    

    import time

    # 1. Page Config - This must be the first Streamlit command

    st.set_page_config(

    page_title="Day 75: Polished App",

    page_icon="✨",

    layout="wide"

    )

    # Initialize feedback history

    if "feedback_stats" not in st.session_state:

    st.session_state.feedback_stats = {"thumbs_up": 0, "thumbs_down": 0}

    def simulated_llm_call(prompt):

    """Simulates a backend process that can fail."""

    time.sleep(2) # Fake processing time

    if not prompt:

    raise ValueError("The prompt is empty.")

    if "fail" in prompt.lower():

    raise RuntimeError("Simulated API Connection Timeout")

    return f"AI Response to: '{prompt}'\n\nThis is a polished, professional response generated by the system."

    # --- UI LAYOUT ---

    st.title("✨ Enterprise AI Interface")

    st.markdown("This interface demonstrates loading states, error handling, and feedback loops.")

    # Sidebar for inputs

    with st.sidebar:

    st.header("Input Parameters")

    user_prompt = st.text_area("Enter your prompt:", height=150, help="Type 'fail' to test error handling.")

    st.markdown("---")

    run_btn = st.button("Generate Response", type="primary", use_container_width=True)

    # Display stats in sidebar

    st.markdown("### Session Stats")

    st.metric("Positive Feedback", st.session_state.feedback_stats["thumbs_up"])

    # Main Logic

    if run_btn:

    # Create a placeholder for the result

    result_container = st.container()

    # ERROR HANDLING WRAPPER

    try:

    # LOADING STATE

    with st.spinner("Processing your request..."):

    response_text = simulated_llm_call(user_prompt)

    # SUCCESS STATE

    with result_container:

    st.success("Generation Complete")

    st.markdown(f"### Result\n{response_text}")

    st.markdown("---")

    st.caption("Please rate this response:")

    # FEEDBACK LOOP

    col1, col2, _ = st.columns([1, 1, 8])

    with col1:

    if st.button("πŸ‘ Like", key="like_btn"):

    st.session_state.feedback_stats["thumbs_up"] += 1

    st.toast("Feedback recorded!", icon="βœ…")

    with col2:

    if st.button("πŸ‘Ž Dislike", key="dislike_btn"):

    st.session_state.feedback_stats["thumbs_down"] += 1

    st.toast("Thanks, we will improve.", icon="πŸ“")

    except ValueError as e:

    # User Error (Bad Input)

    st.warning(f"⚠️ Input Error: {str(e)}")

    except RuntimeError as e:

    # System Error (API Down)

    st.error(f"🚨 System Error: {str(e)}")

    st.info("Tip: Check your internet connection or API credits.")

    except Exception as e:

    # Unexpected Error

    st.error("An unexpected error occurred.")

    with st.expander("See technical details"):

    st.write(e)

    else:

    # Empty State

    st.info("πŸ‘ˆ Enter a prompt in the sidebar to begin.")

    Now You Try

    You have the basics of a professional UI. Now, extend the project with these three tasks:

  • The "Status" Container:
  • Instead of a simple spinner, use st.status. This is a newer Streamlit feature that shows a list of steps completing (e.g., "Downloading PDF...", "Extracting Text...", "Summarizing...").

    Hint:* Look up st.status in the Streamlit documentation. It allows you to update labels as code runs.
  • Conditional Disabling:
  • Currently, the user can click "Generate" even if the text box is empty, triggering an error.

    Task:* Use the disabled parameter on the button. If user_prompt is empty, the button should be greyed out. Hint:* You might need to check the length of the string before defining the button.
  • Persist the Feedback:
  • Right now, if you refresh the page, the feedback stats disappear.

    Task:* When a user clicks thumbs up/down, append the prompt and the rating to a file named feedback.csv. Hint:* Use Python's built-in csv module or pandas. Open the file in append mode ('a').

    Challenge Project: The "Mom Test" (User Testing)

    Your code works for you because you wrote it. You know exactly what to type to make it work and what to avoid to keep it from crashing. Real users do not know this.

    The Goal: Identify 3 friction points in your current deployed app. The Requirements:
  • Find 3 people (friends, family, or colleagues). They do not need to be technical.
  • Give them your app URL and a vague goal (e.g., "Use this to summarize a news article").
  • Do not help them. Sit on your hands. Watch them.
  • Write down every time they:
  • * Stop and look confused.

    * Click something that isn't clickable.

    * Ask "What do I do now?"

    * Encounter an error.

    The Output:

    Create a simple text file usability_report.txt with your findings and your plan to fix them.

    Example Entry:

    > User: Sarah

    > Confusion: She tried to upload a Word doc, but the app crashed because it only accepts PDFs.

    > Fix: Add type=["pdf"] to the file uploader and add a try/except block to catch wrong file types.

    What You Learned

    Today, you moved from "making it work" to "making it usable."

    * Loading Indicators: You learned that st.spinner manages user anxiety during long waits.

    * Error Handling: You replaced raw Python tracebacks with friendly st.error and st.warning messages using try/except blocks.

    * Layout: You utilized st.sidebar and st.columns to create a hierarchy of information that works on mobile.

    * Feedback: You implemented a way to listen to your users.

    Why This Matters:

    In the Capstone project starting tomorrow, you will be building a full-stack AI application. If the UI is clunky or breaks easily, it doesn't matter how smart your AI model isβ€”people won't use it. Polish builds trust.

    Phase 8 Complete!

    You have deployed apps, managed secrets, and now polished the UI. You are ready.

    Tomorrow: The Capstone Project begins. Everything you have learned in 75 days comes together. Get some rest.