Module 15

UI Frameworks for Enterprise AI Apps

⏱ ~4.5 hours ❓ 12-question quiz 🎯 Unlock Module 16

1. Framework Selection Guide

FrameworkBest ForStreamingAuthCustom UI
StreamlitInternal tools, prototypes, data appsst.write_stream()Limited (streamlit-authenticator)Low
ChainlitProduction chat apps, agent UIsNative SSEBuilt-in OAuthMedium
GradioML demos, model showcasesGenerator functionsHugging Face SpacesLow–Medium
Next.js + Vercel AI SDKEnterprise web apps, full custom UIuseChat / useCompletion hooksNextAuth.js / Auth0Full
HTMXLightweight server-rendered UIsSSE extensionAny backend authMedium
Open WebUISelf-hosted ChatGPT-like interfaceNativeBuilt-in user managementVia plugins

2. Streamlit Chat App

Streamlit provides a simple chat component with native message history. It's ideal for rapid internal tools but runs as a Python server — not suitable for high-concurrency production.

bash
pip install streamlit langchain-openai
streamlit run app.py
app.py — Streamlit streaming chat
import streamlit as st
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

st.title("LangChain Chat")

llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    MessagesPlaceholder("history"),
    ("human", "{question}"),
])
chain = prompt | llm

# Session state persists across Streamlit reruns
if "history" not in st.session_state:
    st.session_state.history = []

# Display chat history
for msg in st.session_state.history:
    role = "user" if isinstance(msg, HumanMessage) else "assistant"
    with st.chat_message(role):
        st.write(msg.content)

# Chat input
if question := st.chat_input("Ask me anything..."):
    with st.chat_message("user"):
        st.write(question)

    with st.chat_message("assistant"):
        # Stream tokens into the chat bubble
        response_text = st.write_stream(
            chain.stream({
                "question": question,
                "history": st.session_state.history,
            })
        )

    # Update history
    st.session_state.history.append(HumanMessage(content=question))
    st.session_state.history.append(AIMessage(content=response_text))
st.write_stream(): Introduced in Streamlit 1.31+, it accepts any generator and streams content directly into the current chat bubble — no manual token accumulation needed.

3. Chainlit Production Chat App

Chainlit is purpose-built for LLM applications — it provides streaming, file uploads, step visualisation (showing agent thinking), OAuth, and user session management out of the box.

bash
pip install chainlit langchain-openai
chainlit run app.py -w
app.py — Chainlit with LangGraph agent
import chainlit as cl
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage

@tool
def search_kb(query: str) -> str:
    """Search the knowledge base."""
    return f"Found: {query} is explained in the documentation."

llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
agent = create_react_agent(llm, tools=[search_kb])

@cl.on_chat_start
async def start():
    cl.user_session.set("history", [])
    await cl.Message(content="Hello! I'm your AI assistant. How can I help?").send()

@cl.on_message
async def handle_message(message: cl.Message):
    history = cl.user_session.get("history", [])
    msg = cl.Message(content="")

    # Stream agent response token by token
    async for chunk, metadata in agent.astream(
        {"messages": history + [HumanMessage(content=message.content)]},
        stream_mode="messages",
    ):
        if hasattr(chunk, "content") and chunk.content:
            await msg.stream_token(chunk.content)
        elif hasattr(chunk, "tool_calls") and chunk.tool_calls:
            for tc in chunk.tool_calls:
                async with cl.Step(name=f"Tool: {tc['name']}") as step:
                    step.input = str(tc["args"])

    await msg.send()
    cl.user_session.set(
        "history",
        history + [HumanMessage(content=message.content)],
    )
chainlit.md — welcome message
# Welcome to My AI Assistant 🤖

This assistant can:
- Answer questions about our product
- Search the knowledge base
- Help with technical support

Ask me anything!

4. Gradio Chat Interface

Gradio's ChatInterface component provides a chat UI in ~10 lines. It can be deployed to Hugging Face Spaces for free.

gradio_app.py
import gradio as gr
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    MessagesPlaceholder("history"),
    ("human", "{question}"),
])
chain = prompt | llm

def chat(message: str, gradio_history: list) -> str:
    # Convert Gradio history format to LangChain messages
    lc_history = []
    for human, ai in gradio_history:
        lc_history.append(HumanMessage(content=human))
        lc_history.append(AIMessage(content=ai))

    # Stream response
    full_response = ""
    for chunk in chain.stream({"question": message, "history": lc_history}):
        full_response += chunk.content
        yield full_response   # Gradio expects generator for streaming

demo = gr.ChatInterface(
    fn=chat,
    title="AI Assistant",
    description="Powered by LangChain + GPT-4o-mini",
    examples=["What is RAG?", "Explain LangGraph", "How does LangSmith work?"],
    type="messages",
)

if __name__ == "__main__":
    demo.launch(share=False, server_port=7860)

5. Next.js + Vercel AI SDK

For enterprise-grade custom UIs, Next.js with the Vercel AI SDK provides React hooks that handle streaming, message state, and loading indicators — connecting to your LangServe backend.

bash
npx create-next-app@latest my-ai-app --typescript --tailwind
cd my-ai-app
npm install ai @langchain/core
app/api/chat/route.ts — Next.js API route
import { StreamingTextResponse } from "ai";
import { RemoteRunnable } from "@langchain/core/runnables/remote";

const chain = new RemoteRunnable({
  url: process.env.LANGSERVE_URL + "/chat",
});

export async function POST(req: Request) {
  const { messages } = await req.json();
  const lastMessage = messages[messages.length - 1];

  const stream = await chain.stream({ question: lastMessage.content });

  // Convert LangChain stream to Vercel AI SDK ReadableStream
  const readableStream = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        if (chunk && typeof chunk === "string") {
          controller.enqueue(new TextEncoder().encode(chunk));
        }
      }
      controller.close();
    },
  });

  return new StreamingTextResponse(readableStream);
}
app/page.tsx — useChat hook
"use client";
import { useChat } from "ai/react";

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({ api: "/api/chat" });

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">AI Assistant</h1>

      {/* Message list */}
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={`p-3 rounded-lg ${
              m.role === "user"
                ? "bg-blue-100 ml-8"
                : "bg-gray-100 mr-8"
            }`}
          >
            <span className="font-semibold">
              {m.role === "user" ? "You" : "AI"}:
            </span>{" "}
            {m.content}
          </div>
        ))}
        {isLoading && (
          <div className="bg-gray-100 p-3 rounded-lg mr-8 animate-pulse">
            Thinking...
          </div>
        )}
      </div>

      {/* Input form */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask me anything..."
          className="flex-1 border rounded-lg p-2"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="bg-blue-500 text-white px-4 py-2 rounded-lg disabled:opacity-50"
        >
          Send
        </button>
      </form>
    </div>
  );
}
useChat hooks: The Vercel AI SDK's useChat hook manages message state, loading state, and streaming automatically. Tokens appear in the UI as they arrive — no manual SSE parsing required.

6. HTMX + FastAPI Streaming Chat

HTMX enables server-side streaming into existing HTML pages without a JavaScript framework — ideal for teams with server-rendering skills.

htmx_server.py
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, StreamingResponse
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

app = FastAPI()
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
chain = (
    ChatPromptTemplate.from_messages([("human", "{q}")])
    | llm
    | StrOutputParser()
)

@app.get("/", response_class=HTMLResponse)
def index():
    return """
    <html>
    <head>
      <script src="https://unpkg.com/htmx.org@1.9.12"></script>
      <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js"></script>
    </head>
    <body>
      <form hx-post="/ask" hx-target="#response" hx-swap="innerHTML">
        <input name="q" placeholder="Ask a question..." />
        <button type="submit">Ask</button>
      </form>
      <div id="response"></div>
    </body>
    </html>
    """

async def sse_generator(question: str):
    async for token in chain.astream({"q": question}):
        if token:
            # HTMX SSE swap appends each token to the target div
            yield f"event: message\ndata: <span>{token}</span>\n\n"
    yield "event: message\ndata: <hr/>\n\n"

@app.post("/ask")
async def ask(q: str = Form(...)):
    # Return SSE stream that HTMX sse-swap extension processes
    return StreamingResponse(
        sse_generator(q),
        media_type="text/event-stream",
    )
HTMX SSE extension: The htmx.org/ext/sse.js extension lets HTMX swap HTML fragments streamed via SSE — each data: event replaces or appends to the target element, enabling progressive rendering without JavaScript.

7. Open WebUI — Self-Hosted ChatGPT Interface

Open WebUI is a feature-complete self-hosted web interface compatible with OpenAI-format APIs. Connect your LangServe backend by configuring it as a custom model endpoint.

docker-compose.yml — Open WebUI
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    ports:
      - "3000:8080"
    environment:
      OPENAI_API_BASE_URL: http://langserve-app:8000/chat   # your LangServe endpoint
      OPENAI_API_KEY: dummy-key                              # required but unused if your API is open
      WEBUI_SECRET_KEY: change_me_in_production
    volumes:
      - open-webui-data:/app/backend/data

  langserve-app:
    build: ./langserve-app
    ports:
      - "8000:8000"
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY}

volumes:
  open-webui-data:
LangServe as OpenAI-compatible API: To use Open WebUI with LangServe, wrap your LangServe endpoint in an OpenAI-compatible adapter that speaks the /v1/chat/completions format. Libraries like litellm or a custom FastAPI wrapper can bridge the two formats.

8. Choosing the Right Stack

decision guide
Is this an internal tool or demo?
├── YES → Streamlit (fastest to build, Python-only team)
│         Gradio (ML demo, HuggingFace deployment)
└── NO (production app)
    ├── Need full custom UI / enterprise auth / design system?
    │   └── Next.js + Vercel AI SDK + LangServe backend
    ├── Need chat UI fast with built-in auth and agent step visualisation?
    │   └── Chainlit
    ├── Server-rendered with minimal JS (Django/Flask team)?
    │   └── HTMX + FastAPI SSE + LangChain
    └── Self-hosted ChatGPT replacement for internal users?
        └── Open WebUI + LangServe or Ollama backend
Team SizeTimelineRecommended Stack
Solo developerDaysStreamlit or Chainlit
Small team (2–5)WeeksChainlit + LangServe
Product team (5+)MonthsNext.js + Vercel AI SDK + LangServe
Enterprise (internal)Self-serviceOpen WebUI + LangServe

📝 Knowledge Check

Module 15 — Quiz

Score 80% or higher (10 out of 12) to unlock Module 16.

0 of 12 answered