Build Applications For LLMs in Python

Jeremy Howard and Jono build web apps in Python using FastHTML, creating a to-do list with database persistence and an image-generation UI that polls for results. They show how htmx attributes like hx-post and hx-swap handle updates without page reloads, and deploy to Railway.
Chapters
For those of you that have done the fast.ai course before, you'll know that Lesson 1, we teach you how to train a model. And Lesson 2, we teach you how to deploy it. And so this is the model that I demonstrate deploying in Lesson 2. And you can see here, it's an image classifier. And we do this because, you know, what's the point of building a model if nobody can do anything with it? And so a lot of the time, the thing you want to do with it is to create an internal or external web application. And so generally speaking, the people doing that course are not full-time professional coders, you know, they, they have other jobs and coding is just a thing that they do to get their jobs done. And most folks in that kind of domain specialist area write in Python and obviously Python particularly for anybody doing AI, is necessary. So we, that app is written entirely in Python. And here is that app. And so that app uses a really nifty thing called Gradio. And to create an app in Gradio, you basically create an interface. You say, basically, when you click on the button, what happens? And the answer is it runs a Python function, what inputs are there? And as you can see, you can say, you know, it's an inputs.image and what outputs are there? And it's a label. And so then when the function returns, that causes, you know, this output to get filled in. I like Gradio a lot. And it is one of a large number of tools which are generally put in the category of what PyViz calls dashboarding libraries. Now you can see here, actually, Gradio with 7.5 million downloads a month is certainly the most popular. Bokeh is more specific to visualization, I would have thought. Streamlit though, very, very much up there with Gradio. But there's a lot. So there's 37 of them here. I was like very happy that Gradio, for example, exists. And before Gradio, we showed people how to use Voilà. But in some ways, it always felt a little bit unsatisfying because, you know, if you're this little prototype web application dashboard thing you create with Gradio is great and popular, what do you do next? You know, how do you go to the next level of expanding it out into a more complete web application? And basically the answer is like, okay, well now you've got to learn a whole lot of new stuff. You know, you can probably still use Python for the backend. You might use FastAPI, for example, which is fantastic. But then you're going to have to like learn TypeScript or JavaScript and something like React and, you know, hook all these things together and you're into basically full-time coding job territory at that point. That feels like pretty unsatisfactory. And so what happens in practice is most data scientists, you know, they don't go beyond the kind of Gradio, Streamlit level, you know, they create their little prototypes, and that's it. So this is a bit sad for me, particularly because my first ever successful YouTube series was called End-to-End Web App in Under an Hour back in 2013. I'd never, I don't think I'd ever posted a YouTube video in my life. And I was like, I wanted to show people that you can create things that are more than just a little demo. And, and, you know, I've spent months trying to figure out how to like use Angular plus C# to create a web app, a proper real web app you can scale as far as you like, in under an hour. And I got it working. And that felt pretty good. And much to my surprise, it was incredibly popular. It had, I don't know, like 300,000 views. Like I basically never done a video before. And I thought, this is real cool, you know, like a lot of people are interested in this topic of actually building stuff, putting it up on the web that's, you know, flexible and so forth. And the sad truth is I don't feel like today I could do that anymore. Like, I feel like things have got more complicated, you know, more specialized, And that kind of makes me sad, you know, and I also feel sad that even with that thing I did back in 2013, you know, you had to learn C#, you had to learn OpenAPI, you know, you had to learn JavaScript and Angular, like there's still a lot of things that you had to learn and that felt not great, you know. So Just realized I had the wrong thing recording. Never mind. Yeah, so we've decided to build something to fix this and it's called FastHTML. And it's important to mention it's not released yet. So quite often in the fast.ai courses I did this, I would tell people about a thing that's not released and I would ask people, please don't share links to this on social media, etc. And people always were good about this. So I'm assuming that this group of people is as helpful and honest as I've always dealt with before. The reason for that is that, you know, We want to launch it properly when it's ready and it's not ready. We're giving you a sneak preview. So yeah, don't share this widely. We would hate to have to make everything private again if it was, if that happened. So yeah, this is just for you. And if you want to talk about it, feel free to talk about it here on this Discord or Maybe, Johno, you could also add a link in the Discord chat to the fasthtml channel, maybe invite them to the fasthtml channel on the fastai server.
Sure.
That's another place that people could talk about this. So with that said, what is fasthtml? Basically, it's a library that's designed to bring the entire, like, power of the web to a Python coder. And so the thing we're going to build today, let's, like, load it up. So in the fasthtml repo, I've got a thing called examples. And so let's run that one. easycon dbapp:app. Oh, move to examples. There we go. And the app is very simple looking. Actually, we're not even going to add auth today, but you'll be able to see in the code how to add auth if you wish. But basically it's a very classic, like, CRUD application. It's a to-do list application. And it's got the kind of features that one would expect, like it's properly kind of responsive. And like, if I add a to-do to it, I can use the keyboard. I can get more details about a to-do. I don't have any details to show, but they're there, you know, to delete. I can edit. And you can see it like behaves like a modern web application. One with very few features, but like, this is all the things you basically kind of need if you want to create, you know, Facebook or Instagram. And in fact, it's designed to do everything that Django can do. And Instagram's actually written in Django. So to give you a sense of like this kind of basic idea of how this is built, you know, it's a totally flexible, real web application. But to get started, it's about as easy as Gradio. Okay, so that's the kind of the background. Jono, did you have anything you wanted to add before I started Karting?
I think this is good. I think this— there's a lot more we could say on the framing of like why this seems like an exciting trade-off to us versus some of the other alternatives, but we can do that later, or especially once we've seen the functionality. So yeah, if you want to dive in and show people like the getting started, and then hopefully the rationale will come out as we talk of like, oh, this is another neat thing that we can do that might be tricky in a different kind of framework.
Yeah. And because this is built on web fundamentals, you can use any web component library, you can use any CSS library, you know, you can use Tailwind, you can use Daisy UI, you can use anything, you know. So there's a huge world out there of stuff that we can build on. In the past, it's been like JavaScript developers to build on. So now it's for Python developers to build on. And, you know, you can create really beautiful and interactive and nice websites with not very much code. And so the code of what I just showed you, you know, including security, including plenty of blank lines, is 61 lines. So that's what we're going to code today. So there is, maybe Johno, you could put in the Discord chat a link to the fasthtml-dust shoot repo. And in that repo, I've created two things, simple and fancy. And they are, you know, they go step by step. You'll see all the steps that we're going to go through today. But today we're going to do it from scratch. So, oops, I didn't mean to do that. File, open recent, reopen closed editor. Can't do that. Well, thanks very much. Okay. So I just created an empty folder. So we're going to start with an empty folder, demo. Oh, that doesn't quite work. Annoying. Hang on a tick. Sorry, let me just keep thinking about how to do this right. I think I can do it from here. Okay, cursor. I see. Yes. So then I want to go to demo. Ah, there we go. Nice. All right. So we've got a totally empty screen. So let's get started. And I guess the best place to get started would be the documentation. And so pip install python-fasthtml. I've already done that. And that'll install everything that you need, just not very much actually. So let's just go ahead and like copy and paste this minimal app. And so we'll create a new file, demo.py. There we go. Paste. Okay. And actually those last two lines we don't really need. So I'm just going to delete them. So I'll save that. And so this is, this is a FastHTML application. I'll go through each line in a moment. And a FastHTML application is built on top of a number of really fantastic technologies. And the first one we'll look at is Starlette. So a FastHTML application is a Starlette application for those of you who are interested in the details. You don't need to know about any of this. So it's just FYI. Starlette, again, these are details which you don't really need to know about, but it's what's called an ASGI or ASGI toolkit. It creates ASGI applications. Now, the reason that matters to you is that to use FastHTML, you need to run an ASGI server. And the one you probably want is called uvicorn. So FastHTML installs uvicorn. For you. So that's fine. And so to run an ASGI web application, you type uvicorn, you then type the name of the.py file that you're going to run. And then you type the name of the object which contains the FastHTML application. So you see you've got FastHTML app equals FastHTML. So then you type app. Uvicorn_demo:app. And then something I like to add is --reload, which means if I make changes in the, in the code, it'll appear automatically. So you can see this is now running, uvicorn running. And if I command-click on this, there it is. Okay. So hello LLM course cool people. So if I press save, you'll see at the bottom, it automatically reloads. I still do have to refresh my browser. That's it. Okay. So now we've got a nice little loop we can start going through. And so I will endeavor to keep half an eye on the Discord group.
I can keep an eye on Surface things if you'd like.
Cool. Yeah, yeah. I was going to say, Johno can also do that.
There's a comment that this looks like FastAPI, which I think is—
Yeah, yeah, yeah, yeah. So Great. So FastAPI is the second thing that we're building on. We don't actually use it at all, but we've built it because we really like FastAPI. We built it to be as similar as FastAPI as possible. And so what I actually did when I started is I opened up the FastAPI tutorial and I then went through every single one and tried to make it so that exactly the same thing worked. And except for the bit where you return JSON. So FastAPI is designed to create these like two-tier applications where you return JSON and then you create a JavaScript application that uses the JSON. And that can work pretty well if you've got, particularly if you've got like different groups of people in your company who are like JavaScript developers versus Python developers and they can each do their own thing. Personally, I like to like have everything in one place, in one file, in one language. And so I'm less fond of that particular way of working, but FastAPI is amazing for that way of working. And so we thought, well, given how many similarities there are, we should try and make that work. So actually, if I copy and paste this exact syntax, obviously I'm not going to return JavaScript. Well, I mean, we support JavaScript as well, so we could look, we could return JavaScript, what the hell. Let's try copying Okay, I haven't actually done this before for a while, so this might not work, but let's try pasting. There we go. That's— we're not using FastAPI, but because I, you know, went through the tutorial and tried to make it work the same way, I think if I press save and go back to my browser and reload. Yep, there you go. Okay, so for some limited set of things, you can actually use the same syntax. And if you are interested in creating more of that kind of, you know, multi-language, two-tier style application, I strongly recommend FastAPI. All right. So what happens next? I think the next thing we should do is like have more than one page. So one, one thing that actually stuff like, you know, the kind of the dashboard type things and often not great at is actually having more than one page. So you'll see a lot of those dashboard apps tend to be, you know, one massive screen full of things. So let's go a bit fancy here and create a second page and then I'll explain what all this different syntax means. So the second page, So I'm just going to go and copy and paste this. And this needs a new path, which we'll talk about in a moment. And so this one will say return title. Actually, what I'll do is we're going to make this super simple. We're going to call this a page because I'm actually going to show you two different ways of writing these applications. The first one I'm going to show you is the simplest way. And then I'm going to show you what I generally recommend people actually learn if you've got a bit more time, which is the way to do things much more richly. So our second page. Okay. So the first thing we pass to a page is the title of it. Another page. Okay. And this one. So then they'll have links to each other. So we'll create an a tag. There we go. And this one, we'll also do the same thing down here. All right. And I'm using this kind of somewhat nice IDE called Cursor, which is basically VS Code with more AI built in. So when you see those little gray things that I just press Tab to fill in, that's Cursor being helpful. I say kind of useful because actually quite often it gets in the way and suggests things I don't want to do and doesn't listen to me. But anywho, so what's that doing? Right. Okay. So if I reload now, there we go. We've got an internal server error. So good to learn about debugging. Ah, yes. Page is not installed. So for this kind of extra simple version of the world, actually, we use slightly different. We've got a thing called fastapp. And in that case, we actually go fastapp app here. We'll talk about that shortly. Save. Okay, let's try that again. Okay. Now a page automatically uses this as the h1. So we don't need the h1s anymore. So the page thing tries to make like as little code as possible. Okay, refresh. There we go. So click to another page, go back. Okay, so you've got a beautiful, fancy multi-page web application. I hope you are all suitably impressed by that. Okay, so let me explain how this fits together. So there's, you might have noticed that this is a kind of quite a different style of application to the one I told you we're going to build, which is to say like there's totally different pages, you know, rather than things all happening in the same page. So the first thing I showed you was an SPA or single page application. And this is a more traditional application. These more traditional applications, I think, are a bit easier to write. So that's why we're going to start with them. And to create these kind of like super simple, separate page per thing applications, fastapp is the easiest way to do it. So you just say this. So as I said, you create fastapp rather than fasthtml. I will say though, that if you like, I think I should be able to command-click on this. I can't. That's fine. Let me just jump into Vim then. Like FastApp is literally like 20 lines of code. That's the entire thing. So it doesn't do much, but I really wanted to make it so like absolutely minimal boilerplate, minimal anything. So you can always look at that to see what it's doing as to what page actually is. So you create your app. Now, For those of you who haven't done any web programming, you need to know a little bit about how the web works. So what happens when I go to this link? So this link here is linking to /page. So what happens when I click on something that is an a tag that links to /page? Well, what actually happens is it goes to this server, right, which is my own computer, and it asks that server to get that path. And so what web application frameworks do is they let you say, okay, what happens when a browser asks for that path? And those are called routes, right? So one of the things in a route is what is the path that it's for? So this function is the function that is called when you get this path. So those are the two pieces of information you need to provide. One is what is the path? And the other is what do they want to do? And these are called HTTP verbs. And the main— and the one that basically happens when you're just like looking at a page is called get. So you can see I've got two functions called get. One is attached to the root path. One is attached to the page path. So if I go in manually and type /page, right, you can see all it's doing behind the scenes is it's going to, and it runs this line of code. And then what this line of code does is it returns a Page, which is a special kind of object called an XT object, which we'll be talking about shortly. And a Page takes two pieces of information, the title of the page and, well, actually, and then as many more things as you like, which is basically all of the things that you then want to appear on the page. So normally, if you've used things like Flask or stuff like that before, you've probably created templates, which is where you create a separate file. And in that separate file, you, you write HTML code. For this, you don't have to do that. For this, you can use HTML elements as Python functions. And every HTML element works as a Python function. The nice thing is that because they're both just Python functions, you can create Python functions that return other HTML elements. So container is a Python function. So this is the source code of FastApp, which returns a main. That's a, that's a type of HTML tag. And it, and the main contains an h1, and that's again a tag. So this means that you can have everything in one file. And like, I find my, I'm too much of a simpleton to be able to handle lots of files. So I'd like to have everything in one file and I'm too much of a simpleton to have multiple languages. So I just want to have everything in Python. And that means that if you want to, you know, create components and stuff like that, they're all just Python functions. And if you want to create a library of components for somebody else to use, you can chuck that on PyPI and they can now pip install that. And so actually FastHTML comes with, in fact, a library of components and it uses a really nice little CSS library called Pico. And it does that because Pico is like super, super simple. And so you can see it's got something called components. For example, there's things like a card and an accordion and a dropdown. And a group. And we'll see all of these things today. We're going to be building them. And the way Pico does it is that like a card is literally just, oh, just use the article tag in HTML and it will render things in a particular way. And if you have an article with a header and a footer, it'll render it in a particular way. So we have created as you can see, is a card, right? So here's, here is the card function in FastHTML, and it just returns an article, optionally with a header and a footer. So to create new components, you just write Python. And that Python just returns HTML tags, or it could return other, other functions which return HTML tags. So like another example is like, here's a group, right? So a group, here's an example of a group with a button and an input. And the way they implement that is they create a fieldset. And so I mean, it's hardly worth doing, but to make things as easy as possible, I was just like, okay, we'll create, you know, a Group and a Group just returns a Fieldset with a role of group. And you can see like you can even do it, like you can prototype everything if you want to. I quite like doing this in Jupyter. Everything appears correctly as you see, you know, CS— Pico has a grid, a grid object thing and a grid is actually just a bunch of divs. With class grid on them. So you can see here's a grid with some colors. Okay. So it's like super, super basic. Okay. So, Johno, just dive in if there's any questions or comments from either everybody in the Discord, or if you have anything you think is worth adding at any point. Otherwise, I'm just going to keep driving on.
And go for it.
All right. So to create a to-do list, we need data. And this is often one of the challenges with kind of the dashboarding type things is like, you know, you actually want a database. And actually Python comes with a really great database called SQLite. SQLite is a database which does not require any separate server. But despite what a lot of people think, you can actually create pretty significant applications using nothing but SQLite. The trick is behind the scenes you have to use something called WAL mode, W-A-L mode, and it actually becomes a pretty capable database. But yeah, you don't have to run a server, which is really cool. And so you can use whatever database you like or none at all. And Johno will show examples with no database. But a lot of the time you'll want to have like tables and rows and things like that. So we try to make that as simple as possible to do. And, you know, I quite like playing around with this stuff a little bit, you know, in notebooks to fiddle around. So we can kind of do the same thing here from fasthtml.all import So fasthtml comes with SQLite support built in, but there's no particular, you know, you don't have to be particularly attached to that. Okay, so I'm just trying to see what's everything here. And Jono, do the gang from Railway know, or Hamel, do the gang from Railway know how to come here, by the way? Because I wanted to say hello. Yeah, they have an invite as well.
I've been watching my DMs and I haven't heard anything.
Cool. All right. No worries. They might have forgotten. So one of the things to mention is like what kind of like database you use depends a bit on where you want to deploy it. And so We'll be showing today how to deploy to a really nice thing called Railway, railway.app. But I spoke to the CEO of Vercel yesterday. So they're building a deployment for Vercel. I spoke to the product manager for Python Anywhere. They're going to build one. Hugging Face are also going to build one. So like, there'll be a lot of places you can deploy this. And most of them are going to support this SQLite approach, although Vercel have their own database and they're going to actually create their own Vercel binding, but they're all going to look basically the same. So what you learn here today will be, will be fine. So let me think, where do we start? Yeah, let's just keep kind of building out. We can go back to the Jupyter Notebook as well, but maybe we'll just keep working through Cursor a little bit. So With fastapp, you can see you can pass it a db_path. So I'm going to go ahead and do that. So db_path. All right. So let's, you know, create a directory for that data slash, we'll call this, I don't know, demos. Bb. Okay. And then you can also just pass it a bunch of fields that you want to appear in your table. So they're like this super simple version is if you just want one table. I'll show you how to have more tables shortly. So we're going to have an ID field. We're going to have a title field for our todo. So for each one, you just say what's the name of the field? And then you say what's the data type of the field? So is it done or not? And then also it's good practice to say which one's the primary key. And obviously in this case, we want to use ID. Okay. So that's enough to create a database. And so now we can start. So we could save that. And oh, what happened to our terminal? Do I have to rerun it? It's strange. Did I restart this at some point? Oh, I'm in a, oh, I'm in a different place. Silly me. Let me copy this. We're meant to be in demo.py. All right. So paste, right? Okay. So save that. And let's see, we're reloading. Why isn't it reloading? Okay, then let's go back to our application. That need more. Okay. All right, so back in our application, nothing's happened yet. We've created the database, but it's not actually doing anything. That's what you'd expect. So that's all fine. So let's take a look at our database over here as well in our notebook. So I'm just going to go ahead and copy that path. And so you can manually create a database or a link to a database just by saying database. And put in the path. Okay. Okay. So there's our db. You can list the tables using.t, t for tables. Okay. And am I in the right place? notebooks_demo. I was expecting there to be a table in there. fastapp_data/demos.
Might have to restart the uvicorn thing. Oh wait, you did.
I did. Uh, also I was expecting Oh, I see. So now that we've got a database, we actually get returned two things. We get returned the table and we get returned the class that represents items of that table. So I need three things. Okay, there we go. That was silly of me. Okay. So now if we look at this, okay. So you can see, and by default, it just creates a table called items. And so let's just say todos equals that. And then we can have a look at that todos. And so to get the columns, it's just c. I thought it was c. Is it not c? Oh, sorry. todos equals items. Right. Let's try that again. Columns. Okay. So a, so a database has tables and then you can get any table just like so. And you've got even like tab completion, right? So if you hit tab, You can see it just fills in items automatically. It's all dynamic. And then I can do the same thing here for tab completion for columns, right? And if you want to list out the rows, you just call it, okay? And there's currently no rows. So that's not going to make very interesting to-do application. So why don't we just insert a few rows? And so behind the scenes, you remember it created a Todo class for us. We can manually ask for that class just by saying db— sorry, todos.dataclass, and that creates a class for it. And so the nice thing about that is now we can insert a todo and we get, as you can see, it automatically dynamically lists out the things that we can call. We're just going to use like title here. Show how to use SQLite. So that's going to be the title, which again, I get tab completion for. All right. And I guess we should say done equals false. There we go. And you can see when you insert something, it returns the inserted object. So it automatically has set an ID for it. And make a cool demo app. Okay. So we've now got a todo table with two things in it. So let's show them. So let's make our GET now show those todos. So we probably want to just create a list, right? So to create a list in HTML, you just use an unordered list, right? And a list contains list items. A list, oops, let's give that a list and another list item. Another. So I kind of like to do things in really small little steps, as you see. Let's just make sure at each stage it works.
You're not actually putting that in the page.
That's fair. All right. So let's add, got to call that something. Oopsie daisy. Todos, to-do list. Equals, okay, and then you put it in there. Thank you, cursor. And thank you, Johno. Okay. So there we've got a list, right? So instead of having that text, we would instead like to have the titles of the todos. So we can do like a list comprehension. And it's actually made a reasonable guess. But remember that to actually list the contents of the table, you have to put brackets, you have to call it. So an unordered list containing— yep.
Single star, right? Because we just want to put the list.
The angle star. Thank you. I always do that, don't I, John? Okay. An unordered list containing a list item with a title. To each todo. Okay, great. And we don't really need this a tag anymore. Okay. Now this is a todo list.
And by the way, moderators, how— are we scheduled to stop in 10 minutes? I'm guessing we can go over, but is there any sort of time pressure that we should keep an eye on?
There's no time pressure. Okay. I might have to leave, but please keep going. I'm sorry.
That will be here.
I didn't do a trial run, obviously. We're loving it. Keep going. Okay. All right. So like, let's instead make this like the todos detail page. So if you've used Flask, this will all look pretty familiar. To-dos/id. Putting things in parentheses, in braces means that, in fact, you can see Cursor's guessing it all because it's so standard that it's going to be passed as a parameter. It's a good idea to type that parameter because it'll automatically cast it to the right type if you can. And you can see, you know, we can now grab a to-do and actually to grab it, Cursor does not actually know how my system works, to grab a single item by primary key, use square brackets. Okay. So the one in round brackets is where you pass a WHERE clause and/or a LIMIT and/or an ORDER BY. The square brackets just looks up something by primary key. Okay. So now we actually need to create an a tag, which will link to this, right? So, and okay, Cursor guessed correctly, an a tag.
It's maybe worth calling out why Cursor guessed correctly. Like I find this a little mind-blowing because this is, a brand new library, right? This has been in existence, what, since you wrote it 2 weeks ago or a week ago? So this is not in the training data, but these language models know how to write HTML. And so when you're using things that are like these existing HTML private tips, it sees like, oh, I guess if I want to pass in an href into a link, that's how I do it. Therefore, like if I want to do, you know, a title or a name into a form field or a source into an image, it's going to be the same for— so it's actually very close to something that the language models know extremely well. And so this was very different to like, I've used some very custom libraries where it's all their components, all very like custom, then it's not documented before the language model training, and then the language model is no help, whereas here the suggestions are often pretty good.
Yeah, yeah, for sure. Like Google just released this thing called Mizop or something, which is like a kind of an Angular wrapper, which has got some similarities to FastHTML, and you kind of can't do it with that because it's all like a new custom different thing versus, yeah, this is just HTML. So yeah, let's check this out. Hopefully, okay, so we get the todo and then we return a new page. It's going to have the title as the heading and it'll also say whether it's done or not. Okay. So, okay, looking hopeful. So I click on one of these and so if I inspect this, you'll see that it's linking to /todo/1. So if I click on that, there we go. That's the title and it's not done. All right. And so at this point, we probably want our back button again. So I have an a, there we go. Thank you, cursor. Go back. There we go. Okay. And you can see it's keeping track of the URL correctly up here. Go back. There we go. Make a call, demo app. Cool. Go back. Cool. Okay. The SQLite thing is a library I wrote called fastlite. And you can— this one is released. So feel free to talk about it publicly. It's actually very heavily based on sqlite-utils written by Simon Willison, which is really great. So a lot of the best ideas, if not most of them, come from that. But I added quite a few things to try to make life a bit easier. So yeah, so check out fastlite. Maybe somebody can put a link to that in the Discord. And that's what is actually being used here. And so fasthtml.all automatically imports, you know, the stuff that you'll need from fastlite and so forth. That's why we didn't have to import everything manually. You can import stuff manually if you prefer. Okay. So next step, let's make this look a little bit better. So One thing that I think I quite like to do is for making things look a bit better is to kind of test things out in Jupyter. So to make stuff work correctly in Jupyter, there's basically two things you have to know to do. Let me just try to remind myself of what those are. Oh yes, that one. Okay. So the two things you need to run is show_peacocon_link and sorry, what did I say it was? See, I just looked at it 5 seconds ago and I've already forgotten. It's called set_peaco_class. Okay.
And this is a notebook-specific quirk, right? This is if you want to be able to prototype things.
So now that I can try, I mentioned earlier about cards, right? So a card, you can say, there's the, here's a card. And so by default, it shows you the HTML that it would create. And so in a card, they said you can have a header, and you can have a footer. And that's the HTML that it's spit out. And to see how that would render, you just type show. And okay, that's cool, right? So I'm kind of going to use this. Yeah. So basically without these lines, Jupyter is not going to know how to render EcoCSS. So with these lines, it automatically adds, it's got a little bit of JavaScript basically that automatically adds the necessary thing to render it correctly. And if you can also, if you put this and you use Quarto documentation, then Quarto will also correctly render all of your Pico stuff and you can create similar things for other CSS libraries, CSS libraries. So yeah, given that looks pretty nice, I might just go ahead and change this now as well. So I think what we'll do is we're going to need a way— well, let's just put it in a card first. All right. So card equals card and basically it's going to contain our to-do list. And then we could have like a header. Doesn't need to be H1. You see, this is where it doesn't quite know. It's just guessing, not terrible guesses, but header is a header.
Wait, isn't this an argument? Header equals rather than—
Header equals, yeah. Thank you. This is where sometimes I find Cursor confuses me because it's so keen to, you know, this time it's guessed correctly. Okay. And then this is Nils. We save that. There we go. So you can see it's kind of coming together there. Great. So the next thing we'll do is we need to be able to add a new todo. So what should we do there? So the first thing actually I might do is I'm going to refactor things in a way that I find helps a lot. Which is like the idea of like rendering a todo as a list item is like something that deserves its own function. It's going to become more and more useful. So rendering a todo just means turning a todo into a list item. Okay. And it's actually guessed. So I got rid of the ul. This is just returning a list item. Right. And so now actually I don't even need list comprehension, it can just call map render over todos. All right. So that's going to call this function on every todo. And here's the function that's making it a list item. So I think that looks a little clearer, still works fine. Okay, good. So now we're going to add the ability to create a to-do. So in HTML, the way that you have, you know, the ability to get inputs from people is by creating a form. So let's create a form. Okay. And we're going to use that group thing we learned about earlier. And in the group, we are going to put an input, that's a text box in HTML. This is all just standard HTML. Okay, the name they've guessed correctly, but I want to actually add to it a placeholder, new todo. I think it's quite a nice way to do things. Okay, so our form contains a group, our group contains an input, and it's kind of nice to format things, I think, in a way that's a bit easier to see what's going on. And it's also going to contain a button, and we'll just call this one add. Okay. And I think that's my form. All right. So then the other thing a form needs is like what happens when you click on the button. So when you click on the button, it's going to post it somewhere. So to post things, and we're going to talk a lot about this HX thing in a moment, you write HX_POST. So basically, whatever you are, whatever one of the HTTP verbs you're using, you just write that preceded by HX_. So now we can go ahead and write our route. So now it's not GET, it's POST, right? And what is the POST going to receive? Well, it's actually going to receive, believe it or not, a Todo object. Oh, look, and it's guessed correctly. Thank you. That's very impressive. The reason it's going to receive a Todo object is because it receives the contents of the form. And whatever the name of the inputs are is going to become the attributes of the object. And in this case, we're not even going to, you know, everything's not going to be done to start with and done is set to false by default. So we're going to get a todo. And so to add a todo, render_guesswrong here, it's actually insert. Okay. And you can do this all in one line. Let's just return. And so because todos.insert returns the todo that's inserted, I can just say return that. Now that's actually not quite true because this is going to be a separate page. We're going to use an approach like that later. Actually, what we're going to do is we're going to actually just say return and we're going to take all this out, move it into a separate function called home. All right. And then this is just going to return home. And this is also going to return home. Okay. So that's the kind of what makes this page-based approach pretty convenient is like after you do something, you just return the whole page again.
The form's not actually returning the page.
We're going to put it up here in the header. Okay. There we go. So if I now say create, delete and edit thingies. There, okay, it's working. So we have a to-do list as long as you never be able to set anything to done and as long as you never delete anything. It's— this is my kind of to-do list, something you only add things to. Okay. So this hx thing is using something called HTMX, which is really kind of the secret behind a lot of fast HTML. HTMX is basically something that makes browsers work the way they always ought to have worked. So normally with browsers, they can only do two things without JavaScript. They can provide hyperlinks that when you click on them, it sends a GET request to that place. Or you can create forms where if you click on the button, it sends a POST request to that place. And then it can only do one thing with the result returned by the server, which is replace the entire page with the result. So htmx changes it so that you can send— so that anything you click on, move, touch, wave over, anything at all you do on the page can send any method to any endpoint, and it can do anything you like with a thing that comes back from the server. And so you don't have to write any JavaScript to do those things. It just makes it so that's how the browser works now. So we can just pretend that that's how the browser always worked because FastHTML always includes htmx. So all the htmx things start with hx_. So this just says, okay, when you, when you submit this form by clicking on the button, it should POST to this route. And so this route has a POST function. Okay. And that's how htmx works. And because we're in this like simplified full page version, then the, the thing that comes back replaces the whole page. Okay. So we've got that working. Great. We should probably add a delete button, I guess. So let's put that in the details screen. So contents equals, let's grab that. Okay, so we're going to have a backlink And let's also have a, a DELETE. Oh, cool. And you can see it's filled it all in automatically. That was nice of it. And so now star_contents, good.
Jeremy, hx_delete, I don't think says trigger an HTTP DELETE. I think that's delete whatever.
That's actually, you're right. It's done it wrong. It's that way around. Yes. Thank you. Great. So yeah, all of the HTMX things, you say what method to call and what endpoint to call it on. So they're the two things that you provide. So this is going to call the delete method there. And, you know, maybe these should be buttons. Things that do things feel like they should be buttons. Okay. So now we obviously need a delete. So I can copy that. Delete. And so this time, because we're calling /id, It has correctly guessed all of— okay, that all looks correct. So I think Cursor guessed correctly there. All right, let us— okay, I think we've done the first one. So let's delete it. There we go. Okay, that works. Okay. So we're probably ready to get rid of these annoying things. I don't want those there. Right. So what else have we got now? It's looking pretty good. All right. So I guess we should— oh, we need to make edit work. Okay. So to edit something, we're going to need to go to a new page. It's going to have a form on it for editing our todo. So let's create that page. And so we'll be editing some particular todo. So we'll need that. And it'll be edit. And this one, we're going to return a form. Okay. And the thing we want to edit, okay, it's going to come in as an ID. So we're going to need to grab that todo just like we did before. And then we'll create the form. Let's not return it just yet. Let's create it first. Form equals, okay, the form is going to need let's create a group just like we did before. Oh, look at that. Name, title, button, save. Yeah, that's pretty good actually. Okay. Yeah, look at this. It's even guessed. So PUT is what you normally use as an HTTP verb to edit things. So it has correctly guessed that I want to PUT. So that's good. Although, let me think. Yes, that's right. But we don't need, actually, we can just put directly because it's a form. All of the information is going to be there already. So this time we do want the ability to be able to change done. So we'll have a checkbox. Okay. And ID is done. So it sets the, actually, it's not a bad idea to always use ID. FastHTML always sets the name to the same as the ID if you don't specify both. So that's not a bad way to do things. So we should give it a label. Okay.
Do you want that in the form?
Sorry?
Would you like that in the form? Oh, it is in the form. My bad.
I think it's in the form. Yeah. I've actually made this overly confusing. So I should do, yeah, that. That's the group and there's also a checkbox and then that's the form. I think that's more correct. Now then, when you submit the form, it also needs to know which, which todo_id this is because we're editing a todo rather than adding one. Oh dear, it's— see, this is what I don't like about Cursor, it adds stuff I don't want it to add. We also need to tell it, so we add a hidden. And so that's just going to pass back the ID. Now, the thing that you might be surprised about is that we've created this form, but it's not in any way, it hasn't got any of the information from the todo in it. And that's because we have a very simple but rather nifty little thing where you can simply say fill_form. Don't know why we've lost all of our— fill_form, fill this form with this todo, and that just goes ahead and puts the correct information. So now we don't really even need this on a separate line. And there's no magic here. It's just simply going through and— see, it keeps adding stuff I don't want. It just goes through and adds each attribute that it can find in the todo to each named field in the form. And that actually returns the form. So we can keep simplifying. Okay. That's probably about as simple as I think we can make it. So let's try that. Okay. Not surprised that's broken. Okay. So Oh, did I? I think I know what I've done wrong. Post. And yes, that should be, oh, that's port.
So there's a question.
Oh, I've got todo/1. That's why I need to go back to Yeah. Oh, nice. Yeah, one doesn't exist anymore. Sorry.
You've got, you know, two different things that have the root for slash there, or maybe even three. And so someone is saying, oh, like, how does, how do we define like which method is used? And is it, is it related to the hx_get, hx_delete part? So maybe you can clarify the root split there.
Yes. Fair enough. Yeah. So basically whatever appears after hx is the name of the HTTP verb that's called and whatever HTTP verb is called is whatever function is called. So hx_delete calls the delete function and for our adding_one, hx-post calls the POST function. And the kind of default in this special simplified version in FastAPI is to call GET if you just have a normal href, but we in the future will be using hx-get. Yeah. Okay. So All right. So we actually need a way to get to this edit page. So we can actually quite simply do that by just adding another link here. And oh, it's guessed correctly. So edit and an edit link is going to go to /edit. So return. Okay. We probably want a little bit of something between those two. So maybe we just go bump, bump, bump. There we go. Okay. So I can still go to places to see it, go back, edit. There's my form. Okay. Make a call demo, not a demo app, save. Oh yes, and now the problem is that after I'm done with that is I don't want to return actually that, do I? I want to fill the form. And then when I'm done, actually, I want to return home because this is the multi-page version. Okay. Hang on. No, no, no. That's wrong. What am I trying to do here? We've got a field. So we've got, this is, this is GET method.
This is showing the form. Yeah. We need a PUT version as well.
Yes, yes, yes, yes. This is showing the form. Sorry, my bad. Right. It was fine. So we need a put_version=True, which we don't have. Thank you, Jono. Okay. Put. And at the end of that, it's going to return home. So this is going to receive a todo. And then yes, we just go todos.update with the todo and return home. That's exactly right. So Cursor guessed that one correctly. All right. So make a call, demo, save. There we go. That works. And it's not actually done, but just to test. And actually, we should have something here that at the start has a checkbox. I don't think we need an X if it's not done, that's a bit over enthusiastic. Okay, see how that looks. There we go. Okay. So we're not really done with that. So not done. Great. And then the only other thing, well, I think, yeah, I guess that's it actually.
A few questions from the audience on this one. One is like, how does it know which todo to update?
So in our edit form, we had a hidden. And remember I said that all of the values in the form just get used to fill in automatically the attributes of the thing that's passed to the PUT method. So it's gonna get the title, it's gonna get the done, and it's gonna get the ID. So it's hidden. So in fact, let's look at it. This is a really useful trick, right? Is for these things is just inspect, because it's all just HTML. So here you go. Hidden, create an input type equals hidden, value equals to, ID equals ID, name equals ID.
And there's a slightly less smart way that you could do this as well, right? Which is that you could take the, take the individual things as arguments to your function. So you could, instead of taking a todo, you could take an ID, a name, and a done as arguments. And then you could, okay, I'll go look up the todo with that ID and then I'll change the other things to match. Um, it's just more work. And so if you, you have this data class ready of like, this is what a todo can have, an ID, a name, and a done, um, put that as the argument and then it'll go and find those things in the form.
Yeah. Yeah, you could do that. Hopefully you wouldn't.
Then there's another question that maybe is a good lead on to other things. Someone's saying, oh, so hx_ can only be used by buttons or like what events and browser elements can this apply to?
Every single JavaScript event, every single element can have an hx_ thing on it. So for example, if you wanted to add validation as you like say, oh, enter your, you know, create a user, enter your username. Every time you type, it can call an endpoint, check whether that user exists and fill in a little thing underneath saying like, user does, you know, user already exists or whatever. You can have something that when you wave your mouse over it, you know, something happens. So yeah, full interactivity of everything that JavaScript can do. Okay. So then I guess we should deploy this. And actually, I'll use a slight shortcut here, which is that, although I haven't actually deployed this before, I don't think, so it'll be an interesting test. I'm going to go to the fasthtmljs-tutorial where we already have main.py in simple. And it should be roughly the same code as I just wrote, but it's got 2 extra lines at the bottom, which if you remember, were actually the 2 lines I deleted from the documented suggestion. And these are just the ones that allow me to run it without having to write uvicorn. So let's just cancel this. So if I, when you've got those 2 lines at the, at the bottom, It means you can just say python main.py and it runs. So the reason we have that is because we now want to deploy this to Railway. And by default, and I love doing things that work by default, it'll run something called main.py. So I just dump this standard file into my directory in the same place as main.py. And I've never had to change this yet. I'm not even sure if you need it at all, but that's just what I put there. And then I have a requirements.txt listing the things that I need. I don't think I need anything except the first line, actually. Anyway, I know this works. And then there's a tiny little Do I have it here? Yes. There's a tiny little script deploy, which just runs these steps. Railway init. So that will create the project. Railway up. That will upload your project. Railway domain. That will add a domain to your project. FH Railway link is, will link the current directory to your project. And this is really cool. Railway volume add. Creates a persistent volume and the, the app always will run inside /app on Railway and we created our data in data/. So this means that we're gonna, it's gonna work exactly the same way, but it'll be persistent. So let me tell you about Railway and why we think this is amazing is— And by the way, Jeremy, I think Jake from Railway is here. Oh, great. Okay. So hello, Jake from Railway. I'm glad I was about to say nice things about your product. So Jake is the CEO of Railway and I'm not suggesting Railway because I know Jake and therefore wanted to suggest it. Instead, I tried lots and lots of different options for deployment, found that Railway was the best of all the ones I could find. And then reached out to Jake and say like, hey, you guys are cool. What do you think about, you know, us deploying stuff onto your platform? And I had a couple of minor suggestions for things that could be improved. I thought he had them fixed within a day. And so let me tell you what I was looking for. I wanted this to be really hobbyist-friendly. So it's like the only one I found that's, that you pay $5 a month and then you can create as many deployments as you want, as many volumes as you want, as many domains as you want. And so I've been using it a lot over the past few days, you know, a week or two, as you can imagine, to like try everything out because I'd never used it before. And my total cost so far for all of my projects is 3 cents. And of course the $5 a month, but the $5 a month gives you $5 of credits. So I've still got $3, sorry, $4.97 worth left. And so this is like totally amazing to me is you can, and these things like they spin down quietly in the background. Is Jake just in our text chat or is he able to join the Zoom so he Jake, do you want to say hello? He should be able, he's muted right now, but great. So what I'm going to do is I'm going to run this deploy. And then while it's running, Jake can tell us more about what's happening here. So I'm going to run deploy, I'm going to give it a name. Let's call it simple_demo. And that'll take 2 or 3 minutes. So if Jake wants to say hello.
Hey, how's it going? Looking for the video thing, but I guess I'll just be a voice in the crowd. Yeah, Jeremy reached out to us a little while ago, I guess a couple of days ago. And yeah, didn't really know him, but ahead of time, I asked a couple of friends. I was like, man, this guy seems quite large in the ML community. And so I'm mostly in the distributed community and surprised there wasn't a ton of overlap there generally. But yeah, we're really, really stoked. And we've just tried to build a really, really awesome, quick quick and super easy to use deployment platform. And like Jeremy mentioned, really catering towards students, hobbyists in general. There we go. Join as a panelist.
Hey, there we are.
Okay, beautiful. Awesome. Yeah, so, so probably disconnected there, but yeah, Jeremy reached out and then yeah, I wanted to make sure that, you know, we wanted to make sure that the platform was really, really student friendly and also could scale, you know, to infinity. So we have the $5 plan. So as mentioned, you can, you can host things on there. If you turn on what we call like app sleeping, it'll basically automatically scale down your workloads for you. So you can host a lot of stuff on this platform for that kind of initial $5 a month.
And just before, I'm just going to interrupt just so this can run well in the background. I just tried to deploy it. And if I look at my screen, this has never happened before. Did I do something dumb?
No, that's good. If you're able to open that link, it should also just tell you what the build is.
Oh yeah, this is quite nice actually. So you can click on here and it doesn't look like it actually—
So if you click the build logs, it should give you all the builds and then there's the deploy logs as well. So it looks like the command line for some reason just failed to fetch the logs. I can definitely have a look at that in general.
Okay, no worries. So I should just be able to go ahead and click on this.
And there you go.
Okay, nothing here yet. Something that sometimes that happens for a couple of minutes.
Yeah, the initial domain propagation, we're now really pushing the edges of Envoy's state updating. So we've had to build our own kind of distributed state updating system. So rolling out a new proxy on Friday that'll make this quicker to update the initial kind of edge. But as of right now, it should take, you know, about 60 seconds, sometimes slightly longer for it to kind of like roll out there.
Okay. It was like so fast this time. Like normally it takes 2 or 3 minutes, but this time it took seconds. So that was why I was a bit surprised.
Yeah. So there's automated caching as well with this. So if you've built something before and you've gone and pushed it in general, it will just reuse any of that old caching without you having to configure anything. It's odd that your domain's not propagating.
Yeah, it's taken quite a while, so I will, I will not have people have to look at that while it's happening. But yeah, so, okay, so the fact that it takes a couple of minutes for the domain to get linked up.
That is definitely not, not intentional. We're building a whole system to fix that, which we should move from.
Yeah, normally the screen fills up with the actual logs of the deployment in the CLI, and this is the first time that hasn't happened, but that's the nature of demos.
The classic, classic demo experience, you know, right? Like you just you plan and then it works for everything. And then the moment you demo it, it's everything kind of explodes for you. So.
Okay. You know, I'm inclined to move on to Johno's demo rather than do the fancy version. I'll just mention that, you know, because this took about 3 or 4 times longer than I hoped. So instead what I might just do is like show you all like the fancy version, I'm just going to give you a quick run through, right? So the fancy version, we don't use FastApp, right? We just use FastHTML. FastApp really is just for people who want to copy and paste the page boilerplate and do something as simple as Gradio, you know. And so then if we're not using FastApp, you do the steps we did in the notebook. You create the database, you go database.tables.todos. If the todos is not in the tables, then create it, right? And then create the class for it. So those 4 steps are the things that FastApp does. Then create the FastHTML app. This is the same as before. Where it gets interesting though is post. So just like before, we've got our group with a new todo and a button and it does an hx-post, but we're not using pages anymore. It doesn't return a page. It just returns a todo title. And maybe I'll just run this so that you can see what that looks like, right? So, um, uvicon, fancy_03:app. So what's happening here is that the button I click on is going to POST in just before, but this time I think I've got a target ID. The target ID is details and that is the name of my footer. And what that does, it means is that the result that's returned by this POST is going to be placed into this element, which is not at all what we want. It's just to show you what that looks like.
You're live now, by the way, on your deploy.
Okay, great. So if I add a todo here, ahoy, add, see, it's popped down here. Right. So I didn't, this is not, this is like the stage where, okay, we're now doing JavaScript stuff, right? It's not a full page refresh or not a full screen replacement. It's just replacing something in the DOM. And so we got to say, what verb do you call and what happens with results? So it goes into here, right? So then in fancy_04, we do something even more fancy. Where the, the form, okay, the target is now the todo_list itself. And so then this is kind of the last new piece of thing that you have to learn, which is that there's also a thing called swap. Normally, if you say there's a target, it replaces the contents of that, but we don't want to replace the whole contents of the todo_list with one todo. So swap says, what do you want to replace? This says, oh, just stick it in before the end. Right. So with this version, go to the end. There you go. See, it's put it in at the end. Right. So that's this key idea. Like the one extra thing I wanted to explain is that, yeah, you can have a target. And a swap value. And this now means you can, you know, basically do whatever you like. So actually it might be interesting to look at if I click one, it pops down at the bottom and the delete appears down there. And that's because clicking one has now does an HX GET, right? So it's causing htmx's GET. And it's sticking the result into details, right? And so hx_get is returning that path. It's got a button in it. That button calls delete and delete does something interesting. Delete returns nothing at all. So that is going to replace that target_id, which is the list item itself. With nothing at all. So if I press delete on another one, which is here, just disappears, right? It didn't refresh the page at all. And so you end up with a really nice thing with this approach. So if we have a look at the full version in main.py, our full version actually has, if anything, less code than the simple version. Like it's not harder. It's just like a few more things to learn about. And you can see like, yeah, so there's a PUT. And so the PUT now simply returns todos.update. The POST simply returns todos.insert. Yeah. So it's, it's actually, I think in many ways a lot simpler and clearer when you do it this way. And There's also one extra piece of functionality that if you take your class Todo and patch into it something with this special name, this is always called whenever you render a todo. XT stands for XML tag. That's what these data structures are. So that's why I can just return my inserted todo. I don't even have to render it, you know. And I can just return my updated todo. And over here, I can create just an unordered list of todos. They are all automatically rendered with this special function. Okay. So that's my fancy version. And then Jake said this now works as well. So we'll just go, if we go Railway open, that will pop open the browser to go over here. There you go. Oh, what just happened? Browser. So we should be able to click here, click here.
There it is.
Okay. And so this is persistent. And you'll also see in the fasthtml repo, there's also a version of this with authentication and per-user to-do lists. So like it's something you can actually deploy and it's got user creation and login and all that kind of thing. And I think that's an extra 2 lines of code. So it doesn't get very complicated. Anyway, sorry, Johno, that went a while. So I will hand it over to you. All right.
Thank you, Jeremy. I think I can share my screen now. All right. So What I'm going to do is talk through a couple of new examples. And before I do, I'm going to link to this in the Discord. I'm trying to write up some like illustrations of ways to do things. And so Jeremy's todo example will eventually become a section here. The image example that I'll show will be a section here with explanations that you can like come back and revisit. But yeah, so the goal of this section is to just demo, okay, we've seen the mechanisms and Jeremy's built like from scratch the todo app. I just wanted to show a couple of other things. And these are all bits that I built that this week, didn't take much time, putting together these same pieces. And so let's start with this image generation UI here, right? This is again, if we're talking Gradio demos or whatever kind of classic UI, if I type in a prompt and hit enter or click generate, that's going to get added. And we have this nice little user interface. This is responsive and pretty. So I kind of thought we'd get through more of the more of the demo before we got to this point. And so then I was going to talk about how we do the visuals. And I will still do a little bit on that, but I don't want to get too caught up in like, Jono, how did you do that grid? More the general picture, like how did we build this thing? And so if we look at this, oh, and I can refresh the page.
Given how far over time I've gone, like don't feel too bad at this point. Right. Whoever's still here is hardcore enough.
For the long haul.
All right.
So let's look at this app. Yeah. People agreeing with you in the Discord there. We, we can refresh the page and I still get my images that I've generated. And by the way, I think I shared earlier a link where you can go and generate and there's some credits. So let's think what it would take to build this before we look at the code. So we know how to construct some HTML, right? So this is a form just like Jeremy's todo form. There's an input and a button. And a title. There's some way of rendering our generations, right? And this looks a little fancier than just the raw HTML image. So there's going to be some kind of wrapper around this. We're not refreshing the page. So if I generate something else, it's not refreshing the whole page. And so we're going to be using htmx to selectively like update with our generation. And then there's one extra trick, which is that actually generating these images takes time. And so you can see here we've got this kind of like preview that says, oh, a generation is coming. And then without me refreshing the page or anything like that, once the model behind the scenes comes back with the image, it'll populate in here. And so I want to also show how you can do things like that and start to build up the, the pieces that you need to build something that's interactive and maybe can update on the backend and on the frontend. Yeah, so let's look at the code for this, and then I'll quickly show a couple of other apps without talking about like how it's made, but we can reason through what are the pieces we need. Okay, so this is my image app. There's multiple versions of this scattered around the place, so don't stress if the code here is not exactly the same. I'm going to be using Replicate— you've seen them in the course before— to actually generate the image. This is not an image generation tutorial, but this is some API that I can go and call. Now, unlike Jeremy's demos, I thought I'd make the database explicitly. And you can imagine here, like, if this was a deployed app, I might have a users table and keeping track of how many credits everyone has, what subscription plan they're on. Then I have generations and what date they were generated, what prompt they used, if they used any extra bells and whistles and settings. And so this is creating a database that's going to store that information.
And if you want that Well, not quite there, but there is the user_app.py in fasthtml/examples has a bit more of that stuff. But yeah, we should create some more examples of multi-table.
Right.
But it'll just be the same thing, right? It'll just be basically if something else not in tables, then something else.create to create your multiple things.
Okay. So starting with the styling. In case that's gonna throw people off. I wanted this nice grid and PycoCSS, which is like our default favorite, is pretty minimal, but they recommend using some other layout options if you wanna do something like a grid. And so I come to this page and it's got some examples and also any page that has things you can right-click, you know, inspect and see how they actually do it if you wanna get a feel for it. But this, especially where you get these long strings of CSS classes, I think this is using you know, maybe it's Tailwind or something like that. I don't know. As a Python developer, I kind of want to stay away from those. But it has examples. It's also a web page. And so you could do, hey ChatGPT, I'm trying to build this. You need some way to put the FastHTML in context, which I can share later. But you could show this example. Or what you could do is take this, and this is something I've done, copy and paste this HTML and then put this in a notebook. So somewhere here, when we come to the image generation app, yeah, this is what I'm going for. I can construct this with our FastHTML approach of using tags that are Python objects.
And one thing we didn't mention, Johno, is because the word class is a special word in Python, we can't say class equals. So we say cls class equals.
Right, yeah. So if you inspect the HTML, you'll see what it translates into, and that's div class equals, properly spelled out. Yeah, so to cut a long story short, I've taken this layout library that has some examples, I've turned it into effectively a Python version. I have to tell it to go and get the CSS, and so this is a link, just like you would have the little link tag at the top of your page. And so I'm going to use this to lay out my images. So back in the app, I'm including the link to that CSS and I'm telling FastHTML, put that in the headers whenever you make a page for me. And then this is my home. So before we were constructing the, the form and then the todos and then the todo details. Here I have the input for the prompt and I'm putting that in a form along with a button. And this is going to call a hx-post is going to send a POST request and it's going to tell me what I'm trying to generate. Then I have a list of previews, which we'll look at just now. This is the actual boxes that are going to have the image in them. I have a div to contain those. I've given it an ID. And so if you're thinking back to Jeremy's demo just now, we're thinking, ah, htmx could like request a preview and then put it right at the top of the the list of gens, right? So I've got this nice container. Whenever I send the prompt, what I'm going to get back, I'm going to put at the top of this list. And sure enough, yeah, hxswap=after-begin. That's exactly what that's saying. Right at the top, the beginning of this list, add whatever HTML you get back when you send this POST request. Cool. And I can give it a title and so on. So let's look at the POST request. I'm getting in a prompt and I'm going to generate an image using that prompt. And then I'm going to return back a preview. There's a few other things going on. One, I want to reset the prompt field. And so this is something that Jeremy hasn't talked about yet, but you can use an extra thing. So in addition to this piece, which is saying put this at the start of the list, I'm going to say, look, I'm actually going to pass back some extra elements. This one here is a new version of the input field, but it doesn't have anything in it. So we're going to clear out the old prompt. And the reason it knows to do that is that I set this special thing called hxswap_oob, standing for out of bound, right? And so when htmx gets back several different chunks of HTML, the first one is going to go in wherever the target was, right? So we said the target is right after the start of the generation list div. Right? But any extra pieces that have this HX swap out of band, they're going to go and replace whatever has that ID with this new version. So our original prompt input here, we're just going to replace that. And that's why when you type something in the interface, you don't see that same prompt sticking around. You'll instead see this nice Claire thing. Wonder why. Oh, okay. Yeah, sometimes the generations take a while. Cool. So that's the, that's like the, the framework. There's a magic trick happening because we're not passing back the image straight away. We don't have the image straight away. Instead, what we're doing, and this is something that you'll see a lot, there's some processing that needs to happen and it takes time, but I want my user interface to update like straight away, right? I'll show a chatbot demo just now. The chatbot might take a while to respond, but you want to start showing the response straight away. And so there's a pattern that you can do, which is to say, look, if the image has been generated, I'm going to return back my nice pretty image box. You don't have to worry about the div card image, etc. This is just like the nice pretty version that I prototyped in a notebook. But if the image isn't ready yet, I'm instead going to return a temporary thing. And the reason it's temporary is that this is also going to have an htmx trigger on it. Specifically, it's going to trigger every 2 seconds, right? So we said there's some events like click or whatever, but you can also set hx-trigger to be, you know, on hover or on change, or if it's dragged around, or in this case, just every second or every 2 seconds it's going to be polling. It's going to be sending a GET request to this route, which we can define. It's going to replace the whole thing, like it's going to replace itself with whatever it gets back, right? So every 2 seconds it's coming and hitting this, and what it gets back is just the same preview again. So the image doesn't exist, we do this temporary thing. After 2 seconds, it sends a GET request to replace itself. And it replaces itself with another preview, which again, maybe the image doesn't exist still. We do another temporary preview. So it's just this like polling, and once the image is available, we can now return a div. This has no trigger on it, so this is going to stop querying and querying and querying, but it's not going to show the actual image. And so that's why we can go from this little temporary generating thing to, uh, I guess Maybe my Replicate is, maybe you guys have used all my Replicate credits. A small possibility. Yeah. Okay, so that's what's going on. A final thing to note is the reason this happens at all, like the reason I can afford to like return some stuff when the image hasn't finished generating yet, is this generate and save function is running in a separate thread. So when I'm responding to the POST request, I basically start the generation and then respond straight away with the temporary preview. And this is a very useful pattern because then—
@threaded is a decorator from fastcore, by the way. If you chuck it on a function, it just makes it run in a thread. You don't have to use it. It's just a convenience. Yeah.
So you could do this with your preferred, like, way to start a new thread in Python. But in the background, it's sending a request to Replicate with the prompt and all the other settings. Waiting for the image to come back and then saving that. Yeah, so that's our image generation app. Some pieces to call out. One, I didn't start by writing this whole app like this, right? You start by saying, let me first get a page with a form. Okay, now when I send the form, I'm going to try and get the prompt and print it out. Okay, now I'm going to return the prompt. Okay, now I'm going to call replicate and return an image. Okay, that's taking a while. So now I'm going to do this little trick. So you build it up piece by piece. And each of those individual bits of functionality isn't too terrible on its own. And then for things like styling, this kind of thing, fortunately ChatGPT et al. have trained on a lot of web development and a lot of web development content, asking what this means, trying it out in a notebook, figuring out, oh, all this is saying is on really big screens, I want each column to be 3 units wide and there's 12 units total. Although no one really tells you this unless you happen to like already know this. On a medium screen, each one should be 4 units wide. And again, there's only 12 total, so there's only going to be 3 columns now. On a small screen, everything should be 2 units or 6 units wide out of 12. And so that's how we get this nice responsive thing. And that's a quirk of the specific, like the specific grid library. And every library is going to be different. but you can generally break it down to say, okay, I get all these complicated examples. What's actually going on? Oh, it turns out I need a div with this class and inside there should be divs with that class and I can construct this in Python. I can test it out in a notebook. Then I can start putting that in my app and gradually tweaking. Hey GPT, you know, I want my, my chat box to always be the right height. How do I do that with Tailwind? Oh, you should use this particular class.
This thing of having like, well, for units and splitting them up using classes like this seems fairly universal. I think it might have been Bootstrap that first invented it, but like, yeah, I see it pretty much everywhere now.
Yeah. But the point is like, hopefully you can get away without necessarily knowing that perfectly because you can get around it with these, these models that know it, or you can find a way to convert the esoteric looking HTML into some component that you can use. So another demo that we don't necessarily need to look at the code. Oops, I guess I already had something there. But yeah, we get a nice little, little bit of data back. So this again, like, this is not the perfect way to do this because I haven't gotten my head around WebSockets yet. But this particular chat piece here has—
it works, which is really cool. Like, yeah, like, you were able to use your existing knowledge. And in this case, you've used, I guess this is, you know, some component library you've found. You didn't write all this.
Right, right. Yeah. So that's something we should look at, right? So Daisy, um, oh my goodness, live coding. Daisy UI, um, and many other like collections of components, ShadCN. Um, there's, there's lots of work that's already been done on the web. To make beautiful components, right? And they're designed to be used in HTML and JavaScript and often in some other library. But when you go and look at them, okay, cool. Here, this is a very complicated thing. There's lots of different classes, but I can find some examples and say, I want to emulate this. This is what I need. Copy and paste this, replicate that in a notebook, figure out how to load this library, which is usually like, there'll be some information. Here's how you install it, or here's how you get it with a link at the top of your page.
Just that, that warning about the CDN, like I just always do things from CDN because we're not production, you know, like when you've got 100,000 users, then you can look at the warning. But yeah, just, just look at the CDN one and just do that.
Yeah. But yeah, so this is the general pattern.
Think of what maybe you want. Can we see the code where you like use this Daisy UI thing and?
Sure. I'll show it a couple of places. One is So in the, this is very much a work in progress document, by the way, but I show, okay, here's the code and then we can run this. Ah, and I should switch to this tab where you can actually see the outputs here. Sorry, just a second. Gotta get everything so you can see it.
So just while you're doing that, you know, the way I would do this, and I don't know if you've done it yet, is I would create a function called like chat or a function called chat_header and it would, you know, return those things. And then you could like have a thing, pip installable thing, you know, where you could like pip install fasthtmlchat, you know, and then everybody could use those functions. But all they're actually doing is just going div class equals, you know.
Exactly. Yeah. It's all about like defining that. So this is exploring it in a notebook. And then the next step up is, let me get rid of that.
This is great.
I think I have the chatbot one here. No, this is definitely not the final. Oh yeah, yeah, something like this, right? So a chatbox that takes a list of messages and a single chat message that has text and a role. And it's going to tell you how to return that. So like, that's the general pattern. And so then now, you know, in my main, I'm saying, oh, I'm going to have a chat box and it's going to start out with no messages. And then I'm going to make a chat message out of my message and a chat message out of the model's response, something along those lines. And that's gonna give me the nice little pretty UI. And as you hit things like, oh, okay, I want it to scroll and be a fixed size rather than, you know, doing whatever it does by default. Again, ChatGPT, hey, I've got this thing, it looks like this. What magic Tailwind text do I type after this to set it to some percentage of the window height?
Cool.
Someone asking, by the way, displaying. Yeah, I guess, is there any other things? Oh, I mean, I can say like, you don't have to build apps that look fancy at first blush, right? You can start with the behavior that you want and then you can add the pretty pictures. Pretty much everything I've looked at so far will say, look, here's our library. It's a CSS file and some scripts that you need to include. And then you can copy that. So here's my version of image generation that just says, look, what do I actually need to do this? Like right-click, inspect their source. And then you'll see, okay, they have, you know, a special class on whatever the fieldset is. And they have a special bit of CSS in the header. Nice thing with the web, you can just do this on any website. Like, how are they doing this? Oh, they're importing a script. And then there's putting some special decorators or they're following a specific format. And so yeah, we'll ship with, we'll say, look, here's how you do it with something that looks nice in Pyco, but people who are better designers than us can say, oh, I really like my Bootstrap components or whatever. And it shouldn't be too hard to replicate that.
Nice. Jenna, that's so cool. I love it. Okay. So I think that's kind of everything we had. So like, I know a lot of companies have like given out hundreds of dollars of credits or whatever for using these things. Honestly, for Railway, given it's $5 a month, just go and pay the $5, you know, I don't, you know, it's the amount of time it would take Jake to create a script to give you $5, it just honestly doesn't seem worth it. It's so damn cheap. And the extra 3 cents to actually use it. Yes, mate, go.
And if you are not happy with your $5, we are very, very much here. We are happy to make any sort of changes. We're super open to feedback. You can find me on Twitter, or if you want to call it x.com or whatever now, at justjake. So that's just and then Jake. My DMs are open. You can comment on random posts. You know, we're just looking to make the best possible deployment experience ever.
If you don't like it, Dan will refund your $5 for you, right, Dan?
Yeah, well, I mean, we'll refund it or Dan will, you know.
They made enough from this course, they can afford to. Yeah, and feel free to— so the repo is just dancer.ai/fasthtml. Feel free to, you know, put suggestions there and particularly feel free to talk about the library in the fasthtml channel on the fastai server. And then, yeah, like early to mid-July, we'll launch it. And so if you want to have pip installable modules of components ready by then, then yeah, you know, that's, that'd be great. And if you've got suggestions for things for us to add, that's all good as well. And then, yeah, hopefully mid-July we'll launch and people will be excited. And, you know, one of the things I want to do is kind of create something that works just like radio, for example, but it's written in FastHTML. So when people want to expand beyond that, they just like gradually build up with the complexity rather than having some kind of cliff where you have to learn a new language. So that's the plan from here. Thanks for having us, Dan. Hey, this was awesome to see. You probably saw on Discord, people have loved everything you've done in the past and are quite excited about this. So thanks so much. Thanks, man. All right. And thanks for joining us, Jake. Thanks, Jono, for your amazing help.
Yeah. Thanks for having me.
Bye all. See ya.
Cheers, everybody.
Bye.










