├── .gitignore ├── README.md ├── app.py ├── basic.py ├── components.py ├── designs.py ├── fastapi_better.py ├── fastapi_wrong.py ├── htmxapp.py ├── langchain_stream.py └── llmapp.py /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__ 3 | .sesskey 4 | pirate.png 5 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Overview 2 | 3 | ## Files 4 | 5 | - **app.py** 6 | This file contains a To-Do app developed with FastHTML but without HTMX 7 | 8 | - **htmxapp.py** 9 | This file contains a To-Do app developed with FastHTML but WITH HTMX 10 | 11 | - **basic.py** 12 | This file includes basic examples and experiments with FastHTML, demonstrating fundamental concepts and functionalities of the library. 13 | 14 | - **llmapp.py** 15 | This file implements a simple minichatbot based on a small, specialized model implementation, offering basic conversational capabilities. 16 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app, rt = fast_app() 4 | 5 | tasks = [] 6 | 7 | 8 | @rt("/") 9 | def get(): 10 | add_task_form = Form( 11 | Input(type="text", name="task", placeholder="Add a new task..."), 12 | Button("Add"), 13 | method="post", 14 | action="/add-task", 15 | ) 16 | 17 | task_list = Ul( 18 | *[ 19 | Li( 20 | f"{task} ", 21 | " ", 22 | A("Delete", href=f"/delete/{i}"), 23 | ) 24 | for i, task in enumerate(tasks) 25 | ], 26 | id="task-list", 27 | ) 28 | 29 | return Titled("ToDo App", H1("My Tasks"), add_task_form, task_list) 30 | 31 | 32 | @rt("/add-task", methods=["post"]) 33 | def post(task: str): 34 | if task: 35 | tasks.append(task) 36 | return RedirectResponse(url="/", status_code=303) 37 | 38 | 39 | @rt("/delete/{index}", methods=["get"]) 40 | def delete(index: int): 41 | if 0 <= index < len(tasks): 42 | tasks.pop(index) 43 | return RedirectResponse(url="/", status_code=303) 44 | 45 | 46 | serve() 47 | -------------------------------------------------------------------------------- /basic.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app = FastHTML() 4 | 5 | 6 | def generate_html(): 7 | return Div(H1("Hello, World"), P("Some text"), P("Some more text")) 8 | 9 | 10 | @app.get("/") 11 | def home(): 12 | html_content = [] 13 | for _ in range(5): 14 | html_content.append(generate_html()) 15 | return Div(*html_content) 16 | 17 | 18 | serve() 19 | -------------------------------------------------------------------------------- /components.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import Div, H1, P, Button, Script, Link 2 | 3 | 4 | def with_card(color="blue", border_color="blue", padding="4"): 5 | def decorator(func): 6 | def wrapper(*args, **kwargs): 7 | content = func(*args, **kwargs) 8 | tailwind_classes = f"p-{padding} rounded-lg shadow-lg bg-{color}-100 border border-{border_color}-500" 9 | return Div( 10 | Div( 11 | content, 12 | cls=tailwind_classes, 13 | ), 14 | cls="card border shadow-lg rounded-lg m-4", 15 | ) 16 | 17 | return wrapper 18 | 19 | return decorator 20 | 21 | 22 | def Header(title): 23 | return Div(H1(title, cls="text-2xl font-bold mb-4"), cls="header") 24 | 25 | 26 | def Body(content): 27 | return Div(P(content, cls="text-lg"), cls="body mb-4") 28 | 29 | 30 | def Footer(info): 31 | return Div(P(info, cls="text-sm text-gray-600"), cls="footer") 32 | 33 | 34 | def Page(title, content, footer_info): 35 | return Div( 36 | Header(title), 37 | Body(content), 38 | Footer(footer_info), 39 | cls="page p-6 max-w-xl mx-auto bg-white shadow-lg rounded-lg", 40 | ) 41 | 42 | 43 | def ConditionalCard(content, show_card=True): 44 | if show_card: 45 | return Div(content, cls="conditional-card p-4 bg-blue-100 rounded-lg shadow-md") 46 | return Div( 47 | "Card is hidden", cls="no-card text-red-600 p-4 bg-red-100 rounded-lg shadow-md" 48 | ) 49 | 50 | 51 | class Counter: 52 | def __init__(self): 53 | self.count = 0 54 | 55 | def increment(self): 56 | self.count += 1 57 | return self.render() 58 | 59 | def render(self): 60 | return Div( 61 | P(f"Current count: {self.count}", cls="text-lg font-semibold"), 62 | Button( 63 | "Increment", 64 | hx_post="/increment", 65 | hx_target="#counter-div", 66 | hx_swap="outerHTML", 67 | cls="btn btn-primary mt-2", 68 | ), 69 | id="counter-div", 70 | cls="counter p-4 bg-gray-100 rounded-lg shadow-lg", 71 | ) 72 | 73 | 74 | def Template(content, title="Default Title"): 75 | return Div( 76 | H1(title, cls="text-3xl font-bold mb-4"), 77 | Div(content, cls="template-body p-4 bg-gray-50 rounded-lg shadow-sm"), 78 | cls="template p-6 max-w-xl mx-auto bg-white shadow-lg rounded-lg", 79 | ) 80 | 81 | 82 | def create_alert(alert_type="info"): 83 | alert_classes = { 84 | "info": "bg-blue-100 text-blue-700 border-blue-200", 85 | "warning": "bg-yellow-100 text-yellow-700 border-yellow-200", 86 | "danger": "bg-red-100 text-red-700 border-red-200", 87 | } 88 | 89 | def alert(content): 90 | alert_class = alert_classes.get( 91 | alert_type, "bg-gray-100 text-gray-700 border-gray-200" 92 | ) 93 | return Div(content, cls=f"alert p-4 rounded-lg shadow-md border {alert_class}") 94 | 95 | return alert 96 | -------------------------------------------------------------------------------- /designs.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from components import ( 3 | with_card, 4 | Page, 5 | ConditionalCard, 6 | Counter, 7 | Template, 8 | create_alert, 9 | ) 10 | import random 11 | 12 | tlink = Script(src="https://unpkg.com/tailwindcss-cdn@3.4.3/tailwindcss.js") 13 | dlink = Link( 14 | rel="stylesheet", 15 | href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css", 16 | ) 17 | app, rt = fast_app(hdrs=(tlink, dlink), ws_hdr=True) 18 | 19 | 20 | @with_card(color="lightblue", border_color="blue", padding="10") 21 | def get_card(): 22 | return Div(H1("Hello, World"), P("Some text"), P("Some more text")) 23 | 24 | 25 | @rt("/card") 26 | def get(): 27 | return get_card() 28 | 29 | 30 | @rt("/composition") 31 | def get(): 32 | return Page( 33 | title="Welcome Page", 34 | content="This is the body content.", 35 | footer_info="Footer Information", 36 | ) 37 | 38 | 39 | def get_conditional(show): 40 | return Div( 41 | ConditionalCard("This is a card", show_card=show), 42 | Button( 43 | "Generate Value", 44 | hx_post="/toggle-card", 45 | hx_target="#conditional-container", 46 | hx_swap="outerHTML", 47 | cls="btn btn-primary mt-4", 48 | ), 49 | id="conditional-container", 50 | cls="p-4 bg-gray-100 rounded-lg shadow-lg", 51 | ) 52 | 53 | 54 | @rt("/conditional") 55 | def get(): 56 | return get_conditional(show=False) 57 | 58 | 59 | @rt("/toggle-card", methods=["post"]) 60 | def get(): 61 | random_value = random.random() 62 | show = random_value > 0.5 63 | return get_conditional(show) 64 | 65 | 66 | counter = Counter() 67 | 68 | 69 | @rt("/counter") 70 | def get(): 71 | return counter.render() 72 | 73 | 74 | @rt("/increment", methods=["post"]) 75 | def post_increment(): 76 | return counter.increment() 77 | 78 | 79 | @rt("/template") 80 | def get(): 81 | content = P("This is the dynamic content passed into the template.") 82 | return Template(content, title="Custom Title") 83 | 84 | 85 | @rt("/alerts") 86 | def get(): 87 | InfoAlert = create_alert("info") 88 | WarningAlert = create_alert("warning") 89 | ErrorAlert = create_alert("danger") 90 | 91 | return Div( 92 | InfoAlert("This is an informational alert."), 93 | WarningAlert("This is a warning alert."), 94 | ErrorAlert("This is an error alert."), 95 | cls="space-y-4", 96 | ) 97 | 98 | 99 | serve() 100 | -------------------------------------------------------------------------------- /fastapi_better.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import Response 3 | import httpx 4 | 5 | app = FastAPI() 6 | 7 | 8 | @app.get("/route1") 9 | async def route1(): 10 | return {"message": "This is route 1"} 11 | 12 | 13 | @app.get("/route2") 14 | async def route2(): 15 | return {"message": "This is route 2"} 16 | 17 | 18 | @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) 19 | async def proxy(request: Request, path: str): 20 | url = f"http://localhost:5001/{path}" 21 | headers = dict(request.headers) 22 | 23 | data = await request.body() 24 | 25 | async with httpx.AsyncClient() as client: 26 | if request.method == "GET": 27 | response = await client.get(url, headers=headers) 28 | elif request.method == "POST": 29 | response = await client.post(url, headers=headers, content=data) 30 | elif request.method == "PUT": 31 | response = await client.put(url, headers=headers, content=data) 32 | elif request.method == "DELETE": 33 | response = await client.delete(url, headers=headers, content=data) 34 | 35 | return Response( 36 | content=response.content, 37 | status_code=response.status_code, 38 | headers=dict(response.headers), 39 | ) 40 | 41 | 42 | if __name__ == "__main__": 43 | import uvicorn 44 | 45 | uvicorn.run(app, host="0.0.0.0", port=8000) 46 | -------------------------------------------------------------------------------- /fastapi_wrong.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Form 2 | from fastapi.responses import HTMLResponse 3 | from fasthtml.common import * 4 | 5 | app = FastAPI() 6 | tasks = [] 7 | 8 | 9 | @app.get("/", response_class=HTMLResponse) 10 | async def get(): 11 | add_task_form = Form( 12 | Input(type="text", name="task", placeholder="Add new task..."), 13 | Button("Add task"), 14 | method="post", 15 | action="/add-task", 16 | hx_post="/add-task", 17 | hx_target="#task-list", 18 | hx_swap="outerHTML", 19 | ) 20 | 21 | task_list = Ul( 22 | *[ 23 | Li( 24 | f"{task} ", 25 | " ", 26 | A( 27 | "Löschen", 28 | href=f"/delete/{i}", 29 | hx_get=f"/delete/{i}", 30 | hx_target=f"#task-{i}", 31 | hx_swap="outerHTML", 32 | ), 33 | id=f"task-{i}", 34 | ) 35 | for i, task in enumerate(tasks) 36 | ], 37 | id="task-list", 38 | ) 39 | 40 | content = to_xml(Titled("ToDo App", H1("My Tasks"), add_task_form, task_list)) 41 | return HTMLResponse(content=content) 42 | 43 | 44 | def task_list_partial(): 45 | return to_xml( 46 | Ul( 47 | *[ 48 | Li( 49 | f"{task} ", 50 | A( 51 | "Löschen", 52 | href=f"/delete/{i}", 53 | hx_get=f"/delete/{i}", 54 | hx_target="#task-list", 55 | hx_swap="outerHTML", 56 | ), 57 | id=f"task-{i}", 58 | ) 59 | for i, task in enumerate(tasks) 60 | ], 61 | id="task-list", 62 | ) 63 | ) 64 | 65 | 66 | @app.post("/add-task", response_class=HTMLResponse) 67 | async def post(task: str = Form(...)): 68 | if task: 69 | tasks.append(task) 70 | return HTMLResponse(content=task_list_partial()) 71 | 72 | 73 | @app.get("/delete/{index}", response_class=HTMLResponse) 74 | async def delete(index: int): 75 | if 0 <= index < len(tasks): 76 | tasks.pop(index) 77 | return HTMLResponse(content=task_list_partial()) 78 | 79 | 80 | if __name__ == "__main__": 81 | import uvicorn 82 | 83 | uvicorn.run(app, host="0.0.0.0", port=8000) 84 | -------------------------------------------------------------------------------- /htmxapp.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app, rt = fast_app() 4 | 5 | tasks = [] 6 | 7 | 8 | @rt("/") 9 | def get(): 10 | add_task_form = Form( 11 | Input(type="text", name="task", placeholder="Neue Aufgabe hinzufügen..."), 12 | Button("Hinzufügen"), 13 | method="post", 14 | action="/add-task", 15 | hx_post="/add-task", 16 | hx_target="#task-list", 17 | hx_swap="outerHTML", 18 | ) 19 | 20 | task_list = Ul( 21 | *[ 22 | Li( 23 | f"{task} ", 24 | " ", 25 | A( 26 | "Löschen", 27 | href=f"/delete/{i}", 28 | hx_get=f"/delete/{i}", 29 | hx_target=f"#task-{i}", 30 | hx_swap="outerHTML", 31 | ), 32 | id=f"task-{i}", 33 | ) 34 | for i, task in enumerate(tasks) 35 | ], 36 | id="task-list", 37 | ) 38 | 39 | return Titled("ToDo App", H1("Meine Aufgaben"), add_task_form, task_list) 40 | 41 | 42 | def task_list_partial(): 43 | return Ul( 44 | *[ 45 | Li( 46 | f"{task} ", 47 | A( 48 | "Löschen", 49 | href=f"/delete/{i}", 50 | hx_get=f"/delete/{i}", 51 | hx_target="#task-list", 52 | hx_swap="outerHTML", 53 | ), 54 | id=f"task-{i}", 55 | ) 56 | for i, task in enumerate(tasks) 57 | ], 58 | id="task-list", 59 | ) 60 | 61 | 62 | @rt("/add-task", methods=["post"]) 63 | def post(task: str): 64 | if task: 65 | tasks.append(task) 66 | return task_list_partial() 67 | 68 | 69 | @rt("/delete/{index}", methods=["get"]) 70 | def delete(index: int): 71 | if 0 <= index < len(tasks): 72 | tasks.pop(index) 73 | return task_list_partial() 74 | 75 | 76 | serve() 77 | -------------------------------------------------------------------------------- /langchain_stream.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from langchain_openai import ChatOpenAI 3 | from langchain.schema import SystemMessage, HumanMessage, AIMessage 4 | import asyncio 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | 9 | tlink = (Script(src="https://unpkg.com/tailwindcss-cdn@3.4.3/tailwindcss.js"),) 10 | dlink = Link( 11 | rel="stylesheet", 12 | href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css", 13 | ) 14 | app = FastHTML(hdrs=(tlink, dlink, picolink), ws_hdr=True) 15 | 16 | sp = SystemMessage(content="You are a helpful and concise assistant.") 17 | messages = [] 18 | 19 | model = ChatOpenAI(model_kwargs={"stream": True}) 20 | 21 | 22 | def ChatMessage(msg_idx, **kwargs): 23 | msg = messages[msg_idx] 24 | role = "user" if isinstance(msg, HumanMessage) else "assistant" 25 | bubble_class = f"chat-bubble-{'primary' if role == 'user' else 'secondary'}" 26 | chat_class = f"chat-{'end' if role == 'user' else 'start'}" 27 | return Div( 28 | Div(role, cls="chat-header"), 29 | Div( 30 | msg.content, 31 | id=f"chat-content-{msg_idx}", 32 | cls=f"chat-bubble {bubble_class}", 33 | ), 34 | id=f"chat-message-{msg_idx}", 35 | cls=f"chat {chat_class}", 36 | **kwargs, 37 | ) 38 | 39 | 40 | def ChatInput(): 41 | return Input( 42 | type="text", 43 | name="msg", 44 | id="msg-input", 45 | placeholder="Type a message", 46 | cls="input input-bordered w-full", 47 | hx_swap_oob="true", 48 | ) 49 | 50 | 51 | @app.route("/") 52 | def get(): 53 | page = Body( 54 | H1("Chatbot Demo"), 55 | Div( 56 | *[ChatMessage(idx) for idx in range(len(messages))], 57 | id="chatlist", 58 | cls="chat-box overflow-y-auto", 59 | ), 60 | Form( 61 | Group(ChatInput(), Button("Send", cls="btn btn-primary")), 62 | ws_send="", 63 | hx_ext="ws", 64 | ws_connect="/wscon", 65 | cls="flex space-x-2 mt-2", 66 | ), 67 | cls="p-4 max-w-lg mx-auto", 68 | ) 69 | return Title("Chatbot Demo"), page 70 | 71 | 72 | @app.ws("/wscon") 73 | async def ws(msg: str, send): 74 | messages.append(HumanMessage(content=msg)) 75 | 76 | print("MESSAGES:", messages) 77 | 78 | await send( 79 | Div(ChatMessage(len(messages) - 1), hx_swap_oob="beforeend", id="chatlist") 80 | ) 81 | await send(ChatInput()) 82 | 83 | messages.append(AIMessage(content="")) 84 | await send( 85 | Div(ChatMessage(len(messages) - 1), hx_swap_oob="beforeend", id="chatlist") 86 | ) 87 | for chunk in model.stream(input=[sp] + messages): 88 | chunkval = chunk.content 89 | print("CHUNKVAL:", chunkval) 90 | messages[-1].content += chunkval 91 | await send( 92 | Span( 93 | chunkval, 94 | id=f"chat-content-{len(messages)-1}", 95 | hx_swap_oob="beforeend", 96 | ) 97 | ) 98 | await asyncio.sleep(0.05) 99 | 100 | 101 | serve() 102 | -------------------------------------------------------------------------------- /llmapp.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from fasthtml.common import * 3 | from langchain_core.messages import HumanMessage, AIMessage, SystemMessage 4 | from langchain_openai import ChatOpenAI 5 | 6 | load_dotenv() 7 | 8 | app, rt = fast_app() 9 | 10 | messages = [] 11 | 12 | llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) 13 | 14 | headers = [ 15 | Script(src="https://cdn.tailwindcss.com"), 16 | Link( 17 | rel="stylesheet", 18 | href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css", 19 | ), 20 | Link(rel="icon", href="pirate.png", type="image/png"), 21 | Script(src="https://unpkg.com/htmx.org@1.6.1/dist/htmx.min.js"), 22 | ] 23 | 24 | 25 | def ChatMessage(msg): 26 | bubble_class = ( 27 | "chat-bubble-primary" 28 | if isinstance(msg, HumanMessage) 29 | else "chat-bubble-secondary" 30 | ) 31 | align_class = "chat-end" if isinstance(msg, HumanMessage) else "chat-start" 32 | return Div( 33 | Div( 34 | Div(msg.content, cls=f"chat-bubble {bubble_class}"), 35 | cls=f"chat {align_class}", 36 | ), 37 | cls="mb-2", 38 | ) 39 | 40 | 41 | @rt("/") 42 | def get(): 43 | chat_form = Form( 44 | Input( 45 | type="text", 46 | name="user_input", 47 | placeholder="Enter yer message...", 48 | cls="input w-full", 49 | ), 50 | Button("Send", cls="btn btn-primary w-full mt-2"), 51 | method="post", 52 | action="/chat", 53 | hx_post="/chat", 54 | hx_target="#chat-history-container", 55 | hx_swap="innerHTML", 56 | cls="mt-4", 57 | ) 58 | 59 | chat_history = Div(*[ChatMessage(msg) for msg in messages], id="chat-history") 60 | 61 | return Html( 62 | *headers, 63 | H1("Chat with Pirate Bob", cls="text-2xl"), 64 | Div( 65 | Img(src="pirate.png", cls="w-16 h-16 mx-auto"), 66 | cls="flex justify-center mt-4", 67 | ), 68 | Div(chat_form, cls="w-full max-w-lg mx-auto"), 69 | Div( 70 | chat_history, 71 | id="chat-history-container", 72 | cls="mt-4 w-full max-w-lg mx-auto bg-white p-4 shadow-md rounded-lg overflow-y-auto", 73 | ), 74 | ) 75 | 76 | 77 | @rt("/chat", methods=["post"]) 78 | def chat(user_input: str): 79 | if user_input: 80 | messages.append(HumanMessage(content=user_input)) 81 | 82 | response = llm.invoke( 83 | [SystemMessage(content="Respond like a pirate."), *messages] 84 | ) 85 | 86 | messages.append(AIMessage(content=response.content)) 87 | 88 | return Div(*[ChatMessage(msg) for msg in messages]) 89 | 90 | 91 | serve() 92 | --------------------------------------------------------------------------------