A web interface in twenty lines: Gradio, and what launch() actually starts
From a terminal to a page
The chat loop from earlier in this module reads with input() and writes with print(). Replacing those two with a web page is what Gradio (free, pip install gradio) does:
import gradio as gr
def respond(message, history):
messages = [{"role": "system", "content": SYSTEM}]
for turn in history:
messages.append({"role": turn["role"], "content": turn["content"]})
messages.append({"role": "user", "content": message})
return provider.complete(trim(messages, 4000, count))
demo = gr.ChatInterface(respond, title="Notes assistant")
demo.launch()Run it and a browser opens at http://127.0.0.1:7860 with a chat box. ChatInterface calls respond with the new message and the conversation so far, and displays what comes back. The history is kept by Gradio in the browser session; your function is stateless, which is why the messages list is rebuilt every call — and why trim from the chat-loop lesson is still needed.
Everything you already built plugs in unchanged: the provider, the template, the cache, the search.
Streaming
A reply that takes eight seconds to arrive whole feels broken; the same reply streaming feels alive. Make respond a generator and Gradio streams it:
def respond(message, history):
messages = build(message, history)
partial = ""
for piece in provider.stream(messages): # module 6's stream_text, behind the provider
partial += piece
yield partialYield the accumulated text each time, not the fragment — the interface displays what you yield as the whole current reply. The += on a string is fine here; the pieces are few and the response short. For a very long reply, collect in a list and "".join as module 6 advised.
Other inputs
ChatInterface is one preset. gr.Interface wraps any function with declared inputs and outputs:
demo = gr.Interface(
fn=search_notes,
inputs=[gr.Textbox(label="Question"), gr.Slider(1, 10, value=5, step=1, label="Results")],
outputs=gr.Markdown(),
)Text boxes, sliders, file uploads, images, audio, dropdowns — each is a component, and the function receives their values as arguments in order. A gr.File input hands you a path; a gr.Image hands you a NumPy array of pixels, which module 8 taught you to read. gr.Blocks lets you lay out several components and wire them by hand when the presets do not fit.
What launch() starts
launch() starts a web server — FastAPI and uvicorn underneath, which the next lesson covers — inside your Python process, on port 7860, listening on 127.0.0.1 only. That last part matters: nobody on your network can reach it. launch(server_name="0.0.0.0") listens on all interfaces, which makes it reachable from a phone on the same Wi-Fi and from anyone else on that network, so do it only where you would be comfortable with that.
The process stays alive serving requests until you stop it. Every request runs your function on a worker; two people sending at once run concurrently, which is fine for a function that calls an API and dangerous for one that writes to a shared file without a lock, per module 6.
Sharing
demo.launch(share=True)prints a https://xxxx.gradio.live link that anyone can open for 72 hours. It works by opening a tunnel from Gradio's servers to your laptop: requests arrive at the public address and are forwarded to the process on your machine. Nothing is uploaded. Which means the link works exactly as long as your laptop is awake and the process is running, and dies when either stops. It is for showing a friend, not for hosting.
Hosting for free
Hugging Face Spaces runs a Gradio app from a repository on free CPU hardware:
- Create a Space, choose Gradio.
- Push
app.pyand arequirements.txt(module 5's pins). - Put the API key in the Space's Secrets settings, never in the file; read it with
os.environas always.
The Space builds, starts, and gives you a permanent public URL. Free Spaces sleep after inactivity and wake on the next visit, which takes a few seconds. A local model through Ollama will not run there — there is no Ollama on a Space — but sentence-transformers and small transformers models do, so the notes search runs entirely on free hardware.
Streamlit is the other common choice and also has free hosting; it re-runs your whole script on every interaction, which is a different mental model, better for dashboards than chat.
Keep the interface thin
app.py should contain the Gradio wiring and nothing else. The provider, the search, the templates and the cache live in your package and are imported, exactly as the notebook lesson said of notebooks. Then the same functions serve the CLI, the web page and next lesson's API, and the tests cover all three.
Try this now
Wrap your chat loop's respond in ChatInterface against Ollama and open it on your phone using server_name="0.0.0.0" and your laptop's IP. Add streaming. Then push the notes search as a Space with a sentence-transformers model and send someone the link.
The one thing to keep
gr.ChatInterface wraps a function that takes a message and history and returns text — or yields it for streaming — and launch() starts a local web server on port 7860; share=True opens a temporary public tunnel, and Hugging Face Spaces hosts the same file for free.
Before you move on
A learner runs `demo.launch(share=True)`, sends the printed `*.gradio.live` link to a friend, and closes the laptop. The friend reports the link is dead. What happened?
Pick the one you would defend. Nobody sees your answer.