This is the final feature chapter before deployment. We'll add several Polish features that make the app feel complete and production ready. We will allow the user to trigger a new project dialog with prompt input, allowing users to create new projects using natural language. We're going to improve our use mutation hooks with optimistic updates for instant UI feedback making the app feel snappier and faster. We're going to implement billing, turning this project into a real SaaS and you will learn how to protect certain premium features.
In this example, we're going to protect GitHub's import and export feature, but using that example, you can protect under a premium feature whatever you deem worthy of a premium tier. And then we're just going to create some nice warnings whenever a user tries to open a binary file saying that we do not support the preview of that file. We can still of course have it in our database, we just don't support showing it in the code editor. And lastly, we're going to do some AI element styling and interaction polishing. So let's go ahead and make sure we have npm run dev running, npx convex dev and npx ingest cli latest dev.
Then let's go ahead and start by creating a convex system mutation. So head inside of your convex folder and let's go inside of system.ds. And at the bottom here, Let's go ahead and add it as the last function. We're going to create a mutation that will be used to create atomic creation of a project and its initial conversation. So the name will reflect that.
Export const createProjectWithConversation. It's gonna be a mutation. It will accept some arguments and let's just do a quick preparation of the handler. We already know the handler will have context and arguments and those arguments are going to be as usual the internal key and then project name, conversation title and owner id. Since this is a system mutation and we don't have authentication here we have to validate the internal key to make sure no malicious actors are trying to access this.
Let's store the current date into a variable called now and let's create a project by inserting into the project stable with the project name, owner ID, and updated at to be now. Then let's go ahead and create a conversation by inserting into the conversations table, passing along the recently created project ID, title from the arguments and repeating the updated at variable. Once we have those two, let's go ahead and return them all together so we can work with them since they are related to one another. So this will be used to very simply create both the project and conversation related to that project from one function. Great.
Once we have that, Let's go ahead inside of source app API folder. In here let's create a new folder called projects. And then inside of the projects let's create create with prompt. And then in here create a route.ts. Let's go ahead and import everything we need.
Zod, next response from next server, Auth, and then from unique names generator, adjectives, animals, colors, and unique names generator itself. Besides that we're gonna need clients for Ingest and for Convex. Make sure to import them from their paths respectively. And we're going to need API from convex generated API. And we're going to need to find our default conversation title which I store in here.
Let me show you. In features conversations constants. All right. Now let's go ahead and define what kind of request this API route will accept using Zod. So it's just gonna accept the very simple prompt with a minimum length of one, meaning it's required.
Let's export a POST request from here. And inside of this POST request, let's start with validation. So are we currently logged in? Can we extract the user ID from clerks out function? If we can't let's throw an error Then let's go ahead and check if we have an internal key.
So we know that the environment trying to access this convex mutation is a verified environment because we have no out for system queries. So let me just double check. Polaris convex internal key is the correct one. Great. If internal key is missing, throw 500 because this is something we should set up on the system this isn't the user's error this is truly an internal error if it happens let's go ahead and extract the body and let's parse the contents of the body using the request schema Zod object we've created above Now that we have a safely parsed prompt, we can go ahead and generate a random project name using the unique names generator.
So the project name will use unique names generator. I put these three dictionaries simply because this combination and hyphen and three letters, I'm sorry, three words just create fun project names and we are consistent with all the other places where we use this. I think it's in projects view. I think it's exactly the same. Adjectives, animal scholars, separator, adjectives, animal scholars.
Yes, it's just being consistent. Now let's go ahead and create both the project and the conversation together which we can now very easily do by calling a specific convex mutation right So we already know we're gonna return project ID and conversation ID. So let's await convex mutation, API, system, create project with conversation. If you're not getting autocomplete here, take a look at your server here. For example, I run npx convex, whoops, I meant npx convex dev.
So let's go ahead and make sure you have npx convex dev running and you should see convex functions are ready. So yes, in case you were having an underline here, it's because the function was not synchronized with convex. So just make sure you have npx convex dev running. Now we have to add the JSON, right? The body, so that's the internal key, the project name, the conversation title, which we're going to just use the default conversation title, and the owner ID, which is user ID.
Great. Now we have to create the user message. Basically, what did the user prompt for this project? So we're going to call convex mutation API system create message. We have already used this a couple of times in the project, specifically in API messages, so you definitely should have this in your system.ts in the convex folder.
Pass along the internal key, the conversation ID this message is being stored into, the project ID, the role, which we can hard code the user, because right now we're just storing the prompt, the natural language the user is using to explain what they want this project to be. And then we immediately have to create the assistance message with a placeholder for the processing status and we should get the ID of that assistant message. So let's go ahead and prepare that by storing the assistant message ID and calling await convex.mutation. Let's go ahead and call API.System.CreateMessage again just as we did above and let's pass in the internal key, the conversation ID, the project ID, role hardcoded to assistant, content being empty and status being processing right we are about to process the prompt that the user just sent us and the assistant message id will be the proper ID because the create message returns the message ID. So just make sure yours does as well.
Great! Let's go back inside of the assistant message ID here. We've done that. And now What we have to do is we have to trigger the ingest job to process the message. Lucky for us, we've already developed that.
So all we have to do is call the event message forward slash sent and pass in the data. Message ID being assistant message ID, conversation ID, project ID and message being users prompt. And once we have that let's return next response dot JSON and pass in the project ID. So what's important here is that you double check that you actually have this event in your project. You can see this is the third time I'm referencing that event ID.
So obviously it's the correct one. It's the processing message. It's the process message one in here. What's important is please just check that this message event is correct. So message ID conversation ID project ID message.
Make sure that you don't accidentally misspell something here because there's nothing stopping you, as you can see. You can type whatever you want, it's not going to throw you an error. So be careful, okay? Because the bugs around This might be a little funny, right? Okay, so now that we have that, let's go ahead and implement the UI for this dialog so we can actually test it out.
So we're gonna go inside of source, features, projects. Let's go inside of components and let's create a new file, new project dialog.tsx. Let's go ahead and mark it as use client. Let's go ahead and import useEffect, useState, ky, toast and userRouter from Next Navigation. Let's import Dialog, DialogContent, DescriptionHeader, and Title.
And then from the specific components AIElement prompt input, which we've added by installing AI elements. I'm not sure if that's actually, I think we run chat CN installation on it, but basically you should already have AI elements. If you don't, you can just, it is basically from AI SDK, AI elements, we already have them in our projects because we have been using it in the chat sidebar. My apologies, conversations sidebar. So let me just scroll up, There we go.
You can see we already used AI Elements Conversations and AI Elements Message and Prompt Input. And now we are again, importing from Prompt Input. So that's where we got that. Let's go ahead and prepare the ID from generated data model. Let's create the interface new project dialog props which will accept open and on open change and then in here let's go ahead and export the actual component.
So this is the usual scenario right Almost every dialog popover has the open and unopen change. That's simply because we are creating an abstraction over the composition that ShadCN has given us, which is always having an open and unopen change prop. So let's go ahead and prepare the router hook so we can easily redirect once the project is created. Let's prepare the user's input value here. And let's go ahead and give them a little submitting state for a nicer user experience.
Now we have to develop the actual handle submit method. So that's gonna be an asynchronous method and its message or its value, right? Its prop will be a type of prompt input message which we have imported as a type here from the prompt input. Now the handle submit will first check if there is no message.text and if there isn't any it's just going to return. Then let's go ahead and set isSubmitting to true and let's open a try and catch block.
Inside of the try block let's go ahead and call await ky.post api projects create with prompt So this should match 100% what you've written here app folder API projects create with prompt make sure there are no misspellings in this folder name and no misspellings in here create with prompt and route.ts is a required file name So make sure you didn't accidentally misspell any of that. We already know what we are going to extract from here, the project ID so we can prepare that and then let's just go ahead and add some body to this post. So we're going to be using JSON and we will pass along the prompt which is going to be message.text and just trim it. Great. And then we can go ahead and actually request the JSON back and to make it type safe we can go ahead and cast a type project ID to be a type of ID projects.
And just like that when you hover over project ID here it has the correct type and if you actually look in the route.ts of the create with prompt you can see we do return the project ID. Perfect, so now our frontend is completely synchronized with the backend. After that happens we can go ahead and render a toast success project created. We can close the model. We can set the input to be empty, and finally, we can push to projects project ID.
If catch happens, it's most likely because of an internal error or some invalid data. So let's just say unable to create project. And in the finally block, set is submitting to false. So regardless if this fails or succeeds, it will be reset. Now let's go ahead and actually do the composition of a dialog so we can render it.
So using the dialog component which we've imported above, let's give it an open and unopen change prop. Then let's go ahead and write the dialog content with show close button being false and class name being on small maximum width is large and padding is zero. Then let's go ahead and open a dialog header and I'm just gonna go ahead and give it a class name of hidden but it's a good recommendation to still add the title and the description for accessibility so screen readers can access it. So it's not going to be visible but screen readers can say what this model is doing. Great.
Now outside of the dialog header, let's go ahead and create a prompt input. And let's give it an on submit of handle submit and a class name border none with an exclamation point at the end marking it as important. Inside, let's go ahead and render prompt input body and then let's go ahead and render prompt input text area with the placeholder ask Polaris to build. On change, set the input to events target value synchronize the value binding to input and disabled is submitting. And then let's go ahead and open the prompt input footer.
Let's go ahead and render the tools and finally the submit button which is going to be disabled if user didn't type anything or if we are in the process of submitting so the user cannot spam. Great! That is the UI component and now let's wire it up so we can test if it works. So we have to go inside of projects components and we have to find the projects view right here. Let's start by adding the import first so I'm just going to add new project dialog and I'm just gonna go ahead and render new project dialog like we usually do and we need to have a state for it so new project dialog set new project dialog use state false Let's go ahead and create a little helper function here.
Handle new project open change open set new project dialogue open. And let's go ahead and add another if here if event that key is letter J. You can of course modify these hotkeys. Let's go ahead and prevent defaults and let's set new project dialogue to be true so we can open it. The hotkey perfect And I'm not sure if we even need handle new project open change.
I think this is simple enough. So in the new project dialogue here. Let's add this to be open and on open change set new project dialog open. As simple as that. Great.
Now what we have to do is we have to find a button which is this one. Which right now just creates a new project. So that's actually not gonna be the case anymore. Instead what's gonna happen is set new project dialog open will be set to true. And I don't think we have to do anything else here.
I think we can just test it out. Let me just see if we can now remove this import. Looks like we can. Perfect. We can remove use create project and the import.
So let's try it out now. I think this should work just fine. Just make sure you have your ingest running, make sure you have the necessary credits. Let's try a shortcut. There we go.
So ask Polaris to build a simple React plus Vite to do app. And once you click enter, it should redirect you to that project. And you can see that a new conversation has already been established, the title has been created and the AI is currently thinking and it should start creating the code any moment. And here we go. Just like that, we have kind of improved the user experience so that they don't have to first create a new project, they can like immediately prompt it, right?
And it's just a regular to-do app. Great, So what we have to do next is improve all of the missing optimistic updates. So I think when you search for to do in your app, you will see we have a bunch of these optimistic mutation ones, most of them instead of use files. So let's go inside of use files and see how we can improve that. Basically, this isn't required, it will just make the app feel snappier, right?
So let's make sure we have use mutation, use query, ID and API, we have all of that. And Let's now add a little helper function Which we're going to need since when we are optimistically updating we have to simulate the same behavior as on the server So we're going to implement a function to sort files. Folders first, then files alphabetically within each group. That's the logic we're going to be using. So let's go ahead and define sort files.
And let's go ahead and prepare a type here. So, T extends type, which can be either file or folder name, which is a type of string. So that's where we're gonna have files, which are gonna be an array of that type. And we expect back an array of that type. So, inside of here, let's go ahead and return spread files, sort them, so you have two files now.
If first one is a type of folder and the second one is a type of file, return minus one, which will sort them folders first. If first one is a type of file and the second one is a type of folder, return one, which will do the opposite, meaning again sorting folders first. And for the rest, let's go ahead and do local compare. So we sort alphabetically within groups. Great, so a little helper functions here.
So let's start by finding a use delete file. So in here, what we can do now in use mutation is call with optimistic update. And in here we have local store. And we have the arguments. And then what we're going to do is we're going to get the existing files from local store, get query, API files, get folder contents.
And in here, pass in the project ID, which will be arguments, project ID. Let's see if we have that or not. Looks like it doesn't accept that. So what we have to do is we have to extend use delete file. So let's go ahead and extend it by accepting project ID and parent ID because the problem is in the arguments we only have the ID of the file itself.
So in order to make the optimistic update work we have to find a way to make the hook aware of the project ID and the parent ID and that's kind of a problem. I'm gonna show you why. So make sure parent ID is optional here and let's now continue developing. So Now we know what project ID is, we know what parent ID is and now we can load the existing files. And now what we're gonna do is just check if existing files are not undefined, meaning they have loaded, Let's do local store set query and let's do API files get folder contents project ID parent ID, existing files dot filter.
Let me just fix the type of existing files dot filter. Get the individual file and check if file ID matches arguments ID does not match arguments ID. All right. So what this is doing is it is simulating what the actual backend function this one, deleteFile, is going to do but it is just simulating that, right? But you can already see that this might not be the greatest of examples simply because we should also kind of hide all its recursive children elements which isn't too big of a problem because on the front end I think the UI immediately hides that.
But let's actually try it out to see the example and why you would want optimistic update or maybe you prefer not doing it. So the first kind of caveat is that you have to complicate the developer experience. So if you search for use delete file in your project, you will find that you use it inside of tree.tsx. So let's go ahead and add it here. You can see that now I have an error here because I have to extend it with the project ID and the parent ID item.parentID.
So let's see the difference for example. Let me open one of the existing projects here and you can see that now when I delete it is instant. And I mean Instant. Absolutely instant. Faster than the actual backend.
So let's see the difference. Let's try commenting with optimistic update out. It's still going to be fast because convex is fast, right? Let me refresh just in case. But you will see like a very slight delay.
See, it's very, very small, but it is visible. Whereas with optimistic update is absolutely instant. But that's not the only thing. So for example, let's say that we want to throw an error. If true.
So let's simulate this. Let's always throw an error. What happens then? Still, Optimistic Update will immediately delete it, but then it should bring it back. You can see that's what happens.
That's the power of Optimistic Update. So it gives the user an idea of what was supposed to happen, but it has the ability to roll back if it goes wrong. So it's up to you if you want this compromise or not. I personally think optimistic updates really, really make the app appear faster because for the user it's almost like there are no network requests. For the user, the moment you hit it, it happens.
Instantly. Now, when it actually happens, might be 5 seconds from now. The user doesn't even know that your app is slow, simply because you've done a good job with optimistic updates. But the caveat is that we can see you kind of have a more complicated example here. So if you want to, you can follow along to see me develop the rest of this.
So rename file, I'm going to extend it with the exact same props here. And I'm going to go back inside of use files here and I will add the exact same props here so project ID and parent ID which is optional and I'm gonna go ahead and do the exact same extension, local store and arguments. We're going to fetch the existing files. But first let's just check inside of arguments. Okay, so we only have ID and new name.
Sometimes you don't even need to pass these simply because you have them in arguments but in this case we do need to pass them. And now what we're doing here is again checking if the existing files have loaded since in convex the result is never undefined. It's either a result or null. If it's undefined, it means it's loading. So now let's go ahead and go update our files.
So we're just going to go ahead and go over existing files and when we find our file with the ID, let's simply change the name using arguments new name and for all other files just return their current state. And once we have the updated files, we can simply add it to the local store using set query API files, get folder contents with project ID and parent ID. And see, very important, you now have to sort files again, because you can change the alphabetic order, right? So that's kind of the complexity that we have to do here. Okay, let me see if I'm forgetting to close something.
We have this we have this. Am I missing something? Let me see. Expression is expected. Okay, Expression is expected.
Okay, let me just check why this is throwing. I'm probably missing some... Something, well, definitely missing something. Let's see. Use mutation API file is renamed file with optimistic update.
This seems to end correctly. And then we open this that seems to end correctly to local story, local store get query API dot files that get folder contents is that may be problematic. I don't think it is. That seems to end as well. Then we have this, we have this dot map.
Let me see dot map might be problematic. Are we maybe missing something here? No. I'm trying to figure out what's the error. Let me see if there is even in this file.
It is definitely okay. Oh, I should not add a semicolon there. Great. So now the rename is also instant, right? So if I change this to instant, you can see it's immediately renamed.
If I go ahead and change it to alphabetically, you can see it's immediately sorted to the top, much faster than if you were to wait for a request to happen. Right, so that's the gist of optimistic updates. They basically make your app appear faster than they actually are. So a few more places to do this. Delete, let's see, create file.
Yes, so project ID and parent ID, save that. Let's go inside of use create file. Let's go ahead and prepare the props here. Project ID and parent ID. And let's go ahead and prepare the with optimistic mutation.
Okay. Let's check what we have in the arguments. So for example, in here, we have the project ID in the arguments. So we actually don't need to pass it here, which kind of simplifies things on this end, but we still don't have the parent ID. Yes, as I said, I mean, the developer experience is a little bit worse, but it might be worth it.
And then here for the project ID you're just gonna use arguments.projectID because you can Okay And now that we have the existing files here let's go ahead and again check if the existing files have actually loaded and we're just going to simulate creating a new file now. So let's go ahead and create an object for the new file. Let's go ahead and mock a random ID. Let's mock random creation time. Project ID.
Oh, do we have parent ID in the arguments? Let me see. Oh, we also have parent ID. Great. So we don't need any of them in the create file then.
My bad. Yeah, some of them work just the way they do. So arguments, parent ID. Great, that's even better. So parent ID, Let's go ahead and add name and content.
Let's make the type B file updated at now. And let's go ahead and fix this error by adding slint disable next line react hooks purity. And say optimistic update callback runs on mutation not on render to explain why this is okay great And then let's just go ahead and update the local store with the new file. So local store set query project ID arguments project ID, parent ID arguments, parent ID, And then make sure to sort files and just append the new file here. So then again, you will see this works instantly too.
If I go ahead here, new file, something TS, instantly added, right? There's no question about it. It works super, super fast. Great. So that's it for the use create file and it is almost identical to use create folder.
Let me see inside of use files if I scroll down here, create folder we have the parent ID and we have the project ID so we don't have to extend this at all. In fact I'm pretty sure we can just copy the entire with optimistic update here and chain it here like so we're just going to make it a little bit different. So the existing files all good. This is all good. Instead of new file this should be called new folder just so we stay true to what we're actually developing.
The type should be folder, there is no content and this should be new folder. So again the exact same behavior now. If I go ahead and create a new folder, super fast, immediately created. And again the same is true if I for example go inside of files, let me see, create file. If I go here, and if I decide to throw an error every single time, You can see what happens.
So new file.ts, immediately it gets created and then it's reverted, right? So optimistic update from convex handles all of that. It really makes the app feel just that much faster. All right, and here's what I would give you as a challenge now. I mean, if you want to, you can just watch me do it, but try and do it for project creation, right?
If you want to, you can see it takes like a second before it's created here. But it's not terribly important for it to exist here simply because you can see that even when it's created, we actually get redirected there. So it's not really important that the user instantly sees it in here. So we would implement this inside of conversations, hooks, use conversation. It would be use create conversation.
And let's see. So you just need to use the argument project ID. So if you want to pause the screen and try and implement optimistic mutation for use create conversation, and then I'm gonna show you the results. Okay, So for those of you who want to see the result, this is it. So we will start by loading the existing conversations.
We will map arguments project ID here. The date now we can just copy whatever excuse we had from the use files, right? So we get rid of that. Like so. And we are just creating a new conversation object here and again map the project ID as arguments project ID.
And you are updating the query getByProject. And don't feel discouraged if you didn't manage to get this yourself. The truth is this would be way easier if we added these optimistic updates when we developed these hooks. Because right now it's kind of hard to recall like why am I setting query in getByProject? How are you supposed to know that?
I now realize it's probably not that clear to you why we're doing this. Yes, obviously I have access to the original source code so I can see how it's going to look like. But yes, I just hope you understood that you basically have to create a new conversation then set it to the local query in a very specific cache. In this case, it is get by project cache. That's the one we want.
So actually what I showed you before was incorrect. This is the scenario when you click on plus, it will immediately kind of appear here. As I said, the optimistic mutation is not required everywhere. In some places it makes no sense to have it. For example, here, I don't think anyone will really see the benefit.
But in the files one, in the file explorer, it really makes sense because it makes the app feel that much faster. All right, so let me see if we have anything here. Looks like we have another optimistic mutation. Here's an example of where we really don't need it. Update project settings, really no need, right?
I think actually the only ones that made sense here were used files, even this last one in use conversation which I told you to try and do yourself, in my opinion, you can even decide to not do it. I don't think there's much benefit to it. It's just a good exercise, but not much sense in my opinion. All right, so what I wanna do now is I want to try and find that project I had, which featured an image. So right now I have this very kind of ugly to do implement binary preview.
So how about we just implement a nice error screen or a nice warning? So I'm just gonna close everything here and I'm gonna find editor view inside of features editor components editor view and here it is, is active file binary. So what we're gonna do is something much nicer. Let's go ahead and start with a container. So a div, class name, size full, flex items, center and justify.
Center, Then inside of here another div with flex, flex column, items center, gap 2.5, maximum width of medium and text center. Then let's render an alert triangle icon from Lucid React with size 10 and text yellow 500 like this. And let's go ahead and display a paragraph below. Whoops. So below the triangle, a paragraph with text small, which will very simply say the file is not displayed in the text editor because it is either binary or uses an unsupported text encoding like this.
So just some warning to the user like hey this is why that is happening. You can of course tweak this to make it look better, especially on smaller devices. Maybe even make the triangle smaller, larger, however you prefer. Great. Now it's time to add premium features.
Right now, this export functionality and the import functionality work just fine. So what I wanna do is I wanna protect them. So head to your clerk's dashboard and click on the Billing tab and let's click on Get Started. So in here, let's go ahead and click on Enable User Billing and see if we even have that available. And let's click Save.
Okay, so if this wasn't available for you, there's a chance it is because you don't allow user authentication, you don't allow sign up and sign in with email. So in order for billing to work, you need to allow signing in with email, you need to allow the require email address. So just look at my settings and make sure you have them like that. I mean, the clerk will tell you that when you try to enable billing. Great.
So once this is enabled, We have to create some subscription plans. They already created one for you, which is the free tier. And now we're just going to create a new one. And we're just going to call this pro and let the key be pro. In the description We're going to say this is a pro plan unlocking premium features of Polaris.
Monthly base fee, let's go ahead and set it to like $29.99. You can automatically create an annual discount. Oops, that is not the price I intended. You can automatically set a annual discount. So for example, if someone wants to pay a year in advance, you can go ahead and give them a different deal, right?
You can also enable free trial if you want to as well. So let's go ahead and save that. You don't really need to add features for our use case but if you want to you could specify exactly what plan has what features. And once you have at least one plan, it's important that you remember the key for this plan which in our case is PRO. So for example, let's go ahead and try something now once we have that enabled.
Let's go inside of import route.ts and in here so far what we do is we just check for the user ID but you can also extract has from here. So now let's go ahead and first check if we have the user ID and then let's do has pro to be has plan pro. This is the key that's important and if the user doesn't have pro return next response dot json error pro plan required with a status 403. As simple as that. So I'm going to go ahead and try do that now.
So I'm gonna, I think it might be a good idea to just restart your app simply because we just enabled something in Clerk. So just to make sure it still works, let's restart. Let's go ahead and try and clone something. Github.com, code with Antonio Polaris. Let's click import and you can see we have an error.
Unable to import repository and you can see the result is 403. That is because I should not be able to do that. I don't have the pro plan. Right. So now let me just go ahead and also add this to one more feature so we can wrap up our back-end coding.
Let's just add it to export for example. So after we check for that, check for has and extract has from await auth. Make sure you add it. So simple as that. And make sure you throw an error called ProPlan required because again we're gonna use that to check on the front end if we should tell to the user a specific thing.
So now let's go inside of the import github dialog component and in here we can catch errors. So we already catch if the error says GitHub not connected but now let's go ahead and do pro plan required like this. So if body error includes pro plan required and the body doesn't need the optional chain method let's throw toast error upgrade to import repositories. And let's add an action, label upgrade, onClick, open user profile. Like so.
And let's go ahead and call on open change set to false and let's do an early return so now we don't need to do this in an else if. So let's try it out. Let's see what happens. If I try import now, you can see it says upgrade to import repositories and then I have to go inside of what I now have which is billing and in here I have to switch plans so for example if I want to do monthly it's 29 If I enable annually it switches to 10. I can subscribe and I immediately have pay with test card mode right here.
And just like that payment was successful. If your hangs and it doesn't work it could be because you have an error here which will tell you that you don't have proper course set up. This is why it was important for us to properly configure the next config to use credentialless. Because if you use require corp, I think that's the other one, then it will block Stripe from being able to work. So make sure you put credentialless.
Or if it's still not working, try deleting the headers entirely or specifically maybe make them inside of projects like this. Then it shouldn't matter which one you use because the upgrade one is on this one. But still, Regardless of what you do here, I would recommend using credentialless because you never know where the user might get the update prompt. Great, so now this should work. Let me go ahead and try it.
But since this is going to be a long action, I will open localhost 8288. Let's try code with, oops, github.com, code with Antonio Polaris. I'm trying to import this very source code. You can see now it works. And yeah, okay, it's fetching the repo and I'm just going to cancel it because it's a big repository.
So it's going to take a while for it to load. You can see it's trying to replicate all the folders already. Great, but that works. Let's see what's up with export. So export should also work.
I'm just going to go ahead and create a random repository. Exporting to GitHub. And I'm not sure if it's going to export anything at all because we only have empty files here. But okay, it's unable to export because there are no real files, everything's empty. But we didn't get an error, right?
Whereas if you try with another account which doesn't have billing, which by the way, I think you can also control through your users. You can shut down their billing and stuff. Then this action would still show you the prompt that you have to upgrade. So it was that easy to turn this into a real SaaS, which is actually connected, can be connected to your Stripe if you go inside of billing and explore all the other things it can do. And here you have the dashboard, you can see your monthly recurring revenue, your total revenue, and your actual users here.
So it's that easy to enable billing using clerk. I think this took us like five minutes to do. Amazing, absolutely amazing. One thing we forgot to do. So the same thing we just did for pro plan required.
Let's copy that and let's go inside of export popover in here and let's do the same thing right here so if Pro plan is required set open to false and return And we can remove the optional chain for the body. Great. So now even in this export popover, we're going to get the same toast to open user profile and to allow the user to upgrade. Great. So I think the only thing that's left before deployment is improving this conversation.
Because If I ask it something like, create me a simple, I don't know, JSX snippet, you will see that this is a very weird color. It's barely visible against this background. And if we get a code snippet back, you're gonna see that it just looks bad so what I want to do is I wanna go inside of prompt input which you can find let me go ahead and see you can find it inside of source app components AI elements prompt input and in here we're just going to modify some classes. Okay, so I want you to return the markdown in here, not create a file. I'm trying to make it explain me some code in here and not just create me a file.
Anyway, let's go ahead and try and do something. So let's go ahead and find a component called InputGroup. InputGroup. All right. So InputGroup right now only has overflow hidden.
Let me see if that is the file I'm looking for. Are there other instances of input group? So okay, so find this one input group class name overflow hidden and let's give it rounded large and exclamation points. Alright, and that's going to make it a bit more rounded. Okay, that is step one.
Okay, and we can finally get some snippets here. Perfect. You can see they look very bad. Okay, now let's go ahead and find AIElementsMessage.tsx. Again, inside of source components, AIElementsMessage.tsx.
I want to find some class names here maximum width, so search for maximum width and change 95 to 80% I think it just looks better for our use case. I mean, these are just tweaks. None of this is terribly important, right? And now to make it look better against this background, let's go ahead and scroll down and in here, you're gonna find this big class name for the message content. And let's see, we should see a BG secondary here.
So group, if is user, it uses BG secondary. I want to change that to BG accent. And I think that that already should massively improve. There we go. You can see how nicer this looks now just by using BG accent instead of the other one.
And I think what's left is for us to fix the message response. Let's see. So scroll down and find message response. There's a lot of files here. Okay, here it is message response and in here in the stream down, let's add the theme to be one dark pro and one light as the alternative.
That immediately you can see fixes the look of the markdown. And in here, I do want to add some classes here. So let's do a target to inner div and use bg-accent and then another target to inner div and rounded medium. There we go. So just a slight modification.
It makes this that much more readable, right? There we go. I don't think there's a need to edit anything else, but yeah, this is the place where you can basically tweak any of those things. Perfect! So, what I want to do now is just get a few more things ready for deployment.
For example, you can see we just have a bunch of errors in this file right here and there are probably some other files where we have errors. And kind of the easiest way to test that out is by running npm run build, because this is what all of the deployment services we're going to use are going to run. So let's see, can we locally build something? This will not probably fail, because we have some unresolved files. So let's see which files they are and if we can fix them.
And looks like the first problem is actually in our layout here, which is interesting because CodeRabbit actually warned us about this. Let's visit the layout files that we have. Oh, it looks like we only have one project ID. I think the problem is that we are defining the project ID to be an ID of projects when it's actually a strings and instead we should cast it as the ID. I think just by doing that change, making sure that our params are not defined in funny ways, that should fix it.
But now I'm worried. I think that we did this a lot of times. So let's see params promise. We also do it in a page, projects project ID. So open that page, app, projects, project ID page.
And let's go ahead and do the same thing here. We're going to change this to be a string and then cast it as ID projects. So this way we won't have any problems with build. So let's go ahead and try build again and see what other files are failing. Alright so looks like we have as expected instead of AI elements we have some unused TS expect error directive.
I was searching here if we can perhaps use lint instead of build, but I found it doesn't yield the same results. So let me see confirmation.dsx inside of AI elements here. Okay, ds expect error. So if I remove it, it still has some errors here. So let's get rid of them.
Some more. Basically just removing everything until it's satisfied. If you don't want to do this there also is a solution for that you can visit nextconfig.ts And inside of here you can open TypeScript and you can add ignore build errors and set it to true. And this way you won't have to fix your build errors in order to build. But if you want to go along and fix this, let's go ahead and run npm run build until everything works.
I assume it's mostly going to be fixing the AI elements files. So as I expected, some more TS expect error. So maybe we can actually search through our code base, well specifically AI elements and click find in folder and search through that. Okay, looks like those were the last one in tool.tsx in the AI elements folder. So this comment, let's just remove it wherever it is.
And that seems to work. And I think while we are here, we can just focus on the AI elements folder. And let's like just try going over each file here and see if any of these turn red so we know that there's a lint error inside of them. For example, inline citation of mine has an error here and I'm not going to fix the way it works. I will simply disable lint for this line, for example, simply because I didn't write these components.
These were ShadCN added, so I don't want to accidentally mess up the way they work. And that's kind of the way you can fix all of these components of course. For example, prompt input has some problems. Let's scroll down. A bunch of anys, so I'm going to quick fix and I will disable no explicit any for the entire file.
Okay, and then I'm going to scroll down. Again, calling set state, quick fix and I'm going to disable that for the entire file as well. Looks like we still have some errors so let's scroll up here to find it. Again I will click quick fix and disable that rule for the entire file like that. Okay, prompt input, queue, reasoning, again, some problems in the reasoning.
I'm just going to go ahead and disable that rule. Right so I mean in production obviously you could take more care of these components but right now we just care about being able to deploy and I want to show you how you can go through these files and just add these SLIN disables so they allow you to properly build your app so that you can go ahead and focus on other things. And it looks like that's it. Great. So all the chat C and AI elements files are now fixed, but we are still, you know, not fully ready because we still have a components UI.
Any of these could be problematic too. So now I'm going through them just to maybe be ahead of npm run build if it fails. And the fix is exactly the same or if you really notice that you're not using a specific component You can also remove it. Just make sure there are no other components which use it as a dependency All right looking good so far. No errors Item keyboard label menu bar We really do have a lot of components.
Obviously you can get rid of those you're not using but I always like to add all of them. It's easier to work that way. Sidebar skeleton. Most of these look very good. Trying to find any that turns red.
How about the resizable? Okay, resizable seems to be problematic. It's exactly the one this caught and I think this is actually a problem in the version here. I think I saw it in GitHub. So I'm going to search if we actually use resizable anywhere.
We don't because we use allotment panels, we can just remove resizable in that case. Let's try npm run build again. Looks like we have some problems in components navbar use rename project. Let's see what that's about. That's a component we haven't worked in in a while.
:04 So it is inside of projects components navbar. And it looks like when it comes to use rename project, we were passing project ID, but we later decided we don't need to use Project ID. It looks like everything works just well without it. So again, npm run build until it works. It's not a nice process, but yeah.
:27 And sometimes it's different results on Vercel and on other, you know, wherever you deploy it than it's in your local one. So yeah, it's really fun to do. But you know, this is just to make sure you don't have any build breaking problems in your app. Most of this, you know, the app would work just fine. It's just that for safety reasons, type errors are very important.
:50 And there we go. So this is how it looks like when it all goes well. Finished TypeScript and you will see your app. Amazing. So let's go ahead and merge all of those final changes.
:03 So chapter 16, 16, billing and final polish, git add, git commit, 16, billing and final polish and git push you origin 16 billing and final polish. Then let's go ahead and open up pull request like we usually do. So compare and pull request and let's see the changes we've did. And here we have the summary of the last chapter. We created projects with AI-powered prompt descriptions.
:46 We added ProPlan requirements check for GitHub import and export features, we enhanced message display with improved styling and syntax highlighting, we improved binary file handling with informative messaging, We optimized file operation with instant local feedback, that's referring to optimistic mutations. And we refined project creation workflow with the new dialog interface. Amazing! We have two comments. In the export popover, in the toast error, While I do tell the user to upgrade, I accidentally copied from the import one.
:21 This should say upgrade to export repositories. So it's a minor issue, but yes, it's a mistake. And in here, we used class name hidden where we should have used a screen reader only or a component visually hidden to display this. So yes, go ahead and fix those two mistakes if you want. Other than that, let's go ahead and merge this pull request.
:45 Amazing amazing job. Let me go ahead and go back to the main branch, git pull origin main which will now synchronize those changes. There we go. We can now build our project and as always I like to confirm with my graph here that everything's fine. So 15 and now 16.
:06 Amazing. So the only thing left is to deploy. Let's go ahead and see what we've done here. We've created a new project dialogue with prompt input. We implemented optimistic updates for instant UI feedback.
:17 We added ProPlan billing gates for premium features, created binary file preview warnings and polished all the AI elements. Amazing, amazing job and see you in the next chapter.