{
  "video_id": "Xf9rHPNBMyQ",
  "title": "Build and Deploy a Cursor Clone | Next.js 16, React, Convex | Full Course 2026",
  "url": "https://www.youtube.com/watch?v=Xf9rHPNBMyQ",
  "transcript": "AI editors like Cursor have changed how developers write code. But have you ever wondered what's actually happening behind the scenes? In this tutorial, we're building Polaris, a complete AI-powered IDE from scratch. Ghost text suggestions, an AI agent that can create and modify your files. Live preview running entirely in the browser. And full GitHub integration. This is how modern AI coding tools actually work. Let's start with project creation. Open the new project dialog. Describe what you want to build in plain English. And watch the AI generate your entire project structure in real-time. Files, folders, components. All stored in the Convex database and available instantly, thanks to Convex's sync engine. No refresh, no polling. true real-time reactivity. And here's the magic. Your app is already running. No local setup. No terminal commands. Web containers give us a full Node.js environment right in the browser. You can see install logs, developer server output, and your live app all in one place. The editor is built on CodeMirror 6 with full syntax highlighting, code folding, and a minimap. But here's where it gets interesting. As you type, AI suggestions appear as ghost text. Just hit tab to accept. It's context aware, analyzing the code around your cursor to give you relevant completions. Now watch this. Select any code, open quick edit, and paste a URL. Firecrawl scrapes that page and feeds it directly to the AI. Documentation for a library that launched last week, a GitHub readme, API references, anything on the web instantly becomes context. The AI isn't limited to what it was trained on anymore. That's the power of FireCraw. For bigger changes, open the chat sidebar, ask it to add a feature, and watch it work. You'll see the thinking indicator. Then the AI starts calling tools, creating files, modifying code, building out your request step by step. Ingest handles the background processing, managing the AI agent execution, retries if something fails, and keeps everything reliable. GitHub integration is built in. You can import any repository. We use clerks.oalf to securely access your GitHub token. And when you're ready to ship, you can export directly to a new repository. Ingest runs these as background jobs, so large repositories import smoothly without blocking your UI. For authentication and payments, Clerk handles it all. Sign-up flows, OAuth providers, and now billing. Free tier, pro plans, all managed in one place. In production, you need visibility. Sentry gives us error tracking and session replays to catch and debug issues. They also have a new AI monitoring feature. Every LLM call is logged with model, token counts, and cost per request. Useful for keeping an eye on your AI spend. Throughout this tutorial, we're using CodeRabbit for AI-powered pull request reviews. Each chapter of our tutorial is a branch, and each feature is a pull request. you'll learn the complete Git workflow alongside building this product. Let's go over the full tech stack. Sentry for error tracking and AI monitoring, and CodeRabbit for our GitHub pull request review workflow. This isn't just a coding tutorial, this is a complete playbook for building AI-powered developer tools. And now, without further ado, let's get started. Before we dive in, using the link on the screen, you can get three months of Sentry Team completely for free. We'll be using their AI monitoring to track all our LLM calls throughout this build. If that sounds useful for your project, feel free to grab the deal. And now, let's build. In this chapter, we're going to learn how to set up the project. We're going to initialize Next.js with TypeScript, install and configure ShatCN components, add a theme provider, and configure the fonts and global CSS variables. Let's start by initializing Next.js. Here on the right side I have my code editor, Visual Studio Code, and here on the left side I have opened Next.js documentation page which you can visit by using the link on the screen. Click on get started, click on the installation tab, and scroll down to find the system requirements. It's very important that you make sure that you meet these requirements, otherwise you will have problems running Next.js. So the minimum node version is 20.9 and the supported operating systems are macOS, Windows and Linux. If you don't know your node version, go inside of your terminal and run node-v. If you get an error, it's time to install node. If you have a version lower than 20.9, it's time to upgrade. Either way, make sure you do that and then run this command again. And make sure you have a higher version this time and no errors. While you are here, also check for npx and npm. The version here isn't as important because different operating systems have different versions. So you don't need to have the same version as me and I don't really know what version is the most up to date either. Just make sure you don't have errors for any of these three commands. Now let's go ahead and let's install. Make sure you select npm since that's going to be our package manager. And I'm just going to prepare the command. I'm not going to run it just yet. So as you can see, that's why I told you to test for npx because we are using it right this moment. So I'm going to specifically choose a version 16.1.1 instead of just using the latest. And if you haven't noticed, that is actually my latest version. So why am I telling you to use that? Well, the reason I'm telling you to do that is so you don't have a hard time following this tutorial. Depending on when you found this video, that can be a month from now, six months from now, maybe even a year from now. And during that time period, a lot of breaking changes could have arrived in the newest version of Next.js. that isn't to say that you shouldn't upgrade you should upgrade but I recommend doing that after you finish the tutorial this way you will not have any trouble following along the way and you will be able to give yourself a challenge to finish you on your own after you complete the tutorial so it's not a bad thing that breaking changes arrive in Next.js a lot of times Next.js takes care of backwards compatibility. And if that's not possible, they offer migration scripts. So that's perfectly fine. And you should be up to date in case of any security issues or things like that. But just to make it easier for you to follow along and not encounter any breaking changes and avoid any frustrations, I recommend using the exact same version as me. So we are on the same page and we have the exact same code. So let's go ahead and run this command and let's give our project a name. I'm going to choose Polaris. I'm going to use the arrow keys and select customize settings. I will select yes for TypeScript, slint for the linter. I will leave it as no for React compiler. Yes for Tailwind. Make sure you select yes for source directory. Yes for app router and select no for customizing the import alias and give it a minute to install. Once it's finished, you're going to see a success message like this. So what you have to do now is first change the directory of your terminal into your newest project and then do the same thing in your code editor. So I'm going to go ahead and open folder and I'm going to select the one we just installed, Polaris. So now let's go ahead and let's review the file structure, package.json and things like that. As you can see, this newest Visual Studio Code's AI feature is a little bit buggy, which is ironic considering that in this tutorial we're building our own code editor, which will not be buggy. So let's go ahead and take a look at the folder structure. since we selected yes for the source folder we can obviously see the source folder here and inside of that we have an app folder because we selected yes for the app router it is very important that you have the exact same file structure as me and let's go ahead and confirm the package json since i initialized the command with npx create next app 16.1.1 that is the exact next version that I have in my dependencies and you should have two. And besides that, React 19.2.3 is, I believe, pinned to this version as well because of this option we selected. So you should probably have the exact same dependencies as me. For the dev dependencies, they are probably the same as well. Great. Now let's go ahead and let's run the project to see what's on our screen. so open localhost 3000 I'm going to zoom out and here we have the default landing page so I believe that depending on whether you use light or dark mode this can change as you can see so if you have a light background that's perfectly fine so how do you modify this file well very simply go inside of source app page The only thing that important to know for now is that page is a reserved file name so you cannot name it whatever you want It needs to be page And second important thing is that whatever you write here, so let's do a simple component, right? Const page. This doesn't matter. I mean the name of the component. It can be called whatever you want. So if you do something like this, it obviously won't work. It will throw an error. Even if you do this, it also won't work. It needs to be a default export and then it will work. So for now, that's the only important thing for you to know. Page is a reserved file name and export default is the required way of exporting within page.ts. X. Great. So now let's go ahead and test if Tailwind is working. So I'm going to give this text red 500 and it changes to red. Perfect. Make sure you save the file and then it will change. A lot of people forgot to save the file and then they think the Tailwind is not configured. It is. You just have to save the file. Great. Now you have probably noticed that I have this little a red color indicator, and that I also have these auto-completes, which actually are completely valid Tailwind classes. And when I hover over, I can see the underlying CSS. If you want the same thing, I highly, highly recommend installing Tailwind CSS extension. There we go, Tailwind CSS IntelliSense. I recommend this at the start of all of my tutorials. It is an amazing extension which will help you understand Tailwind much better and make fewer mistakes. Great. So now that we have this, let's go ahead and do the second thing. Install and configure Shadzian UI components. So for now, you can shut down your app. And let's head to UI.Shadzian.com. You can use the link on the screen to visit their landing page. Let's go ahead and click on the menu, documentation. and by default they select the installation tab but you can find it here as well. Pick your framework, for us that's Next.js, select npm and npx chatcn latest. Don't run the command just yet. What I'm going to do first is I'm going to use dash dash version to check what is the latest version. So for me it is 3.6.2 so that's why I will be telling you to use that version. Again, you can upgrade later when you finish the project, but in order to avoid any headaches and to avoid having different components, different props, I recommend using the exact same version as me, 3.6.2. So make sure you are running this command within your project. You should have some kind of indicator that you're within the Polaris folder and run npx chatcn 3.6.2 in it. This will automatically recognize the framework as well as the tailwind. And then go ahead and just select the base color. I will be using neutral. And give it a second to install. There we go. So right now, not much should change besides the folder structure, I suppose. We have a couple of new files. One of them is components.json, which is basically just a config file of the things we selected a moment ago. For example, our base color, we selected neutral. We didn't select this, but it automatically found that. So that's what this file is about. You will never have to modify it yourself, really. And instead of source, we now have a lib folder with utils.ts inside. This doesn't seem like a very descriptive function, but it will actually be one of the most used functions in this project. It will be used to safely merge or create conditional tailwind classes, which seems like a simple task, but if you're not careful and don't understand how tailwind functions, you can accidentally override yourself or create something you didn't intend to do. This simple util right here makes sure that never happens, and it has a very nice developer experience on top of all that. That's all you have to know for now. We don't have to do anything about it. So now let's go ahead and let's add some components, right? Because we just added ShatCN, we can see that the definition of it on their page is the foundation for your design system, but we are not seeing any components. So let's add all of them. So NPX ShatCN 3.6.2 add dash dash all. And this will add every single component from ShatCN to your project. and when it's finished you're going to see a message like this basically an output of all the components that were installed besides that chat cn command also modified our globals.css file which we are going to take a peek at right now but let's first take a look at all of these files what are these files well chat cn works differently from other component libraries primarily because it isn't a component library. It's a collection of beautifully designed components that you can use to create your own component library. So what do they mean by that? Well, if you open source components UI, you will see all of those components, which you can guess what they are looking at their names, right? But what's cool is by clicking on any of those, you can actually access and modify their source code. So that is something you usually are not able to do and that's what they mean by not a component library but a set of beautifully designed components, right? A composition of primitives that allow us to modify them and create our own design system, our own brand guidelines, right? They just give us high quality primitives so we don't have to build them from scratch. and on top of that they are very good looking from scratch so even if you don't modify them they look very nice but they are used to be they're intended to be modified right that's why a lot of times people say all chat cn projects look the same that makes no sense that's like saying all css projects look the same well yeah if you don't modify the css right so that's how chat cn is supposed to be used. So let's try it out. Let's go inside of app folder page.tsx. Let's remove this class name here and let's render a button from components UI button. And I'm going to add click me here. Make sure to save the file and do npm run dev. So I'm going to go ahead inside of my local host. And the first thing you're going to notice is that your theme has changed. So regardless if you are on light or dark mode, the background is now in light mode. So that's perfectly normal. Don't worry. And here we have our button, which simply says click me. So what's cool about this is that this button, for example, has variants. Let's select a variant destructive and safe. And what happens is it changes the color red. And what you can do is you can command click inside of the button or find it instead of source components UI button and scroll up to find the button variants. And inside of this variant object, go ahead and write your own, for example, purple and give it a background of purple 500 and text of white and make sure to put a comma at the end. Then save the file, go back to page, change the variant here to purple, and guess what? You just modified the source code of the button and added your very own variant. That is what they mean by customizable, right? Usually when you use a component library, you don't even see the code of the button. You just import it, and the source code is in node modules, but you are not intended to look at it, nor are you intended to modify it. That's the difference. And this is actually a very good way to learn about how to create high quality components. You can always take a peek from the pros. So from now, I'm going to remove this purple variant and just revert it back to how it was. Great. So one thing that's bothering me now is that my project is in light mode. And most code editors are in dark mode by default. Case in point right here, right? So how do we hard code our project to dark mode? Well, if you take a look at ShadCN and inside of its documentation here, you can find something called dark mode. So let's take a look. Let's click on dark mode and let's click on next.js. Even if you can't find this documentation, don't worry. It's very simple to do. You can just follow my steps. So first things first, let's install Next Themes. So I'm going to go ahead and do npm install next themes. And I'm going to show you the exact version that I have installed in my project, even though I don't think it really matters. This is not really a breaking thing. So my version is 0.4.6. So if you want to use the exact same you can 0.4.6. Great. Once you have next themes installed, you can create your own component. So let's go inside of source components new file theme dash provider dot t s x. The reason I didn't put it inside of the UI folder is because I want to keep that reserved for Shatsy and components, basically components I didn't create. This way it's easier for me to keep track of things I have to take care of versus things I only have to, I don't know, upgrade, right? Kind of a mental separation, at least for me. So open the theme provider here and we're just going to copy this code. Again, don't worry if you can't find it, I'm going to pause the screen so you can copy. We add the use client interactive, which turns this into a client component because by default components are server components in AppRouter. And we import React, we use our new import next themes and we export a component called theme provider which very simply propagates the props and renders the children as its well children Great So once you have the theme provider let's go ahead and go inside of source, app folder, and find layout. So same rule as in page. Layout is a reserved file name. And inside of it, you will find a couple of weird things. This random export of metadata and then this random export of root layout. So this can be thought of the same as the default export that is required in page, right? We need at least one default export and that's what kind of registers the layout or registers the page. That's how file-based routing works. So basically for now, just go inside of this root layout and go inside of HTML, body, and simply add theme provider from components, theme provider. Make sure you're using the one you just created. So theme provider from components, theme provider. When it comes to imports, I like to have a specific sorting organization. I like to keep the npm ones at the top, followed by aliased ones, and then these ones at the bottom. You don't have to follow me, but I really like it this way. And once you do this, I think just by adding this, you might actually get some errors. There we go. Yes, you can see that we have some errors here. that's perfectly fine and in fact the next step here tells you how to fix that we have to add suppress hydration warning prop to the html tag so i'm just going to add that here there we go and now what i want to do is i want to change this theme provider to default to dark mode so attribute will be class default theme will be dark i'm going to choose enable system and disable transition on change and now when you refresh your app you will see we have switched back to dark mode so even if i change this to light mode you can see that my browser has changed to light mode, the app stays in dark mode, which is exactly what we wanted to achieve because most IDEs are in dark mode by default. Of course, later on, you can add light mode. That's why I did it using the theme provider and not directly by modifying the global CSS in a way that just removes the light mode. So if you ever want to develop light mode later on, you will be able to do that and you will be able to change this to light. But for now, let's do it in dark mode. Now, while we are inside of this layout file, I also want to modify the font. So we're going to have two fonts. One will be our sans and one will be our mono. So instead of geist sans, I'm not sure how to pronounce it, I'm going to be using inter. we can import that from next font Google. And instead of gaste mono, I'm going to do IBM Plex Mono. So change this to Plex Mono. Let's change the variable of both of them. This one to be font inter, and this one to be font Plex Mono. And besides these subsets, the mono one also needs the weight. So make sure to add these. Now let's go ahead and add the inter variable and plex mono variable. So if you have a good eye, you might have noticed the change. If not, perfectly fine. We don't really have a lot of content right now to display these new fonts, but why not do it while we are already here? But there is one slight problem. This font didn't even load actually, and that is because if you take a look at globals.css instead of source app, you will find that font sans has defaulted to font gaste sans, whereas we have changed it to font inter. So only when you save this and refresh will it actually modify. But that's not how we are going to change this global CSS. Instead, I have prepared a link, which you can see on the screen. And using that link, you can access Polaris Assets GitHub repository. And in here, you can find myglobals.css. You can see that here at the bottom, I have even added some very specific CSS, which we are going to need later. I will try my best to remind myself about this and explain why I have added this. But I don't think it makes sense to, you know, do this any other way than to copy and paste. It's not a lot of learning material to write each line of globals CSS. I basically just changed the way and look until I liked how it looked. So that's why I'm telling you to copy it, right? I'm not trying to teach you anything less. So just go ahead and remove your existing globals.css and paste the new one inside. And what this will do is it will create a slight modification, as you can see. We are no longer using that black background. Instead, we are using this lighter one. And this will actually look very well once we have all the elements of the IDE, like the code editor and the file explorer and all the other things. Great. So what I want to do now is I want to push these changes to a GitHub repository so that we can start tracking each of our features and chapters in a branch. so in order to add this project to a new github repository we first have to create one make sure you have a github account and once you do click on the plus button and click on a new repository right here one thing i'm going to change that you don't have to change is the owner so i'm using my organization code with antonio you don't have to change this it really doesn't matter so i'm going to go ahead and call this Polaris. And for now, I'm going to keep this private. And I don't recommend adding any of these because we have all of them right here. So just create a repository. Great. So now we have two options here, create a new repository or push an existing repository. This is our case. We already have a repository. It's just local. So we're going to use these three commands right here. So let's copy these three commands right here. Let's go ahead and shut down our app and let's do the following. Inside of here do git add, git commit and let's go ahead and call this 01 and I'm going to call this setup, right? The same as we called it here. So let me just be specific, project setup. And then once we've done that, you will also see a change here. So of course you can use the visual editor here or you can use the command line. I kind of prefer the command line, but a lot of you find it easier to use this. That's also perfectly fine. But for this first attempt, I recommend doing the same thing I'm doing. So I did git add and then a dot, which basically staged all of my changes, which was, as you can see, 62 new files because we added a lot of new components. And then I committed with a message all of those staged files. And then what I'm going to do is I'm going to paste those three commands, which we have copied from here. Git remote, git branch and git push. Go ahead and enter that. and there we go. You can go back here and refresh, and you now have your newest project right here ready. What this will allow us to do is use branches, so then in the next chapter when we commit that, we're going to push that to a separate branch, and then you will have a very nice overview of all of your changes and all of your progress, and if you ever wonder what was your state in in, I don't know, chapter five, you will simply find chapter five here and you will able to go back to that state of the project if you want to learn in that specific way. I think it's a very useful thing to do because you also learn the proper Git workflow. Great. So one thing which you can always do is you can use this, synchronize changes, and then you can click OK. What this will do is this will basically push and pull from your remote branch. What is a remote branch? This, right? What is on the internet on GitHub is remote and what's on your laptop on your machine is local. And this is a button which synchronizes the remote one with the local one. Great. So amazing, amazing job for the first chapter. You did all of this. You initialize Next.js with TypeScript, you initialized ChatCN and all of its components, you added a theme provider and changed the default to dark mode, and finally configured fonts and global CSS variables. And on top of all of that, you've set up a new GitHub repository. Amazing job, and see you in the next chapter. In this chapter, we're going to add authentication to our project. We're going to set up clerk account and SDK, configure authentication provider and middleware, create sign-in and sign-up flows, and then test it all out by building authenticated and unauthenticated views. No need to have your app running at the moment, because we're going to visit Clerks page using the link you can see on the screen. Go ahead and create an account and visit the dashboard. In the dashboard, you will have a list of your previous projects or a button to create your very first application, which is what we are going to do. Let's click on create application. I'm going to give my application a name, Polaris. And for the sign options make sure you have email turned on This is because clerk billing needs this sign option to work As for the other providers it really doesn matter You can add as many of them as you prefer But one that is very important to enable is GitHub. This is because GitHub OAuth token will allow us to later import users' repositories, even private ones, as well as create new repositories on their behalf, which is a crucial feature of our editor. So for simplicity's sake, I'm just going to enable GitHub. You can, of course, add as many of them as you want later. But make sure you have email, email, and GitHub, as those are the important ones. And let's click create application. Now let's go ahead and let's run the following terminal command so that we can install clerk. I'm going to go ahead and paste this, and then I'm going to show you what version was installed. there we go let's go ahead and check the package json so you can see exactly what package i have you been using there we go clerk 6.36.5 so for those of you who want to use the exact same version you can install it like this great now let's go ahead and follow the instructions so we've just added the package and now we have to set up clerk API keys. Add these keys to your .environment file. Let's go ahead and copy these and let's create our .environment file. So in the root of our app right here, I'm going to create a new file .environment. Here it is. And I'm just going to paste this here. So we now have next public clerk publishable key and clerk secret key. It goes without saying that you shouldn't share this with anyone. Since I'm making a tutorial, I'm sharing them, but obviously I will remove them later. I like to add comments to my environment so I know exactly what keys refer to which service. Great. Step two, finished. Let's see what else we have to do. We now have to create what was previously called a middleware. You can see they even have a note about that here. So if you're using Next.js lower than 15, name of your file is middleware. So since we are using next16, we are using a new name proxy, which is honestly a good thing they've changed because middleware was just confusing people because it doesn't actually work as a normal middleware. So depending on whether you have the source folder or not, your middleware file actually goes in a different place. Let me see if they actually tell you that here they do. So you either create one at the root of your project or inside the source directory if you're using that. Since we do, let's go inside of source, create a new file, middleware.ts. My apologies, old habit, proxy.ts. Even though middleware would still work, it will throw a deprecation warning in the terminal. So let's go ahead and copy this code from here and paste it inside of here. So it kind of makes no sense for us to write this. That's why we are copying and pasting. If you want to, you can pause the screen and then write it on your own. But it's just a big regex rule, which covers a bunch of things. Great. And now let's go ahead and let's add a clerk provider. So you can see they are instructing us to add some code to the layout file, which we are already familiar with at this point. So let's revisit it. Source app folder layout.dsx. And in here, let's go ahead and well, let's import all of these. So clerk provider, sign in button, sign up button, signed in, signed out, as well as the user button. I'm just going to add all of them here. There we go. No errors because we have all the packages installed. Great. So let's see what else do we have to do. We have to wrap our entire app with a clerk provider. So I'm going to go ahead and do that right here. And let me indent the rest. And now I'm going to go ahead and go inside of the theme provider and I will create a signed out view. And inside of signed out, I will add a sign in button, which will be a self-closing tab. So all users who are signed out are going to see a button to sign in. And let's add sign up button here as well. Let me go ahead and move this here. Obviously if you want it you can just copy it from here but I'm writing along just so we kind of learn how to compose clerk components. And no need to do the whole class name thing. I think they're just trying to demonstrate how you can modify the look of the sign up buttons in here I'm just going to give it a background rows 500 and the text of white so we have a points maybe some padding rounded and I don't know that's it and for the signed in view let's just render a user button which will allow us to well sign out great yeah and we can kind of keep all of that within a header so it's kind of like a navbar there we go let's go ahead and do npm run dev and we should be able to access our app at localhost 3000 and I think at this point it should already be working there we go so I'm zoomed in a lot but basically you can see that above my click me button, which is inside of my page.tsx, I now have a sign in and sign up buttons. Both of them will actually do the same thing right now because I have no accounts. So as you can see, we are redirected to another page with our sign in box. And in here, I can use GitHub to create an account. So I'm going to go ahead and do that. Let me authorize clerk. and I will be redirected back to my local host app. There we go. And you can see that now right here, I have my account. And when you click on this button right here, you can click manage accounts and you can see a bunch of info about your account. We are going to explore this later, especially when it comes to billing. The only reason I'm not clicking it right now is because it's gonna show my email address, but feel free to explore the manage account settings. and you can also sign out from here as well and that will lead you back to the unauthorized state perfect so this is how easy it was to add authentication using clerk obviously it goes even more than this but it doesn't make sense to explore this any further before we add convex which will be our database provider our sync engine our real-time database basically our backend. And once we add that, we're going to have to incorporate that with clerk. So I want to wait until we do that so that we can, in a smarter way, create components that will combine both and work nicely together rather than building something now and then having to change it altogether. but it goes without saying you can control how your user sees when they are signed in and when they are signed out in many different ways and one of them is by using composition components like signed in or signed out but obviously you will be able to do that also using the middleware or what's called proxy now right what I suggest you do is you follow the guide and there we go okay so my email is shown here now which is what i was trying to avoid but okay not a problem what i would suggest you do now after you do this is visit the documentation in fact we're going to do it together so i can show you a bit how all of that looks like so you can click on any of these actually and they will lead you to the documentation for example add security and in here you will be redirected to next js SDK here and you can read all about this right But if you want to get familiar with their components, for example, you can do that as well. Let's go ahead and scroll a bit up. We have user management here, UI components. There we go. So we have the clerk provider, which is what we've just added, right? And in here, we can see all the props that clerk provider has. And for example, one of the props that we are going to use is going to be the theme. So that's something you can figure out if you go through the props, right? So let's actually try it out. Let's go inside of layout here. My apologies, it's not called theme, it's called appearance, right? So appearance prop, and go ahead and add a base theme here. Actually, let's do theme as base theme is deprecated. And in here, we can use the dark theme. Keep in mind that now we have to install that. So let's go ahead and do that. So we're going to have to add a small package here. npm install clerk slash themes. As always, I'm going to go ahead inside of my package.json clerk themes 2.4.46. So if you want to, you can use that specific version like this. Now that we have clerk themes, we can import dark from that package. Let's do dark from add clerk themes. There we go. And now the clerk provider will match all of its UI components according to the theme we have given it. which is exactly what we want because we have kind of defaulted our app to dark mode, if you remember. And now you can see how it opens in dark mode. So basically that's the kind of thing you can learn if you go through their documentation and figure out all the things that they have. Here it is, appearance, which accepts the theme. And then you will get to the themes here. And this is what we just did. We added clerk themes, right? So obviously I've been using clerk for a long time. So I do all of this, you know, at the top of my mind. But this is how I learned that, right? I went through the documentation and I learned how Cleric works. We're going to go back to this documentation later whenever we need to explain something. I think it's a very, very well-written documentation. That's why I'm pushing it so much for you to visit it, because it's a shame if you don't do it. Usually, the commentations are not very...\nvery easy to read, but Clerks is very, very well done. And I would recommend that you do that. You know, just kind of get familiar. What is Clerk middleware? How does it work with the app router? What is this out helper, right? How does that work? And you will actually find some useful examples about how you can authenticate your page and maybe you can even play around with it. We will explore most of this simply by building the project anyway. But if you want to do it on your own, I highly recommend that. That's always a good way to learn something. Excellent. Other than that, I believe we did what we wanted in this chapter. So we have set up clerk count and SDK. We have configured the authentication provider and the middleware. We've created sign-in and sign-up flows. And we also build authenticated and unauthenticated views using their composition components. So in the next chapter, we're going to join this with Convex to make it even more powerful. So for now, let's go ahead and do what we usually do, and that is commit this and create a new branch. So I'm just going to go ahead and close everything here. As you can see, I have four files changed, package.json, package.lock, proxy file, and layout, where I added all of the clerk stuff. So I'm going to go ahead and do git add dot, which will add all of them. You can see four files have been staged. Then I'm going to do git commit, like this. And now what I'm going to do is going to be a slight change. I'm going to create a new branch. So git checkout dash b, and I'm going to call this zero to authentication. And you can see I have a message now switched to a new branch, right? And now that I'm here, I'm going to do git push u origin 0 to authentication. Whoops, it is not with a slash, it is just origin and then the name of the branch, which in our case is 0 to authentication. There we go. And you will see that the same thing is happening here in the visual editor. So down here, you can see I am on 0 to authentication. So now let's go ahead and go to our GitHub. And you will now see that once we push this new branch, we have this prompt. Zero to authentication had recent pushes 26 seconds ago. In case you are not getting this notification, you can always manually go instead of pull requests here and create it. But just confirm that you actually have that new branch here because we just pushed that branch to remote. So it should be visible right here. So I'm going to go ahead and this time just create it manually. I'm going to go instead of pull request and I will click new pull request. So base is a main and we are comparing that to our new branch. There we go. So we only have one commit here, which I have conveniently named exactly the same as our branch. So we know that's the one. So I'm going to go ahead and click create pull request and then I'm just going to create the pull request. And now we are going to review our changes. As you can see, my pull request has been reviewed by something called CodeRabbit, which probably isn't the case for you. And that's perfectly fine. That's because I'm using CodeRabbit to review my pull requests automatically the moment they are created. And you will be able to do exactly the same. So I'm going to explain all about it in a second. but for now, let's just actually go over what happened in this pull request. So the first thing that happened is CodeRabbit provided me with a summary of this pull request. New features. We integrated user authentication system with sign-in and sign-up functionality. We added user account menu for authenticated users. And finally, we enabled security across the application with authentication middleware. So that is the high-level summary, which is always useful in itself. and then below it actually went file by file and found some what it thought was a bug. So in here it noticed that I have a file called proxy.ts and it labeled this as a critical failure. It said incorrect file name for Next.js middleware and I know what you're thinking. It's not exactly a great first impression for an AI review tool to not have the most up-to-date information. But give CodeRabbit a benefit of the doubt, especially with what I'm about to show you next. So in the last few projects, CodeRabbit has found multiple real critical security issues in my code. I think it is in general a good idea that between you and me, we have a third person, AI, whomever, reviewing the code. Because as much as I think my code is okay, sometimes things slip. And if this wasn't the case, if proxy truly was a misnamed file, it would have been a critical failure and middleware would not have worked at all. So it is actually impressive that CodeRabbit wrote this comment. But here's what's even more impressive. I actually responded that middleware has been renamed to proxy in Next16 and I've provided with the documentation. And you can see the response. The usual, you are absolutely correct. I know we are all tired of hearing that. It apologized for the incorrect review comment and thanked me for the clarification. It even gave me some tips on how I should be exporting a function called proxy if I want to write some custom logic here. And here's the cool part. I can see learnings added. So from now on, I have successfully taught CodeRabbit something new. And this is where you can actually see what a high quality tool this is. Even though this encounter might have seen a bit annoying with its lack of the most up-to-date knowledge, we all know that AI is really struggling with that. The fact that you can feed it new information makes it really, really good because this probably won't happen again now. And I think this will stay true across my entire account. I think that the next time I start a project, this won't happen again because it now has this learning. So I think this is a super cool example, even though it looked like a big failure, it is actually really, really cool. And I promise the more complex our pull requests get, the more useful CodeRabbit will be. Using the link on the screen, you can add the same thing to your GitHub account. And with a literal two-click install, you can add it to all of your pull requests. They have a super generous free tier. It costs you nothing to add this. It would mean the world to me if you would use the link you can see on the screen to create an account. So since this was a super simple pull request, we actually don't have too many things to do besides merge it. So I'm going to go ahead and just merge this. I'm not going to change anything and I'm not going to delete my branch. This way whenever I want I can just go ahead and switch back to zero to authentication and then I can see what I was doing back then. What we have to do now is we have to go back to our main branch here and then we have to, my apologies, we have to pull from origin main. So git pull origin main. There we go. So you can see that I was ahead, but now I am on the same branch. Great. My apologies, this syntax keeps confusing me. So it is git pull origin and then space main. Perfect. So what this means is that now I have completely synchronized my local main branch with my remote main branch and we can confirm that with the visual editor here as well by clicking on the graph here. So we have this initial commit from create next app we then have our first commit which was in our first chapter project setup and then you can see that we have branched out into this zero to authentication branch and then we merge that back into our main branch here. So you should have the exact same graph here. Excellent. I believe that marks the end of this chapter. So going on this is how we are going to end all of our chapters with a branch so we can always easily come back to it and so we get an actual code review of our changes which will come in very handy when we start doing some security things. Amazing, amazing job and see you in the next chapter. In this chapter, we're going to set up our database. This will include setting up a new Convex account and a new project, installing a Convex SDK as well as the CLI tools, creating a table and testing out some CRUD operations on it, and finally configuring the Convex provider together with clerk authentication, which we developed in the previous chapter. So for now, no need to have your app running at the moment. In fact, we're going to visit the Convex website. So using the link on the screen, you can get to this page that I see right here. So why are we using Convex for this application? Well, primarily, one of the biggest problems that I had was how do bring a native file explorer feeling to a browser cloud-based IDE? Because we are not writing files to any file system, right? We are writing them to a database. So we are either going to store text content or binary content, depending on what type of file it is. So how do we make that feel native? How do we make it feel so that the user is using an app, even though this is a browser web application. In short, with Convex, our app will update in real time automatically. There will be no web sockets to set up, no polling, no cache invalidation headaches. It just works. And you will especially see this magic when we build the file explorer. But honestly, you will see it everywhere else as well. So this is why I've prepared the finished app so you can actually see what I'm talking about. This is the finished result and I have purposely opened it in two different browsers. So I'm going to go ahead and just quickly create a new file something like test.tsx and you can see immediately it was created here and here right. So if I go ahead and rename that to something you can see that immediately it's been reflected elsewhere. That's the power of convex. Same thing happens if I create a new folder for example app folder right My apologies this was a file So if I create an app folder you can see immediately all the things are reflected here If I go inside of here and create a new file, let's call this container.jsx, immediately reflected. So this is byproduct of using Convex. It's amazing sync engine that makes every app feel native. And this is reflected even further besides the file explorer. Take a look at the title of this project, right? So I'm using some random unique slug generator. So I'm going to rename this to my project. Immediately renamed in the other side as well. That's what I'm talking about. That is the power of convex. So let's get started and let's create an account. And on the dashboard inside, you're going to see a list of your projects or an empty screen if you have none. So I'm going to go ahead and create a new project. I'm going to call this Polaris and I will click create. Once you've created your project, you don't really have to do anything here, but you can leave this tab open. Now let's go ahead and go to convex documentation here so we can actually follow a simple getting started guide. So in here you actually have quick starts next.js. So since we already have the project, we are not going to run this command. Instead, we're just going to go inside of our app and run npm install convex. And then I'm going to show you the exact version of convex I will be using through this tutorial. So make sure you are inside of your project here and run npm install convex. And I'm going to go ahead and prepare package JSON here. So you can see exactly what version has been added. So 1.31.2. For those of you who want to use the same version as me, you can install it like this. Once you have convex inside, let's see what the next steps are. So now we have to set up a convex dev deployment running npx convex dev. This will prompt you to log in with GitHub, create a project and save your production and deployment URLs. It will also create a new convex folder for you to write your backend API functions in. the dev command will then continue running to sync your functions with your dev deployment in the cloud so let's go ahead and run this command right here so depending on if you've run this before or if it's your first time running it you are probably getting a login link clicking on that login link will lead you to the login screen and then you just have to log in with your account and then it will basically be able to either create a new project in your account or you can choose an existing project. And in here, you should be able to see Polaris. So I'm purposely doing it this way. So you know that you did it correctly, right? We created a new project called Polaris here, and you can now select it here. There we go. And as you can see now, something that will happen besides just the connection is we have a new folder and we have some new things in our dot environment here. So let me go ahead and close everything here, open Polaris. And here we have the convex folder. So right now you can ignore the generated folder. The underscore here kind of represents that it's not meant to be modified and we will never modify it. It will automatically be modified once we add things to our schema or our functions, right? This is kind of like Prisma's generated folder. Another thing you will notice is that we now have dot environment dot local. So what I suggest actually is move this to one dot environment. I'm going to see if maybe we should keep it in dot local or dot environment. For now, I'm just going to move all of them here like this, and I'm just going to change this to be convex. And if you want, you can leave these comments so you know what your team is and what your project's name is like this. So you should have convex deployment and next public convex URL. And for now, let's just remove .environment.local. And I will test this out in just one simple way. I'm going to do npx convex dev again. And if this continuously creates the .environment.local file, then I will just move it to that. Looks like every time we run npx convex dev, it will create .environment.local. So instead of moving convex to .environment, let's move clerk to .environment.local. I think that might be a better solution. So move clerk here, and let's go ahead and change this to convex, and then remove .environment. Sorry for going around, but I just wanted to figure out which was the one that gets created all the time. In your new .environment.local, you should now have the same content that you had before for clerk with the addition of new convex variables. And this time when you do npx convex dev I believe there should be no changes to your .environment.local. There we go. So nothing new nothing gets overridden. You don't have to worry. Perfect. Once we have that ready let's see what we have to do next. So we now have to create a sample data for our database. So I'm just going to copy this and then I'm just going to create that file. Make sure you do this in the root of your app. So outside of any folders here, create a new file. Sample data dot JSON L. Make sure to put the letter L at the end. And let's just paste. So just a simple three list, three item list like this. now let's go ahead and let's run this command npx convex import table tasks sample data dot json l make sure you are running it in the root of your project so that it can actually find sample data dot json l all right as you can see we have a success message added three documents to table tasks and before i go any further if you failed at this command for any reason don't worry this is just to test out how convex works so if you're using a specific operating system where this doesn't work as intended it's okay we will create normal data and schema later on but i just wanted to populate it with something as in the tutorial and immediately if you go back to this screen your database you will now see a table called tasks and inside of here you will see is completed field as well as the text field as well as their creation time and this is your database now. So let's see what are the next steps here. Let's expose a database query. So let's find a way to fetch from this database and display that data in our app. So we're going to go ahead and do the following. Inside of our convex folder I'm going to go ahead and create a new file called tasks.ts. And let's paste this here. So import query from dot slash underscore generated forward slash server, which is basically this right here. We are exporting a constant called get and we are using the query. The query accepts the following parameters on arguments, which is empty for now, and a handler, which is an asynchronous function, which has access to the context. And then the context itself has access to the database abstraction which we can in this kind of query builder tool well query and collect items from which table tasks and what's cool is that it should give you errors if something is wrong we just need to make sure we have convex npx convex dev running let's just ensure that we have that and I believe that then my apologies it will not give you errors here so in order to test this out we have to make sure that we have npx convex dev running so just make sure you have this running and then you will get a simple message of uploading functions to convex and then convex functions are ready. And you can actually test whether this works or not already. You can go back here, go inside of your functions here, and you will find a new function, tasks get. You can click on the run function here. Since there are no arguments to be passed, you can see that the output was a simple fetch of all of our functions. So if you were to purposely mess it up by querying the wrong table here, and then let's try and run this again. Run function, you will see this time we have no output at all, right? I believe that later on, this will actually throw an error right here in the editor. But in order to do that, we need to have an actual schema. Since we don't have a schema, it doesn't really know what's true or not, right? How does it even know that tasks exist? Well, it doesn't. We know that because we created them using a JSON-L file. So if you're worried about that, don't be, because later we are going to have a proper schema, which will give us some type safety in that aspect. Great. So now instead of testing this function through convex's dashboard, how about we actually test it through our app? In order to do that, we first need to create a convex client provider. So I'm going to go ahead and do that. I'm going to create inside of my source components convex-client-provider.tsx like that. And I'm just going to copy the content inside and then show you what it is. So we have use client to make this a client component. We are importing convex provider and convex react client from convex forward slash react and react node from react. We initialize a new convex singleton using new convex react client and we pass in process dot environment next public convex URL and this is the important part. So double check that inside of your dot environment dot local you actually have next public's convex URL. And then we export function convex client provider which is a very simple function which simply returns the convex provider element passes the client prop and renders the children inside. Now that we have our convex client provider ready let's add it to our layout which I believe is outlined as the next step. There we go. So make sure you save this file and let's go inside of source app folder layout and in here I'm going to go ahead and the same way we imported the theme provider let's import convex client provider and make sure you do that from add components convex client provider so don't use any npm import you're using your file that you just created. And then go ahead and render this convex client provider let see around the children So I going to go ahead and do this inside of the team provider like this there we go and all the way to here perfect now that that is ready let just go ahead and make sure our app is running so I suggest that you have both of these running npx convex dev and npm run dev. Imagine this as your frontend and this as your backend. So now I'm going to go ahead and go to localhost 3000 and I'm just going to confirm that nothing crashes and everything seems to work. Perfect. And now that we have this provider set up, we can actually use the hooks. So I'm going to go ahead and do the following. I will go instead of source app page.tsx and I'm going to change this to be a client component. I'm doing this so I can use hooks within this page. And I'm going to go ahead and I will call tasks using use query from convex react. And then I will import API from dot dot dot dot convex generated API. This will give me access to type save functions that I've developed. And then what I will be able to do is iterate over them. So tasks question mark dot map and in here I will have an individual task and then I will be able to return a div like this give hdiv a key task.id and let's go actually it is underscore id my apologies so why didn't it throw an error here again because we don't have the schema so it had no way of knowing if maybe id was some property that we had, right? Because we could have easily added a property called id here. So because of that, it's not throwing any errors. As I said later, when we add a real schema, it will throw errors for undefined fields or tables that don't exist. So now let's go ahead and simply render task dot. You can see there's no auto-completion either, so I have to manually add dot text. And let's go ahead and do is completed. Task is completed. Make sure you don't misspell this because as I said, no type safety at the moment because we don't have a schema file. And just for fun, I'm going to make this class name border rounded padding to flex flex column. And I will go ahead and give this flex flex column gap two. So they appear one beneath the other. There we go. And let's give all of this a padding of four. So you can see that now, and let's just go ahead and wrap this inside of template literals. Let me just see. There we go. So you can see the actual output. So is completed true, is completed true is completed false so let's actually try something out i'm going to open a split view here you don't have to do that this is a new feature so i'm learning it add tab the new split view and i will choose this okay so i have my app right here and i'm going to go ahead inside of my data here and i will find one is completed which is false so integrate convex is currently in state of false. Watch what happens when I directly modify this in the database to true. It is immediately reflected in my app. That is the native feel I keep talking about, right? When you are using a real-time database, which needs to have, and it does have, a very, very good sync engine, this becomes a completely different experience, right? And when we are building something like a cloud-based IDE, we need to have that feeling. No one wants to work in something that feels like a second-hand experience as opposed to, you know, a normal IDE on your machine. So that's why we are choosing convex for this. And same goes, if you change this to false, right? Immediately reflected here. Perfect. And that's actually it for the quick start here. So what I want to do now is I just want to create a schema and then I want to connect all of this with Convex, with Clerk. So I'm going to go ahead and close this for now. And this time, instead of having tasks, let's go ahead and go inside of our Convex right here and let's create a schema.ts. So inside of here, I'm going to import define schema from Convex server. I'm going to import v from convex values and I'm also going to import define table from convex server. I will export default define schema here. I will create new projects table using define table and I will give each project a name. And I'm also going to give it something fun. For example, let's do owner id and let's do import status. V.optional, V.union, V.literal, importing, completed, and failed. So I'm purposely trying to do something a bit more complex than just a normal string so that you can see how you would, for example, create an enum, right? So each of our projects will be able to have an import status, which is completely optional because users can either create new projects from scratch or they will be able to import them from GitHub. In case they are importing, we're going to be using these set of Enniums to let the user know what is the status of the import. Is it currently importing? Has it completed or has it failed? And once you save this, what I want to do is I want to add an index. My apologies, not save this. So I'm going to add an index here by owner. So by adding the field owner ID, I will be able to query the projects much faster by owner ID. So every time I need to load all projects by a certain user, I will be able to do that in that manner. So now that you save this file, it needs to be named schema. It needs to have a default export here. you should see this you should see that it has added table indexes you should see that there was probably some error here because we tried to basically this is this keeps trying to synchronize your code with convex cloud so if you're in the middle of writing it it might be invalid so that's why the errors are happening but if the last message you see is a success that means everything is working. If you want to, you can also restart the entire thing and that will automatically try again. And this time there shouldn't be any errors whatsoever. And now if you go ahead inside of your convex, so let me just go ahead and go to my project here. And if I go inside of my, let me see, is it data? Yes. Inside of my data here, you can see that I now have projects in here. And if I open tables, you can see I still have my previous one, tasks, but it says this table is not defined in your schema, right? So it's keeping them separate. And you can actually always take a look at your schema right here. So you can see what it's doing. It's constantly synchronizing the code you have written locally to the cloud right here. That's also one of the very, very cool features of Convex. And if you learn to use this dashboard, your life is going to be so much easier because you will actually learn to see, you know, how much of your functions are hitting the cache, how much are they failing. You will be able to create log streams, all of those things. And there are so many more things Convex can do. But for this chapter, we're just going to focus on what it does best, which is real-time database. But you can orchestrate agents with it. You can create schedules, a bunch of bunch of things here. And we will also be using it as our file upload system for binary files when we need that function. So now that we have projects here, you can see that you can add them by passing in the name, the owner ID, which are the required fields. So how about we go ahead and try and do that. So instead of convex, we can actually now remove tasks. And you can see this is what I was talking about. Now we have an error here because the only table it knows about from our schema is projects. So it's very confused about the fact that we are querying tasks. And maybe even in page here we now get type errors because it doesn't know what this task schema is supposed to look like. So that's what I was telling you that is going to happen. So yes you can now remove tasks from here and instead in the convex go ahead and create projects.ds. And let's go ahead and export const create. And inside of here what we are going to do is we're going to create a mutation like this with arguments. And inside of here we're just going to accept a name. And let's go ahead and import v from convex values. And then let's add a handler, which is an asynchronous function. We can access context from here. And what we're going to do is we're simply await convex.database.insert into project stable context. My apologies, arguments are separated like this. so let's just pass in the name to be arguments and owner id which is also required and this is arguments.name so for now yeah just hard code owner id to one to three we are doing this on purpose because later we are going to connect this with clerks so we will able actually be able to extract the user's id here and let's export const get which is our query arguments will be empty handler will be an asynchronous function and just await context database Whoops So let's just get context. We have to import query the same place we imported mutation from, and then we will get convex.database.get projects table, and then just collect. my apologies it is not get it is a query the get accepts an id get is for a single one but yeah we can just call this get for now uh and let's save it like that and let's make sure that the last message you see here are that convex functions are ready and now let's go back inside of the page here and instead of tasks this will now be projects api.projects.get we are going to iterate over projects and we are going to use the project name. So we're going to be using project and this time if you try you should be able to see the keys. Let me just see what's going on here. That's because we're not returning this. There we go. And now if you try there we go you can see type safety which I was talking about. So in here you can now do name and you can also do owner ID project.owner ID and you can render this in a normal way now because it's no longer a type of boolean. So right now I don't think anything will appear here. So what we can do is we can create a simple mutation. so I will do const create project use mutation which we can import from convex react and pass in api.projects you guessed it create and then above this I'm going to add a button which we can import from components UI button and I'm going to add a label add new on click create project let's just make sure we do it like this so create project and pass in the name and let's call this new project like this and when you click add new there we go it immediately creates it with a name new project with the owner id so if i change this to new project 123 and click again there we go so you can see how we don't have to do any invalidation. We don't have to do any polling. The sync engine does its magic. That is the magic of this use query right here. So how do I get the currently logged in user, which I am right now, inside of this owner ID? And more so, in fact, how do I throw an error when the user is logged out? Because it's easy to protect the UI, But what's important is to protect our data access layer, because that way, even if somehow someone bypasses our proxy or middleware, which has been known to happen, right? People have found vulnerabilities in the proxy.es, which was previously called middleware. People who still protected their API routes individually, or in this case, API functions, were completely okay, right? So that's what we're going to learn how to do. Let's go ahead and learn how to connect clerk with Convex. You can actually find this exact guide on their documentation page. So let's actually go through it. So we learn how to find it together. I think it's always good to learn how to use the documentation. So they offer a bunch of authentication providers. The one we are using is clerk. So I'm going to find it here in the sidebar. And then I'm going to find the next JS example. So we already created an account with Clerc. And what we have to do now is we have to create a JVT template. So this is very important. Let's go ahead and head to dashboard.clerc.com. You can use the link on the screen, of course. And let's go to our new Polaris app. In here, you should have only one user or more if you added more users and you know about it. And now let's go ahead through here and let's go inside of sessions and click on JVT templates. In here, click add new template. For the template field, you can actually select convex out of the box. You don't have to change anything here and you especially shouldn't modify the name. So just click save as it is. There we go. You should now have a new template convex. And now in here, it says to copy and save the issuer URL somewhere secure. So let's go ahead and do that. we have the issuer right here and I'm going to copy it and now I'm going to go ahead and store it inside of my dot environment file I believe that's where we need to add it so let me just go ahead and check so dot environment dot local I'm going to add one more field to the clerk section and this will be called clerk underscore jvd underscore issuer underscore domain. And let's just paste it here. There we go. Now that we have added this, here's an important thing you have to do. You also need to synchronize your convex cloud environment variables. So I'm going to go inside of my settings right here and click on environment variables. And I'm just going to go ahead and you don't have to add the next public's convex URL or convex deployment. But if you want to, you can just copy your entire environment.local file and just paste things here. So I really don't think you need convex deployment here or next public's convex. So I'm going to remove those. I'm just going to keep next public's clerk, next public clerk, clerk secret key and clerk JVT issuer domain. You most likely don't need this one either. This is specifically used for frontend, but still, we can paste our entire .environment file here, and let's just save all of these. So this is important because the cloud doesn't have access to our local .environment files, right? That is not synchronized, because obviously those are our private keys. It would be a bad idea to synchronize those. So that's why we have to do it this way. So just make sure that in your project Polaris in convex environment variables, you have added clerk.jvt issue domain, clerk.secretkey, and next public clerk publishable key. Great. And now you will be able to learn how to, let me just scroll down, there we go, next.jn, so we created the template. Yes, and now we have to go ahead and do the following. We have to go inside of convex, and inside of here we have to create out.config.ts. And let's go ahead and let's import out.config from convex server. Let's export default. Providers. Open an array. Open an object. Add a domain to be process.environment and then clerk jvt.issure domain. And then application ID. set it to be convex. We're going to go over these values and why those values specifically in a second. And let's add satisfies alph config. All right, let's go ahead and see. The first thing is ID needs to be capitalized. There we go. So why convex exactly in the application ID? The reason is because our template name is convex. That's why I told you not to modify that because it needs to be the same. And the second thing is clerkJVT issue your domain. Double check that you have called it exactly the same here. So clerkJVT issue your domain. Copy it from here and then paste it here. It should match exactly. And then double check once more that you have named it correctly here. ClerkJVT issue your domain. All right. Now that you have that, double check that again, the last messages you see here are that convex functions are ready. It's completely okay to have an error here. That's because it tried to synchronize when we were writing this code. So obviously it broke. We already have clerk next JS. So no need to do that. We already have this and we even have the middleware now. What we have to do now is we have to configure convex provider with clerk. So this is what I suggest that we do. At this point, we are having a lot of providers. so let's go ahead and do the following let's go inside of source components and let's create a new file called provider providers dot dsx and in here i'm going to mark this as use client and i'm going to go ahead and i will import clerk provider and use out from add clerk next JS. I'm going to import convex provider with clerk from convex forward slash react clerk. And I'm going to establish convex here using new convex react client process.environment.next public convex URL. And let me just see. Yes, we also need to import convex react client from convex react. So we are basically kind of modifying this convex client provider. And now that I think of it, maybe we don't need to create a whole new providers again. Maybe it is enough for this to actually, let me just check. Okay, now let's create the providers because it's going to be easier to maintain things this way. So we're just basically creating the same thing. You can copy from here and paste it here to ensure that you've used the correct environment variables here. And then what we're going to do is export const providers. Children, children react.react node. There we go. And then I'm going to return clerk provider here. And then in here, I'm going to return convex provider with clerk. And I'm going to give it client of convex.\nand use out of use out. And inside of here, I'm going to render the children. Like this. And now that we have this, let's go ahead and wrap our app with it. So I'm going to go inside of source, app folder, layout. I'm going to remove the clerk provider from here. and from here. And for now, I'm going to remove the entire header here. So I just have the convex client provider. I'm going to remove all the imports from clerk to clear things up. And then I'm going to import my providers and providers here. And let's just add providers like this. So, so far, not much should change. More importantly, our app should still be working just fine. You should still be able to load your elements here. You should be able to create new ones. So nothing much has changed, right? We created the providers and we just changed from, let's go ahead and go to convex-client provider. So we changed from using the convex provider to using convex provider with clerk. That's basically the biggest change. and we just combine them in one. So we don't have to manually do that. At this point, you can delete the convex client provider entirely. And you can even add the theme provider to here. So let's do that. Let's add the theme provider like this. And I'm going to import theme provider from .slash theme provider. So the local one. And then you can remove the theme provider from here. This way, our layout isn't polluted with a bunch of imports and a bunch of providers. So we just have a nice sleek providers right here. Great. So now what we have to do is we have to learn how to actually access Convex, how to actually access clerks authentication within Convex. Because sure, this is still just UI. We haven't really done anything yet. So let's go inside of Convex. and let's go inside of projects.ts. And now in here, I'm going to go ahead and attempt to extract my current user. And the way I can do that is by doing the following. I can call context.auth.getUserIdentity and this needs to be awaited. And then what I'm going to do is if there is no identity in the first place, I'm going to go ahead and throw new error unauthorized you shouldn't be able to visit this, right? and okay, so that seems to be throwing an error now simply because we haven't established a proper composition of inside of layout here you need to wrap your app inside of authenticated from convex react like this and we need to whoops, not here, my apologies, inside of providers. So let's just quickly do that. Wrap your children in authenticated which you can import from convex forward slash react. And now when you do this, you shouldn't be getting an error here. So let's go ahead and do the following now. in here let's do the same thing. const identity await context.out get user identity and owner ID will now be identity.subject and let's go ahead and do if there is no identity throw new error unauthorized. There we go. And in here let's actually just return an empty array if there is no identity present. Great. So now I'm just going to go ahead and go back in here. Let's go inside of source app, my apologies, components providers and let's add unauthenticated view from convex react. So you can see I have imported authenticated, convex react client and unauthenticated all from one place. And in here I'm just going to say not authenticated. And then in here I'm going to add outloading from the same place, convex react. And I'm going to say outloading. So what we actually want to happen here is to have a sign in button and a sign up button from clerk connects.js so now i'm going to go ahead and sign in here the way i usually do with github i'm pretty sure you will get logged out to during this process and there we go you can see what happened we now had outloading and now we have this right so let's go ahead and just try and let's try and do the following now. So I will move children outside of any out thing and I will comment out authenticated. So now I can load and see this even if I am logged out. So okay, let's re-enable this and let's just render user button inside. Again, you can import this from Kirknext.js. So I should be able to log out myself now. And you can see that now, since I'm not logged in, I cannot see any projects. That is because of this. If we are not able to detect an identity here, we throw an empty array. So if I comment this out and refresh, I can now again fetch the projects. but that shouldn't happen this should be the normal behavior right so that's why we have added this if there is no identity present we just return an empty array but what's more impressive is this the mutation I'm logged out and if I click add new I'm getting an error unauthorized right? But if I sign in with GitHub this time, take a look at what will happen. I will be able to A, load my projects and B, click add new. And this time it has populated the exact user ID that I have. And one thing I've just noticed that I forgot to tell you, right now you can't select anything in your app if you try to. That's because in chapter one, we copied and pasted myglobals.css. So instead of your source app, globals.css, if that's bothering you, just remove select none. And now you should be able to select things. Let me refresh and see. That should have fixed it. Perhaps you just need to, I mean, we just need to restart the app. let's go ahead and do rmrf.next to clear out the cache and then run the app again there we go you can now select things so in case you notice that it's because of this select none on the body later this will be useful to get that real editor feeling and to disallow user from selecting things they shouldn't be selecting but for now it actually might be useful so no needed to add that I'm going to add a to do to do add select none later and let me end the comment here there we go so that's what's happening now we have successfully connected clerk to our backend and here's what we can do even more we can actually now learn to use that index of ours So let's do context.database.query.projects. And let's do now with index by owner. And by owner accepts a query. And inside of here, we can do query equals for a field of owner ID to be matching identity.subject. So let me go ahead and expand this like this. And this. There we go. So we are now querying for all projects from the logged in user. You can see how now I can only see projects which have my owner ID. That is because in my data here, in my projects, all of my previous ones have this hard-coded 1, 2, 3 owner ID, which means that this user shouldn't be allowed to see those. So our authentication is working correctly. this is what I wanted us to achieve my apologies for a few hiccups here and there it's kind of hard to you know demonstrate this with so little data in our project but I think we did a pretty good job with explaining how we successfully connected clerk with convex this will basically be the most important way of building our backend so it's important that we establish this that early on amazing so i believe that actually um did most of the hard work regarding this i think um i just want to check the providers to see if there's something more we can add here i think for now yeah i would recommend still rendering the children only in the authenticated state simply because this is the kind of app that shouldn't even allow the user to not be authenticated, right? This is a cloud-based IDE. The only thing that unauthenticated users should see in this kind of app is the landing page and the login screen. That's it. Everything else is authenticated, right? The list to see your projects authenticated the file explorer authenticated the actual code authenticated right so because of that we just going to render the entire project within the authenticated composition right here so to wrap that up i actually want to create a few components here just to make our app feel nicer and to kind of start building our features folder so this will be our folder structure. I'm going to use the features and I will create an out feature here and each of my feature will have a component. So let's add the components here and let's create unauthenticated dash view dot t s x. So now I'm going to go ahead and import an icon from Lucid React. I'm going to import a component called item from our components UI item which we have added through Shatsian UI. So this has been added in the first chapter. And then I'm going to export const unauthenticated view. This will not accept any props. It's going to be purely a presentational component. And we're going to start by creating a container. This container will have flex items center, justify center, height of screen and bg of background. Then an inner container which will limit to how wide this can be. So we give it full width but we limit it to maximum width of large which is 512 pixels and we give it a background of muted. And then we're going to do a composition using those elements above. So we're using the item component with a variant of outline. Inside we render the item media with a variant of icon and we render the icon inside. And then inside of that, we start rendering the item content. Each item content should have an item title. So we add unauthorized access here. And below that, we add item description. You're not authorized to access this resource. Beautiful. Let's go ahead and go back and set up the providers here. and in the unauthenticated state, let's add unauthenticated view. We can do that by importing from features out components. So I'm going to move this above the theme provider like that. And let's remove the import for these two. So now if you go ahead and if you actually log out, you should see this unauthorized access. You are not authorized to access this resource. Great. now let's go ahead and while we're here let's create besides unauthenticated view let's create an outloading view so this will be even simpler so components outloading view.tsx and we're just going to import spinner from components ui spinner we're going to export outloading view component and we're very simply going to return a div with flex items center justify center, height of screen, bg background, and inside a spinner with size 6 and text ring. The spinner again is just a very simple Shadzian UI component. You can find it in source components UI spinner. And you can see it's practically just a loader icon which animates. Great. Now let's go ahead and render that here. So outloading view, which you can import from features out components outloading view. And you can already see how this happens, right? It's loading, and then it shows unauthorized access. Now, if you want to, you can extend this unauthenticated view, which I believe is called item action. And maybe in here, let me just check how we add this. So yeah, instead of item actions, I think you can add sign in button from clerknext.js and inside you can import a button which should say sign in and I think you can also style this to give it a variant of outline and a size of small so you should have sign in button imported from clerknext.js and button from components UI button and this should redirect you to the login page so that is one solution you can have another solution is to automatically redirect the user so for example inside of source my apologies yes instead of source proxy.ts we can modify the proxy to only allow certain routes so for now i'm going to do const is public route, create route matcher from clerk next JS server. And I'm only going to allow, for example, forward slash API forward slash ingest, and then everything after that, because that's one of the scenarios which we want to allow, right? You don't even know what this is yet. We are going to learn that in the next chapter. But for everything else, I want to redirect the user away. And then what I can do here is first of all move this above the default export and then open the clerk middleware asynchronous. I get access to auth and request. And if not is public route, so if this current request is not a public route, I will trigger auth.protect method. And what this will do is it will automatically redirect your user whenever they try to access this app if they're logged out. If you prefer that solution, you can use that solution. If you don't, you don't have to. So whatever you like best, I just want to show you multiple ways of doing things. Both are completely valid. They are basically just different user experience. They don't offer different layers of protection. If you like it this way, that's completely fine. Just keep in mind, you will probably want to couple this with some kind of landing page so that users shouldn't even get to this page unless they are redirected from your landing page and if they do they will be greeted with a screen like this you're not authorized to access this resource so if you want to skip that altogether you can implement what i did and just automatically redirect them or if you want if you like this explicit action of sign in then you can keep it like this. Great. I think we implemented a lot and it's time to end this chapter. Amazing job. So let's go ahead and commit all of this. Let's see. Our chapter is three database setup. So I'm just going to shut everything down. I will git add, git commit chapter three database setup and then I'm going to git checkout dash b 03 database dash setup. Git push origin 03 database setup. Let's wait a second. There we go. Then let's go ahead and open our new app. And in here we can open a new pull request. So this time I'm going to click here compare and pull request. and as always we're going to review this code. So let's just create a pull request and if you've added CodeRabbit as I did in the previous chapter you will now get an automatic PR review. So let's see what it has to say about our current code. Let's have a look at the summary by CodeRabbit. New features. Create and manage projects with automatic persistence and ownership tracking. enhanced authentication flow with dedicated sign-in interface, loading indicators, and access control notifications. We updated the global styling, which is referring to the fact that we removed select none, and we added backend service integration dependency, referring the convex. And in here, it noticed some issues. So in here, it is a warning us that we shouldn't use non-null assertion on environment valuables and risk runtime failure. and while this is true this type of environment variable allows our app to run in the first place so if we don't have this set up the app will not even work so this isn't something that will maybe fire sometime so that we have to add an if clause check here it will literally crash the app so one way or another, we're going to see it fail. So it's not like this will go, you know, hidden from us. So that's the only reason it's okay to do it like this here. But yes, usually whenever you work with runtime variables, you should do an if check if it actually exists and then throw an error. So it prevents the app from running. But this will do the same thing either way. So that's why we are OK. In here we are missing a semicolon. So if you notice a simple things like this, you probably have a semicolon. You've probably noticed I don't have prettier turned on or any kind of format on save. The reason I don't have that is so that you can see every single line of code that I change in my tutorials. Right. So I don't want any formatters to mess with some code outside of the screen recording view. So for you, this probably didn't happen. If you have a format on save and prettier and eslint you probably have semicolon here. Not a big issue. In here it says that we have a schema mismatch with the project's table so you can see how well it understands our project. It read the schema table and it sees that this sample data does not match that at all and in fact this reminded me to remove that file which is exactly what it suggested and in here it noticed some unused imports another thing for us to resolve in the next chapter and another thing we shouldn't hard code values here right we should do something if we do we should have something meaningful like untitled project but it noticed that this appears to be a testing or debugging code. So perfectly fine for us at the moment. And same advice here to not use non-null assertion operator. But as I said, if we actually do forget to add this variable, the project won't even start. So that kind of acts like a runtime error in itself. Very good review. Let's go ahead and merge this pull request and we are going to fix those issues in the next chapter Amazing So we already have three branches here Actually yeah three of them including the main And now once we done that we should go back here Check out back to the main branch and get pull origin main so that we pull those remote changes. There we go. And in order to confirm everything is OK, double check you are on your main branch right here. and inside of your graph control, you can see that the same behavior happens. We checked out to a new branch for 0.3 and then we merged it back to main. Amazing. So let's go ahead and recap. We have set up Convex account in a new project, installed Convex SDK and CLI tools, created table and tested out CRUD operations, and finally configured Convex provider with clerk authentication. Amazing job and see you in the next chapter. In this chapter we're going to add background jobs to our project. We're going to start by setting up AI SDK which will allow us to create some long-running tasks such as querying an AI and getting a response back. Then we're going to call that AI model through a normal API route which will serve as our quote-unquote blocking example. Then we're going to do the exact same thing, but this time by running it through an ingest background job. And then we're going to compare blocking versus non-blocking. So let's start by adding AISDK to our project. I'm going to be using AISDK version 6. So in my case, I can go ahead and use at latest because this will be version 6 and I'm going to immediately show you that. So inside of my package.json, if I search for AI, you can see that my version is 6.0.3. If you want to use the same version as me, you can do this. Though I don't think that these minor versions matter too much. I think that the version 6 is the most important one because between version 4, 5, and 6, there are quite some breaking changes in how you use it. So that's why I'm making you aware of the version that I'm using at the time of making this tutorial. So now let's go ahead and click on documentation here and let's click on providers and models. So this is your choice. You can choose whatever provider you want. I would highly, highly, highly suggest using Anthropic providers. They are by far the best when it comes to agentic coding and you will definitely feel that in building an app like this, which will have to kind of loop itself into tool calling until it reaches a certain result. And a lot of these other providers and models are not as good as Enthropic when it comes to that. Same goes for their UI and their generative coding models, right? They are just really, really good. So I'm going to show you two examples. I'm going to show you how to set up AI SDK Google because it's completely free. And I'm going to show you how to set up Anthropic because that's what I will be using going through. If you're wondering about the total costs for Anthropic to finish this project, it's going to be around $5 to $10. So nothing too much, but I completely understand if for some of you that is not obtainable for whatever reason. So I'm going to start with Google simply because it's free. So let's start with Google generative AI provider. Let's click on NPM and let's install it. Again, I'm going to use the latest version simply so I know that whatever version gets installed here is compatible with my AI latest version. So if I search for AI SDK, Google, it's 3.0.1. So if you want to, you can do this. Now that we have that, we have to go ahead and create a very simple API route that will call this. So I'm going to go ahead and create an API folder within my app folder here. And then I'm going to create a demo folder. And inside of that, blocking. and instead of the blocking folder, route.ts, what we have achieved with this folder structure is the following API route. So local host 3000, API, demo, blocking. And if you call a post request to this route right here, we're going to trigger Google's Gemini provider to create something, right, to generate some text. So let's go ahead and export asynchronous function post like this. And now we have registered this as a post route, right? So it will return something from here. So inside of response right here, what I want to do is I want to add this. So I'm just going to copy it here. so we can import generate text from AI package which we've just installed and we can import Google from at AI SDK Google like this and then we can return next response dot JSON you can import or you can just do response.json and pass in the response like this. So this is how you would implement a normal Gemini call. The problem is we don't have an API key, so this will fail now. So using the link on the screen, you can visit Google AI Studio and in here we can create one free API key. So I'm going to name this Polaris Dev and let me show you how you can easily create a new project. so Polaris dev create a project and once that project has been created you can go ahead and select it there we go it is automatically selected I will call this Polaris dash dev I'm gonna click create key and once we create this key I'm going to copy it here and I think I can add it here so API key or here let me go ahead and research a bit about how I can directly add usually you can do it through the environment variable but I want to show you how you can exactly do it from here all right so I think that the way you need to do this is by using create Google generative AI and then defining your own Google like this. API key and then add it here. So let me copy this again and let's add that right here. And then we're going to use Google like this. You can of course add this to your dot environment. I'm going to show you how you can do that in a moment, but let's try it directly with this. So let's go ahead and try this out and I'm going to create a very simple UI so we can test this so instead of the app folder I'm going to create a demo folder and then page.tsx inside what this will do is this will create a page that we can access on a localhost 3000 forward slash demo so let's start by adding use client here so we can do some interactivity export default function demo page. Let's go ahead and let's return a div. Let's give this div a class name. Let's do padding. What is the bigger padding? 8 space x 4. And let's call button from components UI button and let's give it a label of blocking. And then let's go ahead and implement a very simple handle blocking asynchronous function which await fetch api demo blocking with a post method so this is the exact api route which we have created you can see the cascading here api demo blocking api demo blocking as long as it starts with the app folder and goes into the api you've done it correctly and then let's just go ahead and give this an on click here and paste it right there we go so i'm gonna go ahead and try it now and let's just make sure we are running npm run dev and here's the thing so i actually tried doing this chapter a few times already and every time i tried my google provider failed so that's why i told you if possible use something other than a Google provider. It's just not reliable in its current state. I'm going to try one more time. We're going to see. Maybe this will be the successful one. Let's see. It's doing something. I'm not sure if it's failing or not. As you can see, it is definitely hitting that forward slash demo. And there we go. This time we managed to do it. And here it is. Here's a delicious and hearty vegetarian lasagna recipe so basically we just had an api call do a vegetarian lasagna recipe for four people using gemini 2.5 flash and a free api key yeah so for some reason uh many times i've tried this it just didn't work for me i don't know why it keeps saying random errors like my quota was finished even though I only have one request to Gemini in a whole month so I'm not sure how that happened then it kept saying that my API key was invalid so I don't really know and let me show you how you can add it to your .environment file so if you don't want to use it like this all of that is actually documented here so you can see that when you scroll down here you can store it inside of Google generative AI API key. So instead of your .environment.local add a hashtag AI add it here and then you can just add it like this. There we go. And then you no longer have to really do this. You should then be able to just import Google and use it directly without having to define it. Let's try if it still works. Maybe the problem is in that environment file. Yeah, it looks like it works now. Great. So you can see that our first example is now working Great Now what we have to do is slightly modify our page into showing that blocking state because so far we've only seen it in the network, but I don't think we visually understand what's actually happening. So because of that, I'm just going to implement a super simple use state here, which will keep track of loading. So loading and set loading. then I'm going to set loading to true when we start doing it and set it to false when we finish and I'm going to go ahead and make this button disabled while it's loading and if it's loading I'm going to show loading otherwise I will show blocking like this so hopefully this will give us a better understanding of what's going on right now so when I click on blocking you can see how It takes 1, 2, 3, 4, 5, 6, 7. Oops, I refreshed. Do you see the point? The point is that when it comes to long-running tasks like this, you cannot let your user wait for 10, 20, 30 seconds because this is not a super simple AI query, but later it's going to be a super complicated one and it's going to last for maybe up to a minute, right, if we are cascading an entire application. so you can't let your user hang like this especially with the fact that they can accidentally refresh and they can go back and all of those things right this takes too much time and it's not the problem about it taking too much time it's the problem that we are handling this through a normal blocking method whereas what we should be doing is simply using an api route to trigger a background job and then immediately tell the user hey we triggered the background job You are free to do whatever you want. We are going to alert you when it's finished. You can go out and take a walk. You can shut down your laptop. Whatever you want to do, it's going to be ready when you come back. That is what we want to do. So let's go ahead and learn how to implement that. Using the link on the screen, you can visit Ingest. And in here, let's go ahead and go through their documentation. One great thing about Ingest is that they offer completely accountless setup. So you can just follow Next.js quick start without ever creating an account until it comes to deployment, of course. So for now, let's just follow Next.js app router solution here. First things first, we're going to do is we're going to install Ingest. So for now, I'm going to shut down the app and just do npm install Ingest. And then I'm going to show you the exact version of Ingest that I'm using since this is an important package. So I'm going to go inside of package.json, search for ingest, and let me give it a moment to install. Here we go. So my ingest version is 3.48.1, so if you wanted to, you could have used this. Great. That is step one, finished. Now what we have to do is we have to run the ingest developer server, which is basically a fast in-memory version of ingest where you can quickly send and view events as function runs. So let's go ahead and copy this and let's run it here. So npx-ignore scripts false ingest CLI at latest dev. It will be best to find this on your own so you don't have to read off my screen here. And it offers to install a new package, so feel free to say yes here. And what this will do is it will spin up like a little local instance of ingest, which you can visit on localhost 8288. So feel free to visit that. And in here, you will basically find this ingest dashboard. Right now, there is nothing to be found. There are no applications which are connected and there are no functions. There are no runs, nothing. So that's what we're going to do next. We're going to connect ingest to our project. So in order to do that, we need to create source ingest client.ds. I'm going to go ahead inside of my code editor here, source and then I'm going to do ingest right here and client.ds. There we go. I'm going to paste this inside. So we are importing the ingest from ingest package and I'm going to call this Polaris. There we go. Once we have that, let's go ahead and register this in an API route because just by adding this, it's not enough. And you also need to have your app running at the same time. And what you will see now is that this ingest is trying to hit a bunch of endpoints for ingest. So it's trying to figure out what our project is. It's trying in Netlify functions, Redwood functions, a bunch of things. This is where we are going to register our, and once it finds that, that's the only endpoint it's going to hit. so let's go ahead and go instead of app folder our api folder let's create a new folder inside called ingest like that and then inside route.ts so i'm gonna go ahead and just copy the contents from here and paste them in here we import serve from ingest next and then we import from our ingest client the client. You can use a shorthand operator alias for this like that. So this is basically going to source folder ingest client. That's what this is. And in here we export get, post, and put. Save that and now when you scroll down you should see success messages here. It finally managed to find our app. And if you go inside of the ingest server here and go inside of apps here, you can see it started to auto detect it. So it knows it's next JS, but we still haven't added any functions here. So it's still not exactly usable. So we now have to create our very first function. So that's what I'm going to do. so inside of our source ingest I will create functions.ts and I will paste this here so we are importing the ingest client and we are exporting a constant called hello world and inside of here we are simply creating a function with an id of hello world and an event of test forward slash hello dot world. This is what will be used to trigger the event and we're going to demonstrate triggering in a moment. What this will do is it will sleep for one second and then is going to return back a message. One way we can immediately test whether this works or not is by going inside of the ingest server. So let's go ahead and let's find our functions here and if it's not found you could resynchronize it by rerunning this, so npx ignore scripts and maybe even npm run dev here to make sure everything works fine, oh, that's not it that's not all we have to do, my apologies, yes, we now have to add that hello world function to our route.ds, my apologies so let's go inside of app folder API ingest route.ts and let's add hello world there we go from ingest functions and now instead of your ingest server there we go one function found and when you click on functions you can see it right here what you can do now is you can invoke it here so I'm going to go ahead and pass in email here and I'm going to use business add code with hantonia.com. It really doesn't matter. Let's just invoke a function. And what this is going to do is it's going to run a background job. So what happened was pretty quick, so you don't really notice the effect of it, right? But it actually run a step called wait a moment, in which it waited for one second. And then it simply returned a message, hello, and then the data it received. My email, right? so you can kind of already guess where we are going with this but now we're going to kind of actualize it right so we're going to go ahead and go instead of ingest functions.ts and we're going to create a completely new one instead of hello world let's do demo generate and instead of this id it will be demo generate this will be demo forward slash generate like that and then I'm going to go ahead and run this instead of await step dot run generate dash text like this. Let's go ahead and call this asynchronous method and let's do await generate text which we can import from AI. I'm going to call the Google model from AI SDK, Gemini 2.5 flash or 2.0 flash. Let me go ahead and see what did I use in my blocking example. Well, I can just copy this actually. So I'm just going to do this. There we go. and in fact I'm just going to return the output here. So this way I don't really need the event at all and let's try that out now. I just have to change inside of my source app API ingest route.ts demo generate. Let's refresh this and let's go back to our ingest server. Let's go inside of functions. Let's click invoke. The data really doesn't matter. It can be empty and click invoke function. And what you see now is the exact same thing happening. It's generating a haiku or it's generating a lasagna recipe for four people, but through a background job. And this is now non-blocking UI, right? So you can now see exactly what it did right here. There we go. This is a vegetarian lasagna. There we go. And now let's go ahead and plug that into our app right here so you can see how differently it behaves. So I'm going to go inside of app folder API. Inside of demo, I'm going to copy the blocking folder and paste it here again. And I'm going to rename it to unblocking or let's call it background.\nlike this. If you have this opened, this is just the React cache, which sometimes gets confused when you rename folders, you can just save that and close it and close the dot next folder. Let's go back instead of our background route here. And what I'm going to do inside of here is a little bit different. So instead of directly calling this, I'm just going to do await ingest from ingest client.send name demo forward slash generate with the data of just an empty object. And this will be status started. And then let's remove. And this is now accessible on this API route. There we go. Now that we have that let's go ahead inside of our app folder demo page.tsx and in here i'm now going to create a handle background function like this handle background which again will call set loading here but it's going to call the background api route and then i'm going to duplicate this button right here. And let's actually, okay, yeah, this will be background, and this will call handle background. So let's look at the differences now. Let's repeat our knowledge, right? Our normal API routes, okay, yeah, this is not a good example. So I'm just going to have a very simple solution, not the prettiest but it works have them use different loading attributes okay so when you use a normal blocking one you can see how long it takes this is how long your user is blocked right your user cannot do anything right now because we have blocked the entire ui and we are waiting until AI finishes responding. And finally, it responded and it unblocked the user. User can now do something with that output. Whereas with a background job, you can see that immediately it finished and it just starts running the job in the background. So it's still not done. It's still creating the recipe, but the user is free to initialize another one, right? There we go. I immediately have another one running. Or I can do, you know, three of them at once. All of them will execute in their due time. This is the power of background jobs. And this is just scratching the surface because later, Ingest will have something that they call their agent kit. And their agent kit is honestly amazing. And we're going to use it to orchestrate AI agents into creating loops which will call tools and that will basically create the agent that can create something. We are going to create an entire network of agents which will be able to call each other, communicate with each other, and in the end create an actual Next.js project or whatever user describes in the prompt as you saw in the demo, right? So that's the power of ingest and the power of background jobs. You can see how quickly this is finished as opposed to this. Don't get me wrong, the speed is the same. It's just a matter of how you handle this process in blocking versus non-blocking way. So that's the power of background jobs. That's what I wanted to to demonstrate, right? We are now comparing blocking versus non-blocking. Great. So as I said, I also told you I will show you how to set up Claude SDK. So let me go ahead and go through that to end the chapter. I would recommend, you know, doing this even if you don't plan on using Anthropic simply because it is a much better model and it will unlock a lot of new possibilities for you. In fact, Google Gemini actually had problems and complete inability to call tools in the past. I think that's changed now, right? But you can see just how advanced these Anthropic models are and why I prefer them so much. So I'm just going to go ahead and do, let me just expand my terminals here. NPM install AI SDK Anthropic. then I'm going to show you the version so AI SDK Anthropic 3.0.1 I believe all providers are on the same version great and then you can see that inside of here they basically explain how you can do the exact same thing and your API key should be stored inside of Anthropic API key so I suggest you find this in the documentation so you don't just blindly trust me if they change it. So Anthropic API key. How do you create an Anthropic API key? Well, by using Anthropic console. You can use the link on the screen. I added $5 to my account. I think in entire development of this project and extensive testing, I spent a maximum of $15. And we won't be doing as much testing in this app. so I think you should be completely fine with five to ten dollars but again I completely understand if that's not possible for you that's why I showed you first how to do it with the free API key so I'm going to go ahead and call this Polaris dev you can of course choose a proper workspace or whatever but I'm just going to use the default one and there it is API key and I'm just going to paste it here there we go and then I'm going to go ahead back here and I'm going to scroll down until I can find the basic generate text. Here it is. So I'm just going to replace both my blocking and background generate text to use the anthropic model now. Not from ingest. From anthropic. There we go. And I'm going to copy this. And I'm going to go inside of my demo functions.ts. That's the file. And in here, I'm just going to replace this generate text to use Anthropic. There we go. So both my source ingest functions.ts and my app folder API demo blocking route.ts are now modified to use Claude Haiku. and I have added my Anthropic API key. So let's just give it a shot to see if it's working or if something wrong is happening. So I think I'm seeing no errors here. There we go. So both of this seems to be working just fine. Here it is. Let's see. Here is a vegetarian lasagna recipe that serves four people. You can see the difference in their outputs. And in fact, I think somewhere here you should be able to see there we go provider metadata so this one used Anthropic. Perfect so it's that easy to actually change providers. You can of course choose whatever provider you want if you have an existing API key you know for OpenAI you can definitely use that that is also a great one. I just personally prefer Anthropic whenever I work with this kind of projects Anthropic provides the best results. I believe that's all we wanted to do for this chapter so let's go ahead and let's merge these changes. First things first, let's see all the changes that we actually have. So we confirm we are on the same page. So I have eight changes. Two of them are package lock and package JSON. Then I added some routes for a background demo, for a blocking demo, the route which registers ingest, page where we have the UI for our blocking and background, right? Client, which is just an in just singleton and functions where we actually write the background jobs. So I'm going to go ahead and shut down all of my apps for now. And I will simply do git add dot git commit. This will be 04, right? Background jobs. And then I'm going to do git checkout b04 background jobs. and then git push origin 0 for background jobs. And then we're going to go ahead and review this pull request. Perfect. You can see how I have switched my branch even in the visual part here. Now let's open GitHub and open a pull request. So as usual, here on the top, I have a new branch. So I'm going to go ahead and open it and create a pull request. And let's see CodeRabbit's summary. Let's read the summary by CodeRabbit. New features. We added a new demo page with interactive examples comparing blocking and background operations. We implemented a synchronous API endpoint for immediate text generation requests, the blocking one. And we implemented an asynchronous API endpoint for queuing background tasks. We also added a backend infrastructure to process and manage queued operations. Ingest. So let's take a look at the comments. Most of the comments are due to the fact that this is just initial demo code. So we didn't handle errors. We didn't handle retries. Absolutely nothing, right? You can see that we always return started here, even though this can technically fail. As long as you can await something, it can fail, right? So it's those kind of things that we should improve later on. But since most of these will actually be deleted later, it doesn't matter, right? Otherwise, obviously, all very valid concerns here. Even the invalid comments, if noticed, that too. Same thing here. So some improvements we should do, but this is just for demo. You can see by the name, right? Loading to. That's not the industry standard when it comes to naming. And yes, in here, it actually warns us that this wouldn't work in production, which is true. There is a separate process of enabling ingest in production. Luckily Vercel offers one setup for that So we will worry about that when the deployment step comes Amazing Other than that let go ahead and merge this pull request As always I not going to delete my branch so I have a clear history of all of my chapters. You can see that I can now go back to anything I want. Great. So let's go back to my main branch and get pull origin main. So I have pulled those remote changes we just merged. And I always like to confirm that everything is fine by going inside of here, graph, and just confirming that the same behavior is present. We checked out to do 04, and then we merge that back into main. Amazing. So I believe that marks the end of this chapter. We've set up AI SDK, created a blocking API route, and then we did the same thing with a queued background job. And finally, we compared blocking versus non-blocking and its benefits. And I hope that I explained why we need background jobs, especially in apps like this. Amazing, amazing job. And see you in the next chapter. In this chapter, we're going to learn how to add Firecrawl AI to our project. And why do we even need it? Well, AI models have something called a knowledge cutoff. That means that from the moment their knowledge has been cut off or up to the point they have been trained, they probably don't know about anything happening after that. That would include, for example, Next.js 15, Next.js 16, React 19, the newest React use hook, or maybe the new file name for a middleware inside of newest version of Next.js. Those are all real problems when working with AI. So the goal of this chapter is to enable our AI models to improve their knowledge and go even beyond their cutoff date by giving them the ability to read anything on the web using Firecrawl AI. In fact, we already encountered this pattern very early on. So I'm going to give you a quick reminder. In the very first pull request that we've created, we noticed this pattern happen, right? We had this comment by CodeRabbit, which basically warned us, hey, you are using an invalid file name. It's not called proxy. It should be called middlewater.ts. And I have corrected it and said, that's not true. Here's the URL to the newest documentation. And it agreed with me because the new name is indeed proxy.ts. So this is a current issue of working with AI models. All of them have a knowledge cutoff date. They can do their best to be up to date with various methods And this is one of them, allowing users to teach them new things. So there must be some way this amazing tool CodeRabbit somehow read this URL. And while I do not know exactly how they do it, I know how we are going to do it. Using Firecrawl, which will allow us to turn websites into LLM-ready data. and in fact I'm going to demonstrate exactly how that works right here this is the finished project right this is what you will have at the end of this tutorial and let's see how that works so I'm going to do the following what is the current file name of the middleware file in next Next.js. And let's see the answer that it gave us. So first of all, it told me this is not a Next.js project. Yes, that is because this AI tool has access to my entire app, and this is indeed a Vite project, not a Next.js one. But still, it answered, and it gave me the wrong information. So it says in Next.js projects, the middleware file is typically named middleware.ts. That is incorrect. So what I'm going to do is I'm going to copy the URL, which demonstrates the new proxy file right here. And I'm going to tell it that is incorrect. That is not correct. Read this and tell me the new name. So let's see if it will be able to do that. and here we have the answer. You are absolutely right. So the same as CodeRavit, right? According to Next.js documentation, starting with Next.js 16, the middleware file has been renamed to middleware.t from middleware to proxy. The documentation states, starting with Next.js 16, middleware is now called proxy to better reflect its purpose. So this is what we will be able to achieve. By default, you are not able to do this just like that. You need a tool like Firecrawl to help you achieve this effect. So that's what we're going to be focusing on. So using the link on the screen, go ahead and create an account with Firecrawl. Once you get to the plan page, feel free to select their free plan. It will be more than enough for this project. And in fact, you will get even more credits than you think. So just go ahead and click get started on the free plan. It's more than enough. And then what I want you to do is go ahead and click on your account here. And let's go inside of our account settings right here. Actually, let's click on the settings here. And let's go ahead and click on billing. And in here, you will find apply coupon. And in here, go ahead and enter Antonio coupon. What this will give you is a thousand extra credits. So let's go ahead and just take a look at this. So successfully redeemed coupon for a thousand credits. You know, you have nothing to lose. Go ahead and enter the Antonio coupon here. So what do we have to do now? Well, I think the best way to use it is by actually opening the documentation as well. So first things first, we have to establish the fire crawl singleton or client, however you want to call it. And for that, we need an API key. So either copy the existing one or create the first one. Let's go ahead and install fire crawl in our project. So npm install at mendable forward slash firecrawl dash js. There we go. Now that this has been installed, I'm going to go ahead inside of my source lib and I'm going to create a new file firecrawl.ts. and then inside of here I'm going to import fire crawl from our newly installed package and then I'm just going to export const fire crawl with an API key which reads from process.environment fire crawl API key now let's go inside of .environment.local let's add fire crawl here and let's add that here. Let's copy this and let's paste it. There we go. So we successfully created the fire crawl client. So what we have to do now to test this out is the following. I think the easiest way to do this is by modifying our ingest function here. Let's go ahead and make this just a bit more advanced. So what we're going to do now is we're going to add more steps to this background job. We're first of all going to allow the user to add their custom prompt into this, and then we are going to use that prompt to extract all URLs which user has pasted. So if user said, hey, read this URL, we have to specifically extract that URL. Why do we need to extract it? Well, because that is how Firecrawl API accepts data. So in their standard features here, we have scrape. Make sure you select node because that's what we're going to be using. So this is the npm install package which we have created and we have this client set up. And basically, this is the function that we are going to call. Firecrawl.scrape. And then we're going to enter the URL which we have extracted from the user's prompt. And we are going to return a format that we want to add to our context. In our case, Markdown will be the one that we need. But as you can see, you can do even more advanced things like scraping down to HTML and so much more things you can do, which we are going to go through later. But for now, let's focus on the most simple and easy to understand feature here. So first things first, let's go ahead and modify this. Let's add event here. And then let's go ahead and extract from event prompt. We can get that from event.data and we are going to define the type of event.data to very simply accept a prompt which is a type of string. There we go. Now let's go ahead and extract the URLs using await step.run and let's called the step extract URLs. It's going to be an asynchronous method. So this is now a separate step. And what we need to define here is URL regex. So I'm going to go ahead and write a forward slash HTTPS question mark colon and let's add a backwards slash forward slash another backwards slash and then another forwards slash open square brackets then go ahead and add a carrot inside another backwards slash s plus forward slash g or just google url regex or use ai okay whatever you do just make sure you have a regex that can scrape urls and what we gonna do now is just return prompt dot match URL regex or fall back to an empty array And this will then be basically an array of strings in the end So that's what URLs is going to be. Now that we have that, let's go ahead and define our scraped content. And we can do that by doing await step.run scrape-urls. Again, an asynchronous method. And what we're going to do is we're going to get the results by doing await promise.all URLs.map. So we are iterating over every single array, over every single URL which we have extracted. So let's go ahead and extract it like this. And let's simply run the following. await firecrawl, which you can now import from libfirecrawl, dot scrape, and simply pass in the URL as the first argument, and then open an object, formats, and go ahead and add markdown as the option here. There we go. And now from this result, let's just return result.markdown or fall back to null in case we were not able to scrape. There we go. And now let's just return results.filter by boolean. What this will do is it will filter out any of those null, basically unsuccessful ones. And let's just join this with a page break. So use backwards slash n backwards slash n. Great. And now what we can do is we can structure the final prompt. So the final prompt here will be scraped content. If it's available, we're going to go ahead and do this context, colon, break, scraped content, break, another break, and then the user's question, prompt. Otherwise, just the prompt. We weren't able to extract any URLs, or maybe there were no URLs to extract. And then instead of the prompt being this, the final prompt will be this. So what are we doing now? We now have three steps. The first step is to extract all URLs from the user's query. The second step is to scrape those URLs using Firecrawl in a markdown format. And then we simply combine that into the prompt, leading us to last and third step to actually generate a response using this new context. So you can do this with, of course, Google. You can do this with Anthropic. You can do this with OpenAI. Depending on what model you use, it might know more and it might know less depending on their cutoff knowledge date. So I am assuming by the name of this one that its cutoff date was 2024. I don't know. Maybe it is, maybe it isn't. So let's go ahead and run our project now. In here, npm run dev. And in another side, npx ignore scripts false ingest CLI latest dev. I'm just going to expand so you can see how it looks like in one line. all right make sure you have all of these running and in fact you won't even need to see your app you just need to see ingest server because we can now actually pass data from here so go inside of functions find the demo generate one and click invoke and inside of here go ahead and add prompt and let's try something simple what is two plus two for example and let's click invoke functions. So right now we're just testing if this works. And this was super fast, as you can see. We first attempted to extract URLs. And as you can see, no URL was found. We attempted to scrape URLs. None of them were found. And finally, we just went ahead and there we go. This is the response. Two plus two is equal to four. Very simple, right? Basically, our prompt thingy works. So let's try this again. This time, let's go ahead and pose it the same question. What is the name of the middleware? Let me just go ahead and use some other quotes. File in Next.js. So if I'm correct, this model shouldn't know that the newest name is proxy. Depending on when you are watching this tutorial, maybe that's common knowledge in AIs now. But I think that right now, let's see, generate text. Okay, in Next.js, there is no specific middleware file. Instead, Next.js provides a middleware feature that allows you to intercept. Okay, basically, yes, this is it. This is the middleware file. I guess it's just understood me differently. But there we go. It has no information about the new proxy thing, right? So let's change that. Let's go ahead and let me just prepare the proxy thing. If you want to find it too, go inside of the documentation and just click on proxy here and then copy the URL. So let's try now. Let's try again, this time with Firecrawl. So, and let's go ahead and give it, here are the docs and paste it. let's click invoke function and this time as you can see extract urls has success successfully extracted the url then it scraped the url so you can see we have every single information about the page in markdown format now and then let's see the output according to the documentation the file for the middleware functionality in next js is now called proxy So successfully, we have extended the context of our very limited and knowledge cut-off model. So Claude's Haiku is actually, well, I'm not sure if the dumbest model is the correct term to use, but it's supposed to be used for very short tasks. So the fact that we were able to extend the knowledge of this very simple model shows you how useful something like FireCrawl is. And if you weren't impressed enough by its generous free tier, amazing feature, did you know it's also open source? That's right. You can actually contribute to Firecrawl yourself. But let's go ahead and see what other things a Firecrawl can do. So we just did the very basic scrape function, right? I think it's self-explanatory what this does. It can scrape. But it can do so much more than that. So if for a specific case, perhaps our case, when we need super fast responses within our code editor Goat's suggestions, we might use their faster scraping function. You can see they even thought of that. Just make sure to always click on JavaScript or Node.js so you can see the actual code that you will be using. They also offer batch scraping for multiple URLs. They offer JSON mode. and this is a cool one. They offer tracking changes on websites. This one is actually super cool. I can already think of a SaaS you could build around this. Your users could give you URLs of the websites they want to track and you could build a SaaS with Firecrawl which basically alerts them every time there's a change or maybe you detect that there is an A-B test going on. Perhaps something to see how competitors are doing. So you can see how many things they have besides this scrape thing, a stealth mode, proxies, so many other things. And if that wasn't enough, you can also search the web in general, right? So if you want to, you can extend this even further by not waiting for the user to give us the direct URL, but instead allowing the user to just search for anything they want, like what is the most up-to-date Next.js version, and we would use Firecrawl's search of that term. So instead of Firecrawl here, we would search for latest Next.js version, and we would maybe limit to top three results so we don't, you know, overload the context. And what that would do is it would return three items like this with relevant information about where you can find that info. So if that crossed your mind, you know, oh, cool, but we have to know the exact URL. Well, not only that, you can literally tell it, hey, can you search for Next.js proxy, right? Are there any updates about that file? and it will genuinely find the results just like a Google search would. And then you could extract the URLs from here instead and do the usual process. So it is a very, very advanced tool. If that's not enough, they also offer something called Map. Map basically allows you to input a website and get all the URLs on that website extremely fast. And I think there's like a billion things you can build with this. How far they've gone with this is absolutely amazing. And the fact they're open source means that they will only grow more. In fact, in here, you can see their GitHub, almost 70,000, more than 70,000 stars and extremely active repository, completely open source. So I highly recommend that you take a look at this. I'm super impressed by everything you can do here. You can even run this locally and you can self-host Firecrawl. Did I mention that? On top of everything else, they offer step-by-step self-hosting guides. So I hope I explained why we need this because in the very first chapter that we did when we encountered an AI model which was CodeRabbit you saw how it was limited by its cutoff knowledge date And the one way we can improve that is by allowing it to read the web And we just added that to our AI model using Firecrawl Amazing, amazing job. So what we've done now is just get familiar with Firecrawl, understand what it can do, and we grab some free tokens. But what we're going to do later is, obviously we're going to plug file crawl in into various AI features that we're going to have which I demonstrated at the beginning of this chapter the agentic chat on the left side will have a step called extract URLs and scrape URLs and then we're going to have a quick edit where user will also be able to add their own URL and we are going to have the same thing happening there so that's how we're going to be using Firecrawl in our app. Amazing, amazing job. So let's go ahead and merge these changes. So Firecrawl AI, I'm just going to shut down my app. Let's go ahead and do git add, git commit 04 fire crawl AI. Git checkout dash B 04 fire crawl AI. Git push origin 04 fire crawl dash AI. Perfect. Now let's go ahead onto our GitHub repository URL. let's open a pull request and this time we don't actually have to review it simply because this was just a demonstration of fire crawl and we are later going to add fire crawl in its proper place in its proper functions so right now it's in a function called demo function right so no need to review that right now since obviously this will later be added again in a proper function. So just because of that, there is no need to go through the entire review process right now. We are going to have the same review later. So let's just go ahead and immediately merge 0 for FireCrawl AI. And now we can go ahead and do git checkout main, git pool origin main. let's wait a second and let's confirm everything is good here so we are on the main branch and inside of my source control in the graph here i can see that i have uh oh is it zero four fire call ai uh did i make a mistake in nourish oh this is chapter five okay my apologies this is supposed to be chapter five so one mistake that i've made my apologies so later when you look at the branches you will see zero four background jobs in zero four fire crawl ai that is supposed to be zero five okay one mistake it's not gonna hurt anyone but you know that this is supposed to be zero five maybe you've noticed and corrected yourself either way not a big deal we go ahead and we continue going with this amazing project. So we demonstrated outdated AI code patterns. We've set up Firecrawl web scraping. We successfully extracted URLs from user prompts. And finally, using Firecrawl's scrape function, we have enhanced prompts with live documentation. Amazing, amazing job. And see you in the next chapter. In this chapter, we're going to add production-grade error tracking and observability to our app. Basically, we're going to transform silent failures, which will happen all across the app, into some actionable insights to something you can fix and something you are aware of when it happens. This chapter will aim to show you the difference between flying blind and having full observability in production. So let's get started by demonstrating some types of errors that can happen within our app. Make sure you have npm run dev running and make sure you have your ingest CLI running as well. So now I'm going to go ahead and make sure my app is active on the localhost 3000 forward slash demo basically where we tested our blocking and background ones and now I'm going to prepare my InJS developer server here as well simply so I'm ready to observe what happens there as well. I'm going to go inside of my app folder demo page dot t s x and now I'm going to go ahead and prepare a scenario of when we have a client error. So what is a client error? That's something that happens well on the client. That would be right here, right, in some use client component. This can be various reasons. It can be an invalid access in an object, it can be an invalid function, an invalid type, many things. But in our demo scenario we can just do this, handle client error and let's just do throw new error. Client error, something went wrong in the browser. So that's the first scenario we need to have. Then let's prepare a second scenario, an API error. So we are going to purposely fetch an API endpoint which will throw back an error. And then we're going to have a third scenario, ingest error. Basically, this will successfully make a fetch request, but the background job itself will throw an error. So that will be interesting to observe. So let's go ahead and first test the client error. This is the one that should be very easy to do. So I'm just going to go ahead here and I will add a new button. I'm going to give it an on click variant of destructive and I will add a label client error. So we should now see client error button right here. What happens when I click? Well, as you can see, we have an error. That is very expected, right? But the problem is only the user and well us now in development mode are aware that something actually went wrong in the browser. We actually have no idea what went wrong. We don't know what user was trying to do and worst of all we genuinely don't know that a user got an error which is unacceptable in production. We need to be aware of every single error that happens in our app, on the client side, on the server side, or in a background job. So now let's go ahead and copy this and demonstrate a server error or an API error. So handle API error. In order to test this out, we have to create the demo error, which is very simple. So inside of an API folder, demo, let's create a new folder called error. And inside of there, route.ts. And let's simply add an invalid post method, just a simple error, something went wrong on the server. So I'm going to go ahead and call this API error. So even worse experience here, we don't even see anything happen. That's because this is a server error and we can only figure that out if we go inside of our server logs right here. API error, something went wrong on the server. So this is even harder to observe, right? Even the user is not really aware of what just happened. And you can guess that the third one is even worse than that. So let's just go ahead and quickly prepare that. So I'm just going to copy this and I will call this ingest error handle ingest error. And let's go ahead and just create a very simple route. So inside of this demo, I will create a new folder and I will call it ingest dash error. I will create a new route.ts inside and I will paste the following. I'm going to import ingest client. I'm going to export an asynchronous post method and all I'm going to do is I'm going to trigger a background job with the name of demo forward slash error and then I'm going to send a what is a success status of rel response.json just status started right this is what this would be a successful api call right so i'm gonna go ahead and now create that background job so let's quickly go inside of source ingest functions.ts and just as we have a demo generate so the same thing right at the bottom here i'm going to add demo error ingest create function first argument is id second argument is very important demo forward slash error which should match exactly what we trigger inside of the api demo ingest error route and we only have one step which will run fail and it will just throw an error background job failed. In order to make this function we also need to go instead of API ingest route.ts and add a demo error from that very same file so we now register it. So now let's go ahead and click on ingest error. So even less information happening on the client but on the actual ingest development server we can see that this step is struggling so something is going on this step is simply keeps failing you can see that ingest thankfully will attempt to retry this step over and over again with polling increasing each time so it doesn't spam or reach a rate limit if it were a third-party service. But still, we know that we hard-coded this to an error. So those are three serious scenarios of which we have absolutely no idea that they're happening. How do we improve that? One of the most popular tools for resolving this issue is Sentry.\nAnd using the link on the screen, you can get Sentry theme for free for three months, as well as 150,000 errors completely for free. Let's go ahead and create an account and then let's create a project. Once you are in your dashboard, you will see a prompt to create a project. I'm going to select Next.js as that is our framework. And I'm not going to modify the defaults at all. let's just modify our project slack to be Polaris and click create project. And then in here, we're going to get an install script. So we can just copy this. I'm going to shut down all of my terminals for now. We're going to get them up and running later. So this is the command npx century wizard latest. And I'm going to use Next.js with a SAS flag organization name this is my organization name John Doe yours will be different and a project name I chose Polaris you chose whatever you entered or you just left it at default it doesn't matter let's just go ahead and run this within our project here so in here it's telling us that we have some uncommitted or untracked files in our repo and that's basically a demonstration of all of these errors. So it's warning us that it might overwrite some files, but this is perfectly fine because these are demo files anyway. So yes, we are going to continue anyway. Let's confirm that. After this, it's going to open a browser and it's going to connect. So as you can see, we now have login complete. You can go back to your terminal and now it is installing Sentry using NPM. And after this we're going to answer some questions. So for the first option if you want to route sentry requests in the browser through the Next.js server to avoid ad blockers basically giving you a better overview of what's going on with your app you can select yes. Same thing for tracing, same thing for session replay which is honestly magic-like reproduction of what happened. logs will be very important this will allow us to create a structured trace of what actually led up to the event and if you want to well i would recommend yes uh select yes for this too which is basically an example error page and you can select yes for using a ci cd tool because that will give us the sentry out token. So copy the sentry out token, make sure you copy it in full, and immediately store it in your dot environment. Just make sure you don't commit dot environment, which should be the default behavior. So I'm going to add sentry and just paste. Let me go ahead and copy this code and paste it here. There we go. And we can just say yes, continue. And I will select no for project scoped MCP server simply because we are not going to be doing any AI coding things in this project. Well, besides building an AI editor. Great. So I'm going to go ahead now and run npm run dev again. And the first thing I'm going to do is I'm going to visit the page that they have created for us. So Sentry example page. You can visit this page at localhost3000.sentry-example-page. So I'm going to copy that and paste it here. And we should be able to visit Sentry's pre-made page here. Ignore the purple around our button. That is because this page has some custom styles. It's just for demo. And in here they have prepared a sample error similar to how we have prepared it but you can see a crucial difference. This time the error was logged to Sentry. So now let's go ahead and let's click on our issues right here. So 15 seconds ago you can see some errors happening. We can see the Sentry example API error as well as Sentry example front-end error. So from now on, we can actually see why our errors are happening. And I want to show you something very impressive. So I'm going to expand my screen here. Go inside of Sentry example front-end error. I'm pretty sure this is where it's happening. And look at this. This is called a session replay. This is somehow rebuilding what the user was doing in your UI before the error occurred. So you can see this is representing, you can see this, right? It's representing this very screen. It's obviously saving on the resources or maybe things it doesn't have access to. but you can see we very clearly clicked on this button and that is what triggered the error. Right? So this would help you a lot if you had some unknown error or if the user doesn't know or didn't explain how it happened, you would have almost like a camera of what the user was doing at the time. And using their breadcrumbs, you can go so much in depth, look at this, about every single thing that happened here, right? This is an ideal way of actually fixing a bug. This is insane, but we're just scratching the surface. So now I'm going to go to my demo page, right? And in here, I'm going to try calling the client error. So this is the problem we had first time, right? User gets an error. and let me just show you in case you're lost we are on forward slash demo because I know this is very small font so that's why I'm showing you so this time we should get an improvement over that there we go another error successfully tracked client error something went wrong in the browser which is exactly what we log in our code so exactly what happened this is the error that we just triggered. You can see the exact URL where it happens, forward slash demo. You can see my Chrome version, my macOS version, a bunch of things right here. And as always, you can see the trace preview. You can see the breadcrumbs. Let's go ahead and go even further. So now I'm going to try triggering the API error again. Right now we can only see that here, right? Something went wrong on the server. But let's give it a second. Let's maybe refresh. And it's now going to appear here. There we go. API error. Something went wrong on the server. So forward slash API demo error. You can now see and observe every single error happening in your project. The only thing that I believe isn't added by default is the ingest error. And in fact, we also need to have our ingest development server running to test this out. I'm pretty sure that when I click on ingest error right here, it is going to fail, but I don't think this will observe it by default. Instead, there is something called a middleware which we can use to add sentry to. This is what I was talking about. So ingest has a sentry middleware and this is the cool thing about using sentry. 99% of the tools that we will be using have full sentry support so that is amazing. That's how good of a tool this is. So let's start by installing the ingest middleware sentry here. I'm just going to go ahead and install that. So npm install ingest middleware sentry. This is the version at the time of me making this video. So 0.1.3. And now I'm going to go ahead and copy this import. And I will go inside of my, okay, I can remove this comment here. Let me copy the import again. And I will go inside of my ingest client.ts and I will add the import. There we go. And then I'm just going to add it to my middlewares here. So currently I have no middlewares at all. Basically this will now intercept all of our background jobs and we will be able to see what's going on between them. You can see some rules here. So in case it's not working or something is erroring it's probably a version mismatch. So I would suggest that you can basically just google Sentry middleware ingest and you can find this page right here. So let's go ahead now and I will just restart my npm run dev and my ingest server. I will refresh both of them and I'm going to refresh my feed here in the century project. And now I'm going to trigger that ingest error once again. And let's see if something else is happening now in the feed here. So I'm going to refresh the feed. And there we go. We can now track all the errors happening in our background jobs too. And keep in mind, this can be so many things, right? So let's go ahead and take a look at something. Inside of our app folder demo, we had some blog, actually we can go directly inside of ingest functions. So take a look at all the steps that are possible. We can fail with a URL extraction, right? Maybe the prompt isn't a string, right? This step can fail. And usually we wouldn't even know that it failed, we probably think that user cannot enter anything other than a string. But now, if that happens, we will be aware of that, right? We are going to know exactly what happened. Look at this. I can see exactly where it appeared, exactly in what step. So this is an extremely, extremely useful thing. It can happen in some third-party providers like Firecrawl, right? What if that fails We are now aware of all of those amazing things That is the power of Sentry But we are not done yet We can improve our observability and monitoring even further When building an app like ours it is also crucial to know how much tokens we are spending That simply cannot go unnoticed. And Sentry has a solution for that as well. In fact, it has a solution for the very provider of AI we are using, Vercel AI SDK. So following this documentation, we're now going to enable that. So you will see exactly how well Sentry understands what's going on with our app with just a few line changes. So first things first, let's go instead of sentry.edge.config. So I'm going to find sentry.edge right here. And I'm going to find my sentry.init. So here it is my dsn, my traces sample rate, and my enable logs, which is sent to true. So at the bottom here, I'm going to add integrations. I'm going to open the array and I'm going to add sentry.versell.ai integration. So make sure you add this to your sentry.edge.config.ts. right at the bottom this is where I'm going to add that and now what we have to do is we have to find some places where we are actually calling generate text so I already know I have one inside of my API demo blocking a route.ts example here it is I have generate text right here and then after I do the prompt I'm going to add experimental telemetry keep in mind that what's called experimental can sometimes be deprecated, right? You can see that we already had a bunch of deprecated things. That usually means that the new name is what was after experimental, right? So you can see now it's active tools, but before it was experimental active tools. I'm just telling you this in case telemetry gets deprecated. That probably means you can now just use telemetry but both should actually work and let's do is enabled to true record inputs to true and record outputs to true and I will copy this and then I'm going to go inside of my ingest background job so let me go ahead and find my ingest functions.ts and in here I have another generate text so I'm just going to add the experimental telemetry here then I'm just going to restart my app and I'm going to restart my ingest developer server I'm going to stay on the demo page here and I'm going to go back to the issues here and I'm going to go ahead and just you know start using my blocking let's just wait for it to finish yeah this is the example of the blocking one and my background one so basically we just triggered some generate texts and now we're going to see if we can find some AI telemetry within our century project. And completely accidentally, Sentry actually caught an error in my ingest background job. That is because I completely forgot, but yes, this will now fail. Because remember, we now have to pass data and we have to pass prompts inside. That is how our background job now works because we accept a prompt to extract some URLs inside. So yours might have failed too. And I would be clueless as of why it happened until I looked at the code. But this way, I can see that it was caught by sentry. So it was so easy to make a mistake on our end. And now, as you can see, it was immediately caught here. And just by seeing this line, I would immediately get reminded oh i forgot to pass the prompt right i i forgot that we modified that from the previous chapters but lucky for us the blocking one should still be working just fine right that is just a normal generate text so now find the insights click on ai and click on agents in here as you can see I can see exactly which LLM calls I did as well as how many tokens those LLMs actually used and in here I can click on the exact trace ID so this is the one API demo blocking let me go ahead and zoom in so you can see post request on API demo blocking we called generate text and we spent that many tokens. And then you can see some more information here. And you can see exactly which prompt it was. And you can even see the exact response right here. And all this information is available. So you can see that just by enabling that one small integration, we now have full observability over our AI agents. We can see exactly which ones are being called, which ones are failing, and how many tokens they are using, right? So all of this is so interconnected when you actually start to research Sentry and how it works. And one last thing I want us to demonstrate here is Sentry logs. So that is actually already enabled if I take a good look in my sentry.edge. So enable logs is set to true. So just make sure you have it here as well. But what we can do is something a bit more fun. We can basically explain to our trace logs what the user was trying to do by giving some logging information whenever user clicks a function or whenever we trigger a background job, even though it's very clear from Sentry itself what happened, we can be more descriptive ourselves and do that. So by using Sentry's new feature called logs, you can actually create structured information and give you a better understanding of why certain errors have happened. And they're actually super easy to set up. In fact, I believe they are already configured here. So make sure that instead of your Sentry config files, both of them, right? I think it's very important that both of them are identical, actually. So yes, make sure both your Sentry server and Sentry edge have the same information in them. But this is the one we are focusing on now, enable logs set to true. And this is what you can basically do now. you can create structured information whenever user is doing something. This is actually a standard practice in production. So if I go inside of my demo page right here, and let's focus on these three functions that I have handle client error, API error and ingest error, I'm going to go ahead and for example, use the century logger to log that information. So import all as century from at century next js and then I can use the century logger and for example let's do info we can of course do error trace debug warn whatever we want right so for example century logger info and in here I'm just going to say user attempting to click on client function and you can give it even more information. So since we are using clerk, what we can actually do is we can get the user ID from use user ID. Use, my apologies, use out. Use out is a function you can import from clerk clerk react or clerk next JS. I think both of them should work just fine. And you can destructure the user ID from here like this and then you can go ahead and just pass that along here right you can see they actually tell you that you can do it like this basically structured information so this user attempt to click this button and then you can track along if that user breaks something going forward and you can see that side by side so I have added this to handle client error which is used in the client error button so when I click here I'm expecting that to be logged so let me show you where you can find logs go inside of explore and then go inside of logs right here and if you have a prompt here to enable it you can go ahead and click enable logs at the top. And after a few moments, you will see a new log here, user attempting to click on client function. And here is our property, user ID. And here's the best part. So these logs by itself are okay. But this is where the magic happens. If you actually go and find the relevant error, which is something went wrong in the browser, and you scroll down, you can actually see the related log. So you now have more information, right? What user this happened to and what were they doing? Well, user attempting to click on a client function. That is what was happening. That is the power of structured logs. You can see that they are way more powerful than that, actually. You can trace things when starting a database connection and tell exactly what database we are connecting to we can throw fatal ones if database connection pool is exhausted and you can track exactly at what number of connections that has happened so very very useful and you can of course automate it and then abstract it to a much more useful state than that but here's another cool thing that they can actually do they have integrations and you can for example send console log warn and error calls all as logs to sentry so i'm gonna go ahead and do that and then whenever my app or one of my third libraries throws these errors or logs or you know things like that uh you can you have to add that to integration So right here there we go Our app will now receive log warn and error ones So I'm going to add that to both here. So I have identical ones. And there we go, right? Just like that, our logs will now be listening to various console traces throughout our app. So that's what was happening here. you can see ingest is throwing some console warns or errors and i can see that happening in my logs and i can see that happening next to my errors so you can do so so much with sentry and i cannot recommend this product enough this is the go-to product for error tracking and they have a super generous free tier and you will trust me you will get alerted for every single error that happens which in production is you know invaluable how much helpful it is uh i believe that i have shown you the power and uh you know we we now learn the difference you know between going in blind and having some proper observability production grade observability so let's take a look at what we did. We've set up Sentry with Next.js Wizard. We caught a background job failure with Ingest. We figured out how to monitor AI calls and token usage, and we added structured logs for user interactions. And finally, we even added user context like user ID for even better debugging. So let's go ahead and let's merge all of these changes. We will clean up our extra stuff later, but for now we can just do git add and then a dot and then git commit so I previously made a mistake I have 0 4 twice so I have to be careful this is now chapter 6 so I'm going to call this error tracking and then once I've created this commit I will do git checkout dash b 0 6 error tracking there we go git push u origin 0 6 error tracking and we can do a quick job of a merge here simply because there is not much to review when it comes to a wizard who added all of the files which was npx century every other new file is a file that's going to be deleted meaning uh for example all of this was added by Sentry. So no need to review that because we are not going to modify what they have added, right? But these things like an ingest error route and the registration of that, all of that will be removed, obviously. These are all just demo examples. So because of that, it makes no sense to waste time. Let's go ahead and merge this and let's get on to building new features. So I'm going to go ahead and merge this pull request. Make sure to not delete the branch. so now I have twice the number 04 but we know that this is actually supposed to be 05. Let's go ahead and git checkout back to the main branch and git pool origin main so we are up to date with our remote merge changes and to wrap it all up now I always love to confirm my graph here Here it is. So yes, 04 appears twice, but you can see logically, chronologically, that is 05. Amazing. I believe that marks the end of this chapter. Amazing, amazing job, and see you in the next one. In this chapter, we're going to build the projects page. This will include the Polaris branding, project creation and get feature, the actual project list, and some keyboard shortcuts as well as a command palette or dialogue. Basically, this is what it's going to look like. So this is the finished project. This is what we are supposed to have at the end of this chapter. This is the Polaris branding that we are going to have. This is the new button. And the only thing we aren't going to do is we aren't going to make this open a prompt simply because we don't have any functionality as to where to send that prompt at the moment. So we're just going to make this instantly create a new project and then we're going to modify it later. Same is for the import feature right here. But everything else we will be able to do including this command dialog right here and the shortcuts. All of that will work at the end of this chapter. So let's get right into it. The first thing we have to do is make sure our app is running. We will not be needing any ingest or anything like that. So you can just have localhost 3000 running. And what I would like to do is just clean up our project first, right? So let's go inside of source app folder. And in here, let's remove the century example page as we're not going to need it. And let's remove the demo page too. We are not going to need that either. And inside of the API, let's carefully go here. So let's remove demo. We don't need that. And let's remove century example API. And inside of ingest route, we can leave it as is. We're going to clean up ingest later when we actually start building some real background jobs. So for now, let's just head back to localhost 3000, where we have kind of started building the projects list, right? So what I want to do first is I want to go inside of convex, and I want to go inside of schema here. The reason I want to do that is because I want to add all the features here, simply so in case we are doing some UI changes depending on the field of the project in the database, we can do that and we don't have to fake it, right? So I'm going to go ahead and add export status here. Give it a v.optional like this. And inside, I'm just going to copy this v.union. Let's go ahead and add exporting. Let's go ahead and add completed. Let's add failed and let's add canceled. And let's also add export repo URL. This will also be optional. And if it is passed, it has to be a string. So the reason I'm adding this is because I'm pretty sure our UI will look different if a project has this field. If it does, we're going to show a little GitHub icon to indicate to the user like, hey, this was imported. Otherwise, it will show a globe icon to indicate it's kind of on the cloud, right? It's custom made. Perhaps we won't need the export status for this exact chapter, but still, no worries. It's completely fine if we add it now. The import status is important, so make sure you have that. And let's also add updated ad field. So updated at is a number like this. Great. So now let's go ahead and make sure we do npx convex dev. Simply so all of that is immediately synchronized. And there we go. Okay, so this is actually all successful. The problem is that we have existing projects, right? So let's go inside of dashboard.convex.dev or you can use the link on the screen. let's go inside of polaris here data projects and let's just delete all of them all of them are outdated and let's do the same inside of tasks because we don't even have that anymore so we can get rid of tasks entirely let me see uh there we go you can delete the table from here perfect so we are only left with projects that reminds me we can also delete sample data dot JSON L. So let's remove that. No need for that either. Perfect. Now let's go ahead inside of convex and let's go inside of projects here. And now we're going to go ahead and just fix the current create method. So let's go ahead and see the arguments here. Still, we're just going to use the name. Nothing needed to change here. And then let's go ahead and do the following. So I'm going to do const project ID await context database insert into project stable name arguments.name owner ID identity.subject and updated at will very simply be a date.now instance and let's return back project ID. All right. And now I want to do one thing, we're going to use this a lot, like in almost every single query and mutation. So let's abstract it. So I'm going to go inside of convex here, and I will create a new out dot ds. And I'm going to import mutation context from generated server and query context from generated server. And then I'm going to export const verify out. I'm going to make this an asynchronous method. And for the context type, I will use either a query context or a mutation context. And then in here, we can go ahead and return the identity. There we go. Now that we have verify out, we can call this instead. So in order to get identity, we can just await, verify out and pass in the context. So you can import that from dot out. Since this is a mutation, instead of verify out, this is the type we will use. So if I didn't past that. Looks like it doesn't have an error because they could be identical. Yes, it uses the generic mutation context. So maybe that covers the type for the other one. I don't know. But I kind of want to use this. It works for me in the original source code. So I will teach you to do it here as well. Great. So this is our create method. And now let's go ahead and let's modify the get method here. We are going to slightly modify it. So first things first, we get the identity by using verify out and passing in the context here. And then let's go ahead and store the result of this inside of a query. And what we going to do is we are going to add an argument here which will be optional limit v and pass in the number here so if we want to we can limit the number of results we get so if arguments actually yeah it would be better to just not call items here at all okay let's call this get partial like this and since the name indicates that it's going to be partial we can actually make the limit required because that makes more sense right if we have a separate query for making partial ones let's go ahead and actually do that here and then we can very simply instead of using collect we can use take and just pass in the arguments dot limit. And I'm not sure I'm doing this correctly. There we go. Arguments dot limit. Perfect. And then in here, well, I think we can just return it now. There we go. So that is get partial. And now let's just have a normal get, which will, well we can copy it since it's going to be very very similar except it's not going to accept any arguments at all and instead of this it will just use collect and no need for this looks like a weight has no effect on this type of expression i'm assuming that's the case here too looks like it's not okay you can see that it tells me a weight has no effect on this type of expression okay I'm going to trust it. Let's go ahead and have that. In here we got an error. Use query projects.get. It looks like something is incorrect here inside of the convex one. Let me go ahead and do npx convex dev again in hopes that that will resolve it because I don't see any, at least I can see any issues with the code. there we go convex functions ready so can I refresh this now okay I still cannot refresh it how about we go and take a look what it actually is so source app folder page.tsx let's see what is the problem projects what is the problem dot map does not exist okay apiprojects.get oh I'm sure a lot of you have noticed this sorry about that okay if you didn't see what it was I forgot to execute the collect method now it should work just fine my apologies okay so we now have the get method we have the get partial so we can limit the amount of things we get and we have a proper create method perfect so let's go ahead and let's start with the creation of this so I'm going to go ahead instead of source features and in here I will create projects like this instead of projects I will create UI my apologies I'm not going to call it UI I'm going to call it components and inside I will create let's call this projects view dot t s x like this let's mark it as use client export const projects view and let's return a div projects view just like that and then i'm going to go ahead inside of the app folder page dot t s x right here i'm going to go ahead and call this home i will remove every logic inside, all the logic, remove all of the imports, no need for it to be used client anymore, and I'm just going to return projects view, so that's going to be my only import here, there we go, so now I will no longer develop here inside of the app folder, instead I will just import projects view, which is correctly put here in the projects folder, so I don't have to care if you know this accidentally becomes part of the url or something because that's how a routing works inside of the app folder i can safely develop things here in the structure that i want with the folder names that i prefer great so we are inside of the project view let's go ahead and let's start by actually building the layout so i'm going to give this a minimum height of screen so it always takes 100% of the view. I'm going to give it a background color of sidebar which is going to make it slightly darker. I'm going to give it flex, flex column, items center, justify center, padding of 6 and on medium padding of 16. Let's go ahead and open a new div inside and in this one we're going to limit how wide the content will go so maximum width of this content will be small we use mx auto to equally push it from both sides and we repeat the flex call we give it a gap for items center and let's go ahead and keep it at that now let's go ahead and let's add a new div so I'm going to add some spaces so it's easier for you to follow there we go another div with a class name flex justify between gap four with full and items center inside and then let's go ahead and add another div again I'm going to add spaces there we go div flex items center gap two with full and group forward slash logo so this is how it's supposed to be okay inside of here we're going to have an image element and for now let's go ahead and use if we have any image in our public folder so we can add an image later so we don't concern ourselves with that looks like we have versell.svg can i use that there we go perfect and let me give this an alt of polaris which will be the name of our app and let's give it this a class name size dash 32 pixels in square brackets and the medium size 46 pixels in square brackets there we go so a very small one and maybe I should zoom out actually so I see the browser mode here okay and then below the image or should I say next to it we are going to have a text polaris like this and let's go ahead and add the poppins font so I'm going to go ahead and define that const font will be poppins from next font google subsets will be latin and then we need to add the weight property so it's going to be 400, 500, 600, and 700. Great. Now let's go ahead and let's prepare an import of, if you remember, very early in CN. This helps us make dynamic tailwind classes. So we are using it for the first time now. CN, like this. And in the first argument of the CN, I'm just going to give it some default classes like text for excel to make it larger on medium text 5 excel font semi bold text white even though that's already present so no need to do that and then i'm going to go ahead and add a comma here and i'm going to pass font.class name and that's going to change the font. Perfect. So now let's go ahead and let's go outside of this div, outside of this div, and in here let's go ahead and open a new one like this. This one will have a class name of flex, flex call, gap four, and full width. And then in here we're going to create a grid. So class name grid, grid columns 2 and gap 2. Let me just fix the indentation here. And inside, we're going to use our chat CN buttons. So make sure you add an import for that. And I'm going to give this a couple of things. So variant here will be outline. On click for now is just going to be an empty arrow function. class name will be full height items start justify start padding of four bg of background border flex flex column gap six and rounded none so we make it sharp inside of here open up a div and inside a sparkle icon from lucid react so make sure you import sparkle icon and i'm just going to move it up here okay i'm going to fix this little issue i've created the sparkle icon will have a class name of size 4 like this and the div encapsulating the sparkle icon will have a class name of flex items center and justify between as well as a full width then below this we're going to add our shortcut ui so kbd component we have this from chat cn ui so kbd and let's go ahead and add a class name here bg accent and border let's make sure it's closed and then inside of here we're simply going to say what our shortcut will be so for me it's going to be command j since i am on a mac os you can also use you know control plus j whatever you want right if you want this specific icon i would suggest just googling command icon copy paste you know just find it on google okay and then outside of this div i'm just going to add a new div and this pen which says new and i'm going to give it a class name of text small like this so let me go ahead and check it out there we go looks nice we now have this button right here perfect uh let's go ahead and let's copy this button so the entire thing we can duplicate it below there we go now i have two of them so i'm going to work in the bottom one the class will be exactly the same\nthe shortcut will be the letter I. Instead of new, it will say import. And for the icon, we actually have to install React icons for this one. So let's do that. npm install react-icons, simply because Lucid React doesn't really have a good GitHub icon. So make sure you have React icons installed. And then let's go ahead and import FA GitHub from react-icons forward slash FA. And then we can replace down here this one, find import and find sparkle icon and replace it with GitHub. There we go. We now have our new and our import buttons right here. Amazing. So now what we ought to build next is the project list. So where does the project list belong? Well, it actually belongs right outside of this div right here after we close the button so let's prepare it projects list like this now let's go ahead and create it so instead of components here i'm going to create a new file projects dash list dot t s x and let's start creating that so first things first i'm going to add an interface projects list props which will have one prop on the view all function and then we can go ahead and create a proper export projects list on view all don't worry i'm going to collapse this so you can see better there we go very simple so projects list is a component in which we've destructured on view all and we gave it a type of project list props above so make sure you have destructured the props here this can also be written as props like this but we have this structure it since it's going to be the only one here all right so now that we have that i'm going to go ahead and write return i'm going to add a div here with a class name flex flex column and the gap of four like so and then uh let's go ahead and let's actually you know list our projects. And before we do that, let's just render them. So projects list, make sure you import it. There we go. I'm going to give it an on view all to be an empty arrow function simply so we get rid of the type error. And now what I like to do is I like to abstract my hooks. So I'm going to go inside of features projects and I will create a new folder called hooks like this. and my hooks will have a file called use-projects.ts and in here I'm going to import useQuery from convex React import API from convex generated API and I will export const useProjects here like this and return useQuery API projects get. like this. And then I'm going to immediately duplicate this. And I will call this use projects partial with a limit, which is a type of number, get partial. And in here, we have to pass the limit. There we go. So a simple abstraction. So I can now call this instead. So let's do that. Let's get set of projects list right here. And let's do that right here. const projects use projects partial from hooks use projects and pass in the number. So I'm going to do six actually. So six of my latest projects. And if projects is undefined, that means it is still loading. So I'm just going to return a spinner component. You can import this from components UI spinner. And I'm going to give it a class name, size4 and text ring. so if you refresh for a brief second, you see the spinner. In fact, if I change this to true, you can see how it looks like. Let me zoom in so I can actually see what we're developing. There we go. So yes, in convex, if the query result, any query result is undefined, that means it's loading because it's either going to be an empty array or null if we weren't able to find it right so if it's undefined that means it's loading and that is from the documentation of convex right so that's how you detect if something from convex is loading okay now inside of here let's go ahead and do something so I'm going to go ahead and do most recent projects and then I'm gonna do rest from projects. So what did I do here? Well, I will extract the most recent project I have worked on from the rest, simply because I'm gonna have a separate card showing me like, hey, you can continue working on your most recent project. And then here are the rest of the projects. So that's why I'm doing this. For now, let's just do the rest thing. So if rest.length is larger than zero, meaning, okay, we do have the rest of the projects. In that case, let's go ahead and start creating a list for this. So let's give this flex, flex call, and a gap of two. Then let's go ahead and create another div inside with flex items center and justify between and gap two then let's go ahead and create a label so basically a span element with text extra small and text muted foreground which simply says recent projects right okay so the reason we're not seeing that text would indicate that this is actually empty. So let me go ahead and sort of use projects, specifically get partial here. Let's see. Can, oh yes, it is because we don't really have any projects here. So this is what I would like to do right now. I would like to just, just because it makes no sense, you know, to develop thing, a UI thing we don't see, right? So let's just quickly go back to the projects view right here. Let's focus on that and let's prepare our hooks use projects. And inside of here, let's go ahead and let's create export const use create project like so return use mutation from convex react API projects create. like this that's the only thing we're going to do now so let's go back instead of the projects view and now I'm just going to go ahead and well I'm just going to add that hook here const create project use projects use create project from dot dot hooks use projects perfect so now let's go ahead and find this new button right and in here I'm going to go ahead and call create projects and for the name so what should we use for the name well what I the solution that I have found is to basically use a library, which will allow us to create a unique slug. So let's go ahead and do that. The package name is the following. So unique names generator. So let's do npm install unique dash names dash generator. This one. Perfect. And let's go ahead and import everything we need from it. So we will later, you know, move this somewhere else. But for now, it's just going to be here. So let's import adjectives, animals, colors, and unique names generator itself from the same named package. And then once we have that, we are able to actually generate the name. So I'm going to go ahead and prepare that just above here in the onclick function. So the project name we'll call the unique names generator function and instead of its object we're going to add dictionaries. So we're going to use adjectives, animals and colors. I simply found that to be a nice combination. The separator is going to be a dash and we're going to use three words. This is kind of the standard in most of these types of projects you see these days. So there we go. Now when I click on new here, I'm not sure if it works or not. Let's see here. It works. Do I have the fun name? I do. Shaky Canid Teal. So if I click again here, there we go. It works. And you can see my owner ID is stored inside. And now I can even see the recent projects here. Perfect. So make sure you have at least two created. That's why the recent projects were not showing. Remember, inside of projects list, we separate the most recent one from the rest. So make sure you have at minimum two items inside. So maybe for simplicity's sake, we can just use projects for now here. Projects. That will be simpler, I think. Great. So now that we have the span recent projects. Let's go ahead and add a native HTML button with view all text here and a KBD so import it again. Let me just move it to its place here. Okay. KBD and inside I'm just going to do well I'm just going to write the shortcut which will be the letter K and the command. I'm going to give this a class name bg accent and the border there we go and for the button itself it will have a couple of class names so let me go ahead and add them here so you can see just a second flex items center gap 2 text muted foreground text extra small on hover text foreground and transition colors. There we go So yeah this is how it going to look like all right now let go ahead and let actually render the items so outside of this div add an unordered list element ul and let's do projects.map get the individual project here and I'm going to render project item here. Let's give the unordered list a class name, flex, flex column. And for the project item itself, let's pass in a key of project underscore ID, and then data project. And now let's go ahead and let's actually create the project item. And we're going to do this in the very same file, right so it's easier so I'm gonna do this below here or maybe we should do it above so we don't reference it before it's created project item will accept data and the data type will be a type of document we can import doc doc not from Zod from convex. So let me go ahead and find convex generated data model. Is it doc? I think it is doc. And then we have to define from projects. Okay, I think I'm not importing this correctly. the doc is used but it's never read I think I just have to finish the function there we go yes so make sure that we are using the data here we gave it a proper type and now I'm going to go ahead and use a link component from next link so I'm going to give this an href of forward slash projects and then data underscore id like that let's go ahead and give the link a class name. So I like to have my attributes one below another like so. So it is going to have a couple of class names as well. Let me go ahead and add them here. Text small, text foreground with a 60% opacity, font medium on hover, text foreground, padding on the y-axis one, flex, items center, justify between, full width, and it's also going to be a group. so now inside of here I'm going to open a div with a class name flex items center and gap two let me fix my typo here and inside of here we have to render the icon of this project so I will now use globe icon here as default and then I'm going to change it later so make sure you just import Lucid React here, the globe icon. So that's the first thing we're going to have. And then I will add an arrow right icon. Again, from Lucid React, make sure you import that. So outside of this div, and then, let me see if I... My apologies, no. Not yet. We're not going to have this. instead let's just add a span and in here we would have to format the timestamp and for that we're going to use date fns so let's npm install date fns so we have to now import format distance to now and let's go ahead and create a little helper function here so I'm just going to add that here at the top format timestamp will accept the timestamp which is a type of number and it will return format distance to now like this and inside it's going to pass the new date around that timestamp and then there's the second argument options in which we add suffix to be true. Great. now that we have format timestamp inside of this span let's go ahead and simply use it for project so it's called data data.updatedAt there we go. Now let's go ahead and render the project item here project item cannot be used as a JSX probably because I never return anything here So make sure to return something. And there we go. So I can already see two of them here. Perfect. Now let's go ahead and just style this a bit. So what do we have to style? First things first about this span here. Oh, we are missing another span, which renders the data name. There we go. and let's give this a class name of truncate like so for this span right here it will have some larger class name text extra small text muted foreground on group hover text foreground with 60% opacity and transition colors. All right. So we have that. And now let's go ahead and check it out. Let's see how it looks like. There we go. You can see how I create a new one. Perfect. So what should we do next? The new one is finished here. Okay, so the globe is too big. and the globe actually shouldn't be rendered like this. Instead, we should develop a function called getProjectIcon. And we're going to accept project as the prop. And if project import status is completed, we will return FA GitHub icon. so make sure to import this from react icons fa and let's also give it a class name size 3.5 and text muted foreground and then let's go ahead and just do other options right so if import status is failed let's import alert circle icon again from lucid react then let's go ahead and check if import status is importing and let's use the loader to icon so all of them have the same class name except the last one which also has animate spin so just make sure to add that and then let's go ahead and add the default one so outside of all if clauses let's return the globe icon. You can obviously turn this to a switch case if you prefer it that way. For some reason I have an aversion towards switch cases I just don't like them but you can of course use them if you prefer. And then replace the globe icon with get project icon and pass in data. And all of ours will have the globe icon right. But if you want to play around If you go to one of your projects here, I don't know, import status, if I say completed in quotes and save, you can see that one of them was changed to GitHub. If I change another one to failed, you can see it shows an alert. And if I change another one to importing, it's a spinner. So there we go. those are all the instances that can happen great now let's go ahead and show the last project we worked on into a separate container simply so we have some better user experience here so i'm just going to copy this because it's identical as what we're going to build and it's going to be called a continue card like this so it will have the exact same props here and in order to make it immediately visible let's go back inside of projects list here and then let's go ahead and do the thing with most recent and the rest so now let's use the rest dot length and let's use rest dot map which will as you can see remove one and then what we can do is just do continue card and pass in the project or let's do data most recent great now let's go ahead and develop the continue card inside of here let's return a div with a class name flex flex call and gap two then inside of it let's add a label span with a text last updated and text extra small text muted foreground and that's it then let's add a button but this time a button from components ui button there we go and let's go ahead and give it a few props so it's going to have a variant of outline it's going to have an as child prop and it's going to have the following class name height auto items start justify start padding for bg background border rounded none flex flex column and gap two. Then let's render a link. Let's give this an href forward slash projects and then render the data underscore id here. And the only class name the link will have is group. Then let's go ahead and render a div inside with a flex items center justify between and with full class name Then let go ahead and open a new div inside Flex item center and gap two And then inside of here, we're going to render get project icon for that specific project we are continuing our work on. And next to it, we're going to add a span with font medium and truncate class name to render the name. And then outside of this div, let's render an arrow right icon from Lucid React with a class name size 4, text muted foreground, group hover translate x.5 and transition transform. There we go, like this. now let's go ahead and render the format timestamp as well so outside of this div text extra small text muted foreground and let's render the timestamp there we go so now this is our kind of last worked on project so we can access it easily here so we don't have to search it through here. Excellent. So this is now working pretty good. What I want to test is what happens when I have no projects. Uh-huh. That's what happens. So let's go ahead and make sure that inside of projects list only if most recent exists we show that otherwise we don't render anything. there we go so let's click new here and there we go that's how it looks like and now i want to show you something that we can do to improve our hook actually so instead of use projects if you find use create project you can see there's kind of a slight delay happening right and during that delay we don't even block this. So what we can actually do is we can add an optimistic mutation. So with optimistic update like this and let's just push a new project to the list sooner. So let's open local store and the arguments and then let's go ahead and let's get all the existing projects. So existing projects using local store get query api projects get if existing projects is not undefined meaning they have loaded let's go ahead and kind of simulate a new project so date dot now const new project will be an object underscore id will use crypto dot random uuid and we are we have to cast that as the id from convex so make sure you add this and let's also add doc here besides id we will have creation time which will use now name which will use arguments dot name owner id which will be anonymous updated ad which will be now we could also use user id use out from clerk next js and then we could pass user ID here. I think that should work too. So you can import use out here so we don't pass anonymous. And then what we have to do with this new project is call the local store and set the query API projects get skip the arguments part and then inside of here let's go ahead and add new project to the list of existing projects and let me just see what type of problems do we have here so we have date.now and this can actually be fixed with a comment actually cannot call impure function let's just go ahead and add eslint disable react hooks purity. So that should get rid of that. This is what I've added. And now I'm trying to figure out ah, I see. So let's just use anonymous. It doesn't matter. Either way, this is not backend anyway. So now we have optimistic update. And if I'm correct this should now appear instantly and i think it does it's like significantly faster let me go ahead and delete all of them again so we can test it out so now we don't even need like any loading state because it's instant like it's like it's super fast great so yes you can do optimistic updates with convex as well so i believe there's one thing left to do here and that is to implement the view all and the actually it makes no sense to implement shortcuts because shortcuts will open dialogues so the only one that makes sense is the view all one so we're gonna we're gonna create a project command dialogue so inside of components new file projects dash command dash dialogue dot tsx let's mark it as use client and let's go ahead and import everything we are going to need so okay we actually don't need use client because it's already within a client component so use router f a github the following icons from lucid react alert circle icon globe icon loader to icon. Then we're going to need all of these from components UI command. So dialog empty group input item and list. All of this come from chatsy and UI. And last but not least use projects from hooks use projects. Then let's create an interface projects command dialog props to accept a boolean called open and a function on open change which will be used to modify that boolean. Then let's go ahead and export the following function projects command dialog using our projects command dialog props and destructuring the props above. Perfect. Let's define the router here. Let's define projects. Let's quickly define handle select method. So whenever we select one of the projects from this dialog, we will get project ID from that selection. And we can use the router dot push method to redirect to projects project ID like that. And then let's call on open change false so we immediately close this dialog great just make sure you have imported use router from next navigation because in the previous versions it had another one so now i'm going to go inside of projects list i'm going to find this function get project icon and i'm going to copy it so the entire function. And I'm going to go ahead and let's see, I'm going to add it up here. I will see if this ends up being identical, we can export it. But for now, we're just reusing it in two places. And yeah, let's just do doc here, we have to import this from convex generated data model. So make sure you have this import. We now have get project icon. Perfect. And okay, the only difference is going to be the size of the icon. So instead of 3.5, we're going to use four. It's just a subtle difference, but it makes it look a little bit better for the dialog, right? And now inside of here, let's return. And then let's go ahead and render everything we need, right? We need the command dialog. The command dialog should have open and on open change, which we can immediately pass along. Let's give it a title and description relevant to what they do. Search projects, search and navigate to your project. Let's render a command input with a placeholder, search projects. And then let's go ahead and render the command list. Let's go ahead and define the empty state, which will simply show the label no projects found. Then let's go ahead and add a command group with a heading of projects. And then inside of here, let's iterate over our projects with a question mark in case they are empty. And in here, we are going to render the command item. And it looks like I'm doing something incorrect here. Let me just figure out what. There we go. I was missing another parenthesis here, right? So I have to add it. So let's give the command item a key of project underscore ID. Let's give it a value of project name dash project ID. The reason we are doing this is because if you just use, you need to use the project name because that is what will be highlighted when you search. So it's going to look for value. The problem comes when you have projects with the same name and then all of them are highlighted. So that's why we kind of combine it with project ID. So they are kind of unique in their own way. And let's add on select to call our handle select method and redirect to the project ID. Then let's go ahead and render the get project icon. And finally the project name Now we have to go back to the project view page And let add a state Command dialog open set command dialog open controlled through use state. I'm just going to move this to the top. And now that we have this, we can go ahead and render it in the return. So I'm going to do that within this fragment. So I'm going to encapsulate the entire content of the return inside of a fragment. So my order is semantically correct, because the projects command dialog is above all of those other elements, right? So make sure to import projects command dialog which we have just developed here right make sure you have exported it correctly and I'm going to go ahead and give it open and on open change there we go and now what we have to do is we have to pass the on view all option properly so find project list and pass in the on view all to call set command dialog open and set it to true. Now I just have to confirm that I'm using on view all within the projects list. Looks like I'm not. So I'm just going to go ahead and find my recent projects here and then here we go. This button right here will call on click on view all. Let's test it out. So I'm going to go ahead and click view all and here we go. So let's search for Cougar. There we go. Perfect. And clicking on it will redirect me to a 404 because, well, we haven't developed that yet. So what's left to do now is just add a shortcut. So let's go inside of projects view back here. And using use effect, we can very simply achieve this effect. So use effect. Make sure you have imported it from react same place as use state const handle key down event keyboard event if e.meta key or e.control key is included and then if e.key is equal to k E prevent default set command dialog open to true. As simple as that. Let's add document add event listener on key down and passing the handle key down. And super important, make sure to unmount it. So document remove event listener on key down handle key down. and let's move the state above the use effect like this so it can properly access it. So if you try pressing on the control key or the command key if you have macOS and hold the letter K at the same time, you can see you can now open view all. Amazing. and to end the chapter I think the only interesting thing to do is add a real logo to our app so I found the logo on untitled UI and usually I also use logo Ipsum so whichever of those you like go ahead and just click copy you know and that will copy the SVG I think it works very similarly here. There we go. Yeah. So, Untitled UI and Logo Ipsum are my go-tos for finding logos. Or if you want the exact one that I have using the link on the screen, you can visit my assets, find logo.svg. I'm going to copy raw file. And then I'm just going to go ahead inside of source, my apologies, inside of public and I will create logo.svg. I will confirm this open using standard editor and I will paste inside. There we go. That's how I do it. You can drag and drop if you want. There we go. This is the logo. So let's go inside of page.tsx projects view and let's remove the or sell one and use our logo.svg. And yeah, this is a warning that we should use next image instead of image. If you want to, you can just turn that off. Let me use for this line. No? Not working. Okay. Yeah. It's just going to show like this. It's okay. It's just a warning. And that's it. I believe this is exactly what we've envisioned. Obviously, we don't have the dialogues, but those will come. And yeah, everything else works just as it should. Let me see. I think the only thing we might want to improve could be instead of convex projects, maybe the way, maybe the order of fetching these so we show the newest ones first let me see yes we can do that so in dot get let's go ahead and do dot order descending and in get partial let's do the same thing descending let me refresh and that should reverse it now yes because when you create the newest one that one should become the last updated one. There we go. So now they are constantly switching. You can see how fast this creation is. That's optimistic mutation. Amazing, amazing job. I'm super satisfied with how this went. So let's go ahead and review all of these changes. There's a lot of them, right? So I'm going to go ahead and do git add and then a dot git commit this is chapter 7 so 0 7 projects there we go and then git checkout dash b 0 7 dash projects i'm i keep checking if i'm correct with my names now because of that number I missed. So git push origin 07 dash projects. There we go. Perfect. So we have officially pushed that branch. And now let's go ahead and review it. So I'm going to open a pull request. And this time we can definitely let CodeRabbit review all of these changes we just did. So in this pull request, we added projects interface with search functionality using the command dialog. We added project creation, which now generates random project names automatically. Recent projects overview displays the most updated projects. And we also have added the export status for tracking projects. If you remember, I added this because I thought we might use it for the UI. Looks like we only use the import status, but still it's not going to hurt. We removed the demo and the example pages and we added new dependencies. And looks like our code was very good. No comments from CodeRabbit besides some nitpick comments, which are also interesting. As you can see exactly what we said, we could potentially extract get project icon to avoid duplication. And in the projects view, it is detecting that we are missing a keyboard shortcut, which is very interesting because the only thing it knows is that one of them is implemented, right? And it only knows that in the UI, we show the other one. Very, very interesting. But yeah, we will add that later when we do. Again, consider using the Next.js image components. So same as the linter. We do have some buttons with no functionality like the import button. We will add that later. In here, we added slint disable for the date.now. So it suggests explaining why we did that. So you can see it even agrees with us. It just wants us to explain why. Overall, very, very good. yes we can definitely you know validate the limit parameter but yes most of these are just you know nitpick comments but still very cool to have someone else take a look at the code before we merge simply so we know if we made any serious mistakes amazing so that is a branch 0 7 projects now let's check out the back to our main branch and let me go ahead and just try it like this there we go. Git checkout, my apologies, git pool origin main. This should now synchronize our local state with what we just merged. And as always I like to confirm this by going inside of my source control, opening the graph below and here we have it. 07 projects detached into its own branch and then merged back to main. I believe that marks the end of this chapter. We built the landing page with Polaris branding. We built the project creation and get feature, project list layout, and finally keyboard shortcuts and the command dialog. Amazing, amazing job and see you in the next chapter. In this chapter we're going to develop our IDE layout. We're going to start by creating a dynamic project routing system. Then we're going to build resizable IDE split panes using a package called allotment. We're going to implement a navbar with project actions, set up editor and preview containers, and add code preview tab switching functionality. So how is that going to look like? Well, we have a finished example here. So this is the finished product. We don't have this yet. And we're going to start developing some things here now. So let me go ahead and show you what. the first thing we're going to do is this navbar right here or at least we're going to prepare this navbar right so that's what we're going to implement and then we're going to implement the basic split pane this will be pane on the left side and this will be the pane on the right\nside and besides that we're going to implement the ability to switch between code and preview but we're not actually going to implement any of the contents inside right so let's go ahead and start building make sure you have your app running at npm run dev and what we've built last is this the landing page and currently if we try to go to a project we get a 404 so let's start with resolving that and let's also get rid of our user button on every single screen. So I'm going to go ahead inside of source, components, providers. I will remove the user button from the authenticated state and I will remove it from the imports. Speaking of unused imports I believe there's one inside of app layout. At least I have it. So I'm just going to remove it. Great. So now we have a clean 404. Now let's fix it. So how do we do that? Well, let's take a look at our URL. localhost 3000, projects. And here's the tricky part. We now have something dynamic. So far, we've only learned how to create URLs when we know exactly the words. But it's not difficult to make it dynamic either. So we already know the first part. Inside of the app folder, create a folder called projects. And now what do we put inside? Well, it's actually very simple. Open square brackets and then type the name of the variable where you want to store the param. In our case, that's going to be project ID with a capitalized letter I. And then inside, add a page.tsx. And you will probably notice that you will now get a different error here. That's because this is a reserved file main. So it officially registered this route and we are hitting that route here. But the problem is page is not exporting anything. So it's not really not found anymore. is just incorrect. That's why we have to export something. Project ID page. Div and let's prepare just a label which is supposed to show the project ID which we've entered. So how do we access this project ID now? Well through params. So let's go ahead and prepare the params here and let's give it a type. A type of params is a promise and then inside what variable we expect. Well we expect project ID. So let's go ahead and add it here and let's give it a type of string. And then let's go ahead and make this entire thing asynchronous like this and let's destructure project ID from await params. And let's render project ID. There we go. So now no matter what you type in your URL, like 1, 2, 3, 4, you should still get the same result. See? It should be immediately reflected. The reason I explicitly mentioned with a capital letter I is because people often overlook this. If I rename this to something like this and update the imports, you don't have to do this. You can see it just causes a bunch of cache issues, right? And if I refresh here, it looks like it's actually still working. That's interesting. I'm interested now because I wanted to teach you a lesson, but maybe I actually learned something new. So I'm purposely deleting dot next to get rid of the cache. Because what I'm expecting here is that this shouldn't load and it doesn't. Okay, so it was just cache, right? Basically, if you are not careful with how you name your folder, you will probably write the wrong variable here. That's what I was trying to tell you. That's why it's important to do that. So if you wanted to fix this, you would now have to use lowercase project ID. You can see that resolves it, but we don't want to use that. We want to use the proper camel case. So because of that, I'm going to rename it once more. And then again, I'm going to have to update my imports here and most likely remove cache and run it again. Let's restart and we should be good to go. There we go. Now let's go ahead and let's create the layout. So layout is also a reserved file name here. And the reason we're using layout is because layout will not re-render on every project ID change. But we are kind of going to need to have it to be static. It's kind of easier to explain if we just start building it. So it's quite similar. Let's go ahead and do layout here. And well, for now, let's just render the children because that's kind of the only important thing. We've never written a custom layout before, so I will stop to explain a bit. So the type will be React React Node, and that's not where I type this. My apologies. This is where I type that. and just by saving everything should work perfectly fine right so what's the purpose of a layout why did i just do this first of all it's a reserved file name so make sure you didn't misspell it second of all it's going to be used like this imagine this is a nav bar and i know it doesn't seem like much yet but if i create another thing here for example settings page right so imagine each project has a settings page and if I add page inside you don't have to do this I'm just trying to demonstrate settings page div whoops project settings page how do I access this now so I go you take a look at the url projects 1234 forward slash settings that's how I access it what are you noticing here? So this content has changed, but the layout content hasn't changed, right? So I'm just being semantically correct when it comes to the things I want to put in my layout and the things I want to keep in the page. The layouts are also less prone to re-rendering, making the app more optimized. For now, we can get rid of the settings one. We are not going to need it. I just wanted to explain to you in that way. So yes, let's go back to just projects and then one, two, three, four. Great. Let's now focus on this layout.tsx and we can actually access the params here as well. And they are the exact same type. So let's add them. There we go. And let's make sure layout is asynchronous as well and let's destructure it and let's change our return to very simply be project ID layout which we don't yet have and render children inside. Let me go ahead and add parentheses around this so it's easier to look at and the only thing we're going to pass as a prop will be the project ID. So when I save, we're going to get an error because project ID layout doesn't exist. So let's go ahead and create it. Inside of source features, projects, components, let's create project ID layout dot t s x. I'm going to mark this as use client and then I'm going to export const project ID layout. the children the props I will expect whoops the props I expect are children and the project ID children are a type of react react node and project ID is a type of string and then let's go ahead and return a div and let's just render the children so we know it's working now let's go ahead inside of app projects layout and let's import project id layout from features projects components project id layout and since i can see my children it means everything is correct we can now focus exclusively on the project id layout inside of the project id layout we're going to go ahead and give this container div a class name of full width, height of screen, flex, and flex column. And then we're going to implement an actual navbar component and pass along the project ID. Let's go ahead and let's implement the navbar component. So again, inside of projects, let's create a new file called navbar.dsx. And I'm going to import a type of ID from convex generated data model. I will export const navbar. I'm going to prepare the types here. So we expect the project ID and I'm going to do something different. I'm going to give it a type of ID projects. The reason I'm doing this is that I can easily pass it to convex queries. So in order to make this work, first of all, let's make sure we return something so hello navbar then let's go back to project id layout let's import the navbar from dot slash navbar and you can save and it will work so we now have hello navbar here but the project id is incorrect so we're just going to do the same thing here instead of project id layout give this a type of id projects id is also a string but we are just making sure that it works as an identifier for convex. Great. So now that we have this project ID, let's go ahead and let's style this a bit. We are going to use project ID later. For now, let's give this a flex, justify between, items, center, gap, x2, padding 2, background color of sidebar, border, bottom, and border be border. Actually we don't need that. We can just do border bottom And then in here let create a div with a class name flex items center and gap x2 Let's change the outer one to nav actually. I think that makes more sense, right? Now what we have to do is we have to import all the components from Shatzian navbar. So that's going to be breadcrumb. My apologies from Shatzian breadcrumb, not navbar. so breadcrumb item link list page and separator all from components ui breadcrumb and once we have that we're just going to create a composition to render the breadcrumb so starting with the actual breadcrumb then inside we're going to add a breadcrumb item then we're going to add a bread crumb list with a class name and another property. So let me just go ahead and properly indent this. So we have a class name flex items center gap 1.5 and group forward slash logo. In fact we don't need group logo just this and an as child prop. And inside of here we're going to render a button. Make sure you import button from components UI button. So it's chatCN button. The button will have a variant of ghost. It will have a class name of with fit with an exclamation mark. This basically means important, like override whatever other style it had. Same thing for padding and same thing for the height. And as child here as well. Then finally inside, we're going to import link. from next link and give it an href to go to a well to the root page so just make sure you've imported link from next link great so what will actually be displayed here well an image so let's import next image let me show you what the import looks like just a second. There we go. Next image and I will put it right next to link. The image will have an href, my apologies, a source of forward slash logo. We are going to have an alt of logo, width of 20, and a height of 20 as well. And then next to the image we're going to have a span polaris. let's go ahead and give this a class name and let's use the cnutil library so in the first argument here we're going to put text small and font medium and then what i'm going to do is i'm going to go back to projects dash view component and in here i'm just going to copy the poppins instance. And then I'm going to go back inside of my navbar here. I'm going to paste this and I'm going to import poppins from next font Google. And I'm just going to move it here at the top. So now we have the poppins font again. So I can now add a comma. And then in the second argument of the cnutil font dot class name, making this have the poppins font. Great. So that's the first item. And now let's go ahead and add a breadcrumb separator. And actually it's a self closing tag. The only class name we're going to give it is ml-0 and mr of one. And then let's add a breadcrumb item again here. so what should we write here well we should use the breadcrumb page that's the one we should use and inside of here for now let's just do demo project and for the class name let's give it a text small cursor pointer hover text primary font medium maximum width of 40 and truncate let me go ahead and zoom out so we can see how this looks like so looks like it's not taking a hundred percent height. I apologize. Width looks like it's not taking a hundred percent width. So let's debug Y. Okay, that works. Let me go back here. Yes, this should definitely expand to a hundred percent width. So let me just go ahead and see why that is not happening. Oh, it is because I gave you an invalid composition. So, instead of breadcrumb, we should have breadcrumb list. We're not using that. So, let's go ahead and encapsulate both of our breadcrumb items within that list. And then let's indent everything together. There we go. And I'm going to give this a class name of gap0. All right. So let's take a look now. This is our navbar. In here, we can see we have what will be the text of the currently loaded project. And in here, we have a button to go back, right? So our next step is to actually load the name of the project that will be here. But before we do that, we can actually do one easy thing. So let's go at the bottom here. and in here let's just add a class name flex items center and gap two and just render user button from clerk next js so make sure you've imported user button from clerk next js and now you have a place to log out so if you need to change your account you can just go ahead and log out from here. Great. So let's see what's next. So we can use the project ID to actually load the project. In order to do that, we have to go inside of convex projects.ds and we have to develop get by ID. So I'm going to copy the existing get and I will rename it to get by ID. the arguments it's going to accept is id which will be a type of vid projects so now that we have the arguments we also have to extract them here and the first thing we're going to do is just attempt to get the project using await context database get projects and then pass in arguments.id that's the first thing we're going to do then we're going to add an if check if the project is missing and we're going to throw a new error here project not found that's the first check the second check will confirm if project.owner id is not identical to the current identity.subject meaning we have to throw a new error unauthorized access to this project because this user should not be looking at this project. If all of these cases pass, we can safely return the project. You might notice that I'm not sure how well you know Convex, but they've undergone a change recently. This also works just fine because their IDs are kind of special. You can see you have to define that it's an id of the project. So in the past, you just passed arguments.id and it immediately knew it has to load project. They've done an update where you can be more explicit. And it's super cool they made this backwards compatible. So both work, but this is the new way. So you should be writing it like this. As far as I understand, the reason they're doing this is A, to be more explicit, and B, to enable local running of convex. Because right now, this ID thing is probably related to the cloud. So if you want to enable someone to run convex 100% locally, they have to fix that first. So they are working towards that. And I kind of like this explicitness more. Great. So we have the get by ID. Now let's go ahead inside of source features projects, hooks use projects. And inside of here, let's implement a super simple hook use project. So make sure to not name it the same. It's use project. It will accept one argument, project ID, which is a type of ID projects. It will return use query API dot projects dot get by ID with the argument id of project id so this is the new route that we have just created right and we only accept that so make sure you have this and click save great now let's go ahead and go back inside of the navbar.tsx and let's load the project and it should be fairly easy now that we have the hook So, project from useProject and pass in the project ID. Make sure to import useProject from hooks, useProject. There we go. And we should probably mark navbar as useClient. So, let's do that, useClient. There we go. That will get rid of the error. Or not, because that's not what the error is. the error is because we added a new function but we didn't run or should I say aren't running actively npx convex dev so make sure you have convex functions ready here and let's do one more refresh there we go but still we're not using this project anywhere so now let's go ahead and scroll down to demo project breadcrumb page and do project question mark name or loading. There we go. You can see that for a brief second it's going to say loading and then it will change to the name. So try with your project and you should see different name for every project. Excellent. So now we have to add the ability to rename this and that will be a little bit more tricky but not too difficult Don worry I want to start by actually building the rename mutation So let head back inside of convex projects I going to copy get by id because it very similar I going to rename it to rename and it will accept a name and it will accept an id and a new name which will be a type of string. There we go. First things first, we do the identity check. We fetch the project. We confirm it exists. We confirm we have access to it. And then what we do is, well, we don't have to return anything. Let's just do await context database patch. And again, you can just pass arguments.id, but I like this more. And this is the new way. So get used to doing this. Let's pass in the new name and let's move the updated add to be date dot now. All right. So let's see. Yes. Why does it not exist? Because this isn't a query. This is a mutation. So make sure you change this. And we have it imported from here. Great. And now it should work just fine. Perfect. I believe it returns a project ID back. No, it doesn't. Okay. no need to return anything so we now have the rename mutation perfect now let's add it to our hook use projects here it is so I'm gonna go ahead and I'm gonna copy use create project because it's more similar to that than anything else so I now have copied the use create project and I'm going to change it. So the argument we're going to accept is project ID. The use mutation we're going to call is projects.rename. And let's do optimistic update here as well. So instead of calling existing projects, we are going to change this to call an individual existing project, right? So let me just go ahead and see. I'm trying to make this easy to look at. okay, so getQuery, API projects getById, which we previously developed, and pass in the argument id, projectId. I think I can collapse this like so. Just trying to make it easier to read. Okay. So now that we have the existing project, let's go ahead and check how we should actually update this. So I'm just going to remove everything inside. We're going to check if we have the existing project, meaning it is not undefined, and if existing project is not null, right, because undefined means loading, and null means not found, only then can we call our local store setQuery API.projects getById, id, project id, so we are going to find it in the local cache and then we're just going to spread the existing project and change the name and we can also change updated add to be date dot now which isn't exactly correct because it's going to be different on the server but good enough so that's for updating the existing project but we're still not done because now we have to push it to a list of our existing projects. So for that I believe we can copy this from use create project. Yes we can rename this to use rename project. So we get rid of the error. So after this first if block here let's again fetch for the existing projects. check if existing projects are not loading and then call local store set query api.projects.get skip the arguments go inside of existing projects existing projects so be careful right dot map, get the individual project and in here return project underscore id matches arguments id. If it does spread the project property, change the name to arguments dot name and update it at to date dot now. Otherwise, just return the plain project. There we go. So I believe that is it. That's what we have to develop here. So this will change both the query for get by ID, and also it will update the project in all the places where we fetch projects. So a powerful optimistic update. We now have user name project, but what we have to develop is the UI so that when the user clicks on this, it changes to an input. So I'm going to go back inside of navbar.tsx and I'm going to start developing that. So first I'm going to add our new method, rename project, use rename project, and pass in the project ID. Make sure to import use rename project. Great, that's the first thing. Then let's prepare two states. IsRenaming and name. And then just import useState from React. I'm going to move it here to the top. Okay, so we should have renameProject, isRenaming and name, as well as its setters. Now that we have that, let's go ahead and see what should we render when we are renaming and what should we render when we are not. So down here, I'm going to find breadcrumb page. And actually, I'm going to focus on the breadcrumb item. So if is renaming, I will render a native input. Otherwise, I'm going to render a breadcrumb page. So let's indent this. There we go. And on the breadcrumb page, let's give it an on click handle start rename. And now let's go ahead and just develop this method real quick. So constant handle start rename. We'll check if we don't have a project and immediately break. And then call set name with project dot name. and set is renaming to true. So in case project hasn't loaded, we're just going to break the method. Otherwise, we're going to set the local name to be project's name that we loaded. And then we're just going to change set is renaming to true. What this will do is it will render the input. And inside of this input, let's see what we have to do. So I want to auto-focus on it. I want to give it a type of text, value of name. On change, let's get the event and call set name event target value. On focus, event current target select. On blur, for now an empty arrow function. on key down an empty arrow function as well. And now let's focus on the class names here. So we're going to have text small, bg transparent, and text foreground. Then we're going to have outline none, focus ring one, focus ring inset. And then the last few focus ring ring font medium maximum width of 40 and truncate so feel free to pause the screen and confirm you have the entire class name here and let's test it out so now when i click here it turns into an input but currently it doesn't really do much right we cannot submit we cannot cancel and when we refresh we can very well see that it wasn't saved so let's implement the function we need to actually save this. We have only implemented handle start rename so far. So now let's do const handle submit. The first thing we're going to do is set is renaming to false. And then let's go ahead and trim the name. So name dot trim. If this new trimmed name doesn't exist, meaning it's falsy, meaning it was just a blank space which we don't accept as a name or if trimmed name is exactly the same as current project dot name and to fix this we can just check if there is no project return there we go break the function as well so we're not going to waste resources updating to the same thing. Let's call rename project, give it an ID of project ID and name of trimmed name. Great. And now let's go ahead and implement a super simple handle key down. So handle key down will accept an event, which is a React keyboard event. If that key pressed is enter, we're going to call our previously created handle submit. Otherwise, if the key is escape, we are simply going to toggle off the renaming functionality. Now that we have both of them, let's go ahead and add them to on blur and on key down. So right here. There we go. So let's refresh for good luck. and let's change this to test and press enter and you can see it's immediately optimistically updated right so optimistic update immediately you don even feel that it loading and you can see it reflected here as well So definitely works Amazing job So that that finished There is one more element left to develop here And that is the import status indicator. In order to implement the status indicator, we first have to import all the components from tooltip. So tooltip content and trigger. Then let's go down here. And after the breadcrumb ends, but still inside of this div, let's go ahead and check if project dot import status is importing. In that case, let's render a tooltip let's render tooltip trigger let's add as child prop and let's add a loader icon from lucid react so make sure you add this import i'm going to move it here the loader icon will have a class name size 4 text muted foreground and animate spin let me just write the alternative here so I don't get that error even though I still have the error okay so that's the tooltip trigger and the tooltip content will simply say importing And now for the alternative, let's check if project?updatedAt exists, even though it should always exist, though. It is set at undefined. Let me check. UpdatedAt is always a number. Okay, yeah, we can do it like this. It's okay. copy the tooltip paste it inside the trigger will be a little bit different the trigger will be a cloud check icon from lucid react the class name will be identical besides I mean accept animate spin like that and then the tooltip content will tell the user when was the last save So saved, add this space, format distance to now from date FNS. So make sure you import that. We installed this package in the previous chapter. In the first argument, pass in project updated at, and in the second one, add suffix to true. So let's check it out. Next to our name, you can see that it says saved five minutes ago. So if I update it, saved less than a minute ago. Let's try and find some of my projects which are importing. Do I have any? I do not. So I'm going to go to dashboardconvex.dev, and I'm just going to purposely change one. So here in my database, I'm going to find one project, maybe this one, two, three, so it's easy to find. and I'm going to change its import status to importing. So there we go. You can see how it's changed here. And when I click here, you can see it says importing. It's a little bit twitchy, but okay. Let's leave it like this for now. That's it for the navbar. Our navbar is now finished. We can now focus on the bottom half, which will basically have an allotment pane to separate the items. So that's it for the nav bar. Let's go ahead. I guess the only thing that's kind of worrying me is this syntax. It feels weird. I'm not sure it needs to be like this. I think we can render it regardless. And then in here, do the thing. So if project question mark updated at, then use format distance to now. Otherwise unknown. I guess that's like an edge case. Weird syntax, but let me see if I can somehow make it easier for you to read. Okay. And just move this here. okay i guess you can recognize this right if we have project updated at we format its distance otherwise unknown we have no idea when it was last updated because we don't have that data which would be very weird because we always have the updated ad property cool or how about loading yeah how about we okay i'm doing too much but you can do whatever you want this is an edge case that should almost never appear. Great. Now that we have that, let's focus back inside of our projects, components, project ID layout. What we have to do now is we have to install a package called allotment. So npm install allotment. And I'm going to show you what version I'm using. So package.json allotment. This is my version in case you want to use the same. and this is its npm page. So what's cool about allotment is that it has an industry standard look and feel. So if you like VS Code's split view implementation you are in luck because this component is derived from the same code base and I actually like this more than I like ShadCN's resizable component because those resizable components for some reason do proportional expansion on zoom and that just doesn't look or feel good but this this feels perfect so we just installed that and let's go ahead and add it to our project id layout what we have to do is we have to import allotment from allotment and we now have to define some constants here so minimum sidebar width, maximum sidebar width, default conversation sidebar width, and default main size. Then let's go ahead and wrap our children here in a div. Let's give this div a class name, flex1, flex, and overflow hidden. and then let's add allotment around the children itself. The allotment will have a class name of flex1 and default sizes of default conversation sidebar width and default main size. So let me collapse them so it's easier to read like this. which means that we're now going to have to add two allotment panes here. So allotment.pane number one, and allotment.pane number two around the children, like this. Inside of this one, let's give it a snap property, a minimum size of minimum sidebar width, maximum size of maximum sidebar width, and preferred size of default conversation sidebar width. And then in here, I'm going to add a div conversation sidebar. So let's go ahead and save this. And so far, we're not really seeing much, right? So if you take a look at how allotment should be used, we install allotment we use allotment but we also need to import its styles so let's make sure we add that for now i'm going to add it here actually i think we have to like do it at the end there we go and here it is here is the split you can see how it has this almost recognizable VS Code like highlight. And you can also snap, I think. Maybe you can't. Let's see. Did I enable snap here? I did, which should make it possible for us to snap this all the way back. But maybe I'm not understanding its property correctly. There we go. You can snap, which kind of means hide it completely and you still have an option to bring it back. So this is where our conversation sidebar will be. And now we're going to implement this part right here in which we're simply going to implement, you know, the tabs. So where is this part? Well, it's the children. So let's go ahead inside of, we don't have it yet. So instead of app folder projects, project ID page, this is where that is. So let's improve that. So instead of rendering the project ID here, we're just going to render project ID view and pass it a prop project ID. Obviously we have an error now because this doesn't exist. So let's create it. Back inside of features projects components I will create a new file project-id-view.tsx. I'm going to mark this as use client and now I'm very simply going to prepare the following and let me end it here. There we go. So project ID view. It's going to have one prop, project ID and that project ID will be the same type as in our project ID layout if you remember ID projects. So let's import ID from convex generated a data model and let's return a div project ID view. Great. Now let's go back inside of the project ID page and let's import project ID view. And in order to fix the type error, we have to change this to be the ID projects as well. There we go. All fixed. So we should now have the conversation sidebar and the project ID on the other side. Going well. Let's go ahead and set up project ID view here. And now what we're going to do is give this a class name, height, height,\npull, flex and flex column, create a navbar with a class name, height of 35 pixels, flex, items center, background color of sidebar, and the border bottom. And then let's develop a component called tab. This component will have some props, such as label, which is a type of string, is active, which is a boolean, and onclick, which is a void, I mean a function. So label is active and onclick. Let's go ahead and return a div, a span inside, and render the label. The span will have a class name of text small and the div will have an onclick onclick property and a class name which will use the cnutil because this will be dynamic based on its active prop. So if is active, it will have a bg background and text foreground. but if it is not, we're just going to have some default classes here, such as flex, items center, gap 2, full height, px3, cursor pointer, text muted foreground, border, right, hover, bg, accent with a 30% opacity. That's our tab component. Now that we have our tab component. Let's go ahead and render it inside of here. So tab like this. Label preview is active false on click empty arrow function. Duplicate this and change this one actually the first one to be code. And yes in here we have a warning so looks like this can be written as height 8.75. Really cool. Let's write it as such then. And now as you can see, we have tabs, code, and preview. Clicking on them doesn't do much though. So let's improve that. In order to do that, we have to introduce a state. So right here in the project ID view, use state. Make sure to import this from React. So I'm going to move this to the top. So what type can the state be? Well, only two things. Either an editor or preview. And by default, it's going to be editor. And the property that we are going to store is active view and set active view to control it. So now we can modify is active and on click here to be like this and call the editor and the opposite in the preview. If active view is preview and set active view to preview. There we go. You can now switch between the two states. Perfect. Now we have to add some content here. So outside of the nav here, let's create a div with a class name, flex1 and a relative. and inside of here a div with a class name cn absolute in set 0 if active view is editor it is visible otherwise invisible and a div editor there we go and then let's go ahead and do this again so I'm just going to copy this and we're going to do the opposite here. So if active view is preview, render preview. Let's try it out. So when I click on preview, I can see the preview content and when I click on code, I can see the editor content. Great. So one thing left to do is to add a simple export to GitHub button but without any functionality. It's not even going to open anything. I just want to have the UI ready. So right below our last tab here, we're going to add a div. This div will have a class name, flex1, flex, justify end, and hide full. And then inside of here, we're going to do the following. So this will later actually be in a separate component, but for now we're going to keep it here. I want you to copy the entire class name from the tab and go ahead and add it here in a div. So class name and just paste the entire thing. The only thing we're going to change is from border right to border left and gap to be 1.5. And then inside of here, let's go ahead and render FA GitHub, which we can import from react-icons forward slash fa and let's give it a class name of size 3.5 and then a span which will say export with a class name text small so let's go ahead and check it out there we go we now have a button to export to github that currently does nothing but will in the future. Amazing, exactly what we envisioned and more. I didn't even completely forgot about this indicator but we did it on the fly and it was quite easy for us to do so, wasn't it? So we are now ready to start doing some real work here, aren't we? Pretty good setup so far. So we've created dynamic routing system, we build resizable IDE split panes, we implement a knobbar with project actions. We've set up editor and preview containers and we added the tab switching functionality. Awesome. Let's go ahead and merge all of that. So this is chapter eight. I'm going to shut down all of my IDEs here. I'm in my terminals. So git add dot git commit 08 IDE layout git checkout dash b 08 IDE layout git push dash u origin 08 IDE layout and once we've pushed it you should see it right here and now we can go ahead and open a pull request So I'm going to go ahead and open a pull request and let's review our code. And here we have the summary. New features. We added individual project detail pages with dedicated layout. We enabled inline editing of the project names in the navbar. We introduced split pane interface with sidebar navigation. We added code and preview tabs for project viewing. We added a placeholder of project exporting and we are displaying project status indicators, import progress and save timestamps. Let's take a look at two important comments, actually three important comments that CodeRabit left. It actually found some bugs which I've left. The first bug is instead of our layout.tsx, it's a tight mismatch. Basically, in here, we define it as a string. But CodeRabbit knows that inside of project ID layout, we expect a type of convex ID of projects. We can actually confirm this bug. So if I go inside of app folder, projects, layout, you can see it's a bug, right? It's a super easy fix. We just have to modify this, but I'm going to leave that for the next chapter. I don't like to modify my branches once I push them. so that's definitely a thing to fix. Great catch by CodeRabbit. Then we have the second one. In here, it mostly talks about how this is an incomplete implementation, which is true, right? We have this unused project ID prop, and it indicates that we should use this to fetch data, correct? But this is still just a mock-up. We are later going to implement the file explorer, so it will make sense. and here is an interesting one. So inside of the username project it's telling us about a potential inconsistent ID usage in this optimistic update. That is because we are kind of mixing the argument project ID and arguments dot project ID. You can see how here we're using project ID but down here at least somewhere I think we're using arguments dot project id or it's just the fact that we even use this project id because again I'm going to take a look just to confirm so in my navbar when I implement rename project yeah you can see that I pass in dot id so technically Why am I passing it here then? Right? We should probably get rid of that. Because it's just, it makes no sense. We should access this through arguments. This should be arguments.id. I'm not going to do it now. I don't want to change my branch. But yes, CodeRabbit actually noticed an important mistake here. There's no reason for us to have to pass this. because when you look at it, its only purpose is optimistic mutation for which we have a solution. We have the direct arguments which are more reliable than this because yes, if you pass one thing here and call another thing here, there will be a mismatch with optimistic update. Very, very good catch by CodeRabbit. See, this is what I was talking about. This is why I think it's important to have another set of eyes take a look at the code. Amazing amazing job We going to merge this for now I like to fix these things in the second chapter rather than now so we don mess the branch And now that we have merged that let go ahead and go back to our main branch gitful origin main, so we are up to date. There we go. And as always, I like to confirm with my graph here. There we go. We checked out for chapter 8, and we merged it back to main. Amazing, amazing job. I believe that marks the end of this chapter. We created this, this, this, and this. Amazing, amazing job. In this chapter, we're going to develop the file explorer. We're going to design the file folder data model inside of the convex schema. We're going to build a recursive tree component. We're going to create the collapsible folder behavior. Here we're going to implement file and folder icons using VS Code icons JS package. And we're going to add file selection state management. Let's take a look at how that actually looks like when it's finished. So this is what we are supposed to have at the end of this chapter. Basically a file explorer that let us explore files, folders recursively. and we will also have the logic which decides what folder or file is currently open. You can see that we even have this advanced temporary open file logic so when I click on something else it overrides that tab but if I double click it kind of makes it persistent and then the next one I click goes in that other tab similarly to how the actual VS Code works and I think that's a better solution than just to open a bunch of tabs whenever you click. So let's go ahead and focus on building that. I'm going to go ahead and make sure we have our app running and make sure you have npx convex dev running as well since we are going to be updating our schema and creating some new functions here. So in our project localhost 3000 here what we finished last time is the ability to click on a project. And inside of here, we have allotment panes, which we can resize. And we've also prepared the code and preview feature. So now we're going to focus on the code feature. So make sure you have that opened here. And let's start by defining the schema. The schema will basically define how this file explorer will actually be able to work. So I'm going to go ahead and create a new files table here. Files, let's use define table and let's go ahead and give it some properties. So first things first, each file has to belong to a project. So each file needs to have a project ID with the type of ID, specifically project ID. Next, each file can technically be a folder or it can be inside of another folder. Because of that, a file may or may not have a parent ID. So it's optional and its ID is a reference to itself, another file. Each file will also have a name. And now we're going to add what I was talking about here for the parent ID. each file can either be a file or a folder technically we could separate those into two different tables but I think the complexity is not necessary at this level of project and more so they're going to have a lot of similar fields so it will mostly just be maintaining two almost identical tables because of that giving it a type which is a union which can either be a file or folder, in my opinion, makes more sense. So now we're going to add an optional field called content. Content will be optional and it's going to be a type of string. And you can see I have a comment here, text files only. Basically, if our file is text-based, like majority of the files in a code editor, we will store that inside of content. But if we attempt to add PNG, JPEG, a GIF, or anything like that, which is basically a binary file, and basically anything that is not a text file, which is a binary file, we have a couple of options to store that. We technically could store that in content by converting it to base64. But as far as I know, that really isn't optimized. And I'm pretty sure it will break some limits in convex. So because of that, we're going to use convex storage. So because of that, we're going to have storage ID, which again is optional. And it's basically going to have a reference to a storage property. So V ID underscore storage. So that's going to be used for binary files and it's optional. And the last property we're going to have is updated at. And now let's go ahead and let's add some indexes here. So the first index we're going to add is going to be by project. And it's going to be referencing to the project ID. The second one will be by parent, referencing to the parent ID. And the third one will be a combination of the two if we ever need to do that. Great. So make sure that you don't have any errors here. You should have successful indexes. You should have everything successful here. If you want to, you can do npx convex dev again, just to confirm that you don't have any errors. Great. So now that we have this done, let's go and inside of convex folder, create a new files.ts. So this will be very similar to projects.ts. In fact, you can keep them both open because we are going to kind of copy and paste some things. So how about we actually copy the get query since it will be quite similar. So I'm going to go ahead and paste this and I'm going to go ahead and I'm going to copy all the imports here. So we save some time. So make sure you have convex values, mutation query, and verify alph. So for the get one, let's call this get files. And the arguments it's going to accept will actually be project ID. V-I-D projects. Like that. Same things as the usual. We need to verify our identity. And then we need to first fetch the project and make sure to extract the arguments. So why do we need to fetch the project if we are just getting files? Well, first things first, we need to check if this project still exists because if it doesn't, we need to throw an error and not do any computation further. But there is another thing we have to check and that is the ownership. So we shouldn't just allow a user to fetch files if they know a project ID. We need to make sure that the user who is logged in actually has access to that project. And only then, let's go ahead and return await context database query files with index by project. And then inside of here, make sure to query for equals project ID arguments project ID. I'm going to try and expand this so you can see how it looks like in one line like this. And make sure to execute collect. In the previous chapter, I believe I made this mistake. so make sure you don't do it. Great, that is our first one finished. So now I'm going to go ahead and copy it and I'm going to create another one but this one will be called get file as in individual one and in here the argument will simply be an id, id of files. So let's go ahead and first things first, after we get the identity, let's attempt to get the file. And then, if there is no file, let's go ahead and throw an error, file not found. And now, once we have the file, we can actually do file.projectId to do the exact same thing as above. And then, finally, if all of the checks pass, we don't have to fetch anything new, we can just return the file. There we go. So, that's our code to get the file as an individual one. Now let's go ahead and let's build get folder contents. So again, I'm going to copy get files because it's actually quite similar. And I'm going to rename it to get folder contents. And besides accepting the project ID, we're also going to accept parent ID. And that's a type of, well, it's not required. So let's go ahead and give it optional. an ID of files. So same things. We're going to verify our identity. We're going to fetch the project. If it's not found, throw an error. If we don't have access to it, throw an error again. And then let's go ahead and just modify this again. So we're not returning early. We're just going to define files as await context.database query files with index by project and by parent get the query and immediately return query equals project id to arguments project id and then go ahead and just chain maybe this will be easier to read like this chain parent id arguments.parent id and collect them once we have the files let's go ahead and sort them so how do we want to sort them folders first then files and alphabetically within each group. At least this is the pattern I've noticed in all code editors. All of them show folders first and then files below them. So they don't actually mix folders and files. I don't know if you've noticed that. You can see that all the folders are at the top and only then files come. And same is true for individual inside of the folders. So I'm going to go ahead and develop that here. Return files.sort. We receive an argument A and argument B. First things first is we do folders before files. So if A.type is folder and B.type is file, return minus one as in the reverse order. And if A.type is file and B.type is folder, we just do the reverse. So reverse logic for a reverse return and in here within the same type so if this is not the case if all of these files are the same type we just sort alphabetically by name so return a.name locale compare b like that great and i actually kind of not sure just by looking at this i think we can actually use get folder contents without using get files Because get files seems like a primitive version of get folder contents. So I'm going to see, except if we're going to use this later in some other area. But I think we might not even need this. But still, it's okay because we copied and pasted it here. So it's mostly the same, except this has proper sorting logic for the file explorer. But let's go ahead and continue developing all the mutations that we're going to need here. So again, I'm going to copy the last one here, even though this one will be a little bit different, because this one will be called create file, and it's going to be a mutation. So it will accept a project ID. It will accept an optional parent ID. It will accept a required name. And it will accept a required content. So now let's go ahead and do what we usually do. Identity check. Check if the project doesn't exist. Check if we are the owner of that project. And then let's go ahead and check if the same file exists within the parent folder. because we cannot have the same named files, right? So that's why I'm adding this comment here. Let me show you that. If I go inside of Convex and create a new file, files.ts, you can see I have an error. So we need the same behavior here. So let's see. Can we maybe reuse this? Files, query files by project and by parent. This stays exactly the same. And collect, great. And then let's go ahead and just check if an existing file is present. So constant existing files.find file file.name is equal to arguments.name and file.type is equal to file because this method right here will be used to create file specifically not folder. That's why we don't even accept the type here. We're going to make the type be file. so if this existing one is true we're going to throw an error file already exists within this folder and at the end here we actually do not need this at all instead we can just await context database insert into files project id arguments project id name arguments name content arguments content, type, we hard code it to file, parent ID, arguments parent ID, updated at date.now, and that's it. That's how we create a file. So we just make sure that it doesn't already exist within the same project parent situation. And now let's go ahead and copy it and do the identical one for folder. We could probably create some abstraction to make this make more sense, but it's only two scenarios and I feel more confident having them separated so they don't mix or so they don't have to take care of things, right? So the only thing that's different here is folders do not accept any content, so we can remove that. Other than that, everything should be exactly the same. So we do identity, we do the project. If it doesn't exist, we throw an error. If we don't have access to it with throw an error and now we do the same thing here so check if and change this if folder within the same name already exists in the parent folder and then in the existing logic simply look for file type folder and change this to folder already exists and then inside of here we're going to modify this to not pass any content and this be a folder like that. I think everything else is the same. Now we're going to implement the rename file logic. So this one will be a little bit different and you're going to see why. So let's go ahead and let me just fix this should be a curly bracket not a square one. This too. The arguments is going to accept is just the ID of the file we are going to rename and the new name. Then let's define the handler. Let me fix this to be context. So first things first, we do the identity check as always. After we do a successful identity check, let's see if the file arguments ID exists or not. And if it doesn't, we can just go ahead and throw an error. Then let's go ahead and using that file.project id, let's get the project this file is located in. And then we do our usual check. If there is no project, throw an error. If we don't belong to that project, throw an error as well. What we have to do now is we have to check if a file with the new name already exists in the same parent folder. so what does that mean well let me try and show you somewhere i have to rename something so if i want to rename projects to schema same thing same behavior as creating a new file so that's what we're doing here we have to check if a file with the new name already exists in the parent folder so let's go ahead and let's fetch all the siblings siblings await context.database query files with index by project and by parent and I'm going to do the same thing here so it's easier to understand so we are querying both by project id using file.project id and parent id using file parent id and now we're going to check if we have an existing sibling but this time we're going to check for both file and folder type at the same time. So existing siblings.find sibling if sibling.name is equal to arguments.new name and if sibling.type is equal to file type and if sibling id is not the same as the arguments id. So the only thing that should be allowed to have a new name is the very same file. That's why this is not true. Because the point is, if this results to true, we throw an error. So the only way this will not resolve to true and still have the same name and the same type, if it's the ID is the same. Other than that, it's obviously a different file. If we detect that the name is the same, we are getting ready to throw an error. But not just yet. Because you are allowed to have the same file and the same folder. Those are two different things, right? That's why we also do a strict check on the type. And now if we have the existing, let's throw a new error. And it will basically throw a folder or a file with this name already exists. So that's the difference. And then we just have to update the file's name. So let's go ahead and do that. Update the file's name like this. whoops we don't need this one so context.database.patch files arguments id new name and updated at so yes a lot of logic for a very simple result but this is you know to create a good project a high quality one we don't want this bugs to happen we have authorization we have authentication and we even have this logic to make sure we don't have the two same named files because if we do that's an invalid file 3 structure and this project cannot run. Great. And we actually don't have to update the descendants, right? Because of the way our schema is structured. We simply refer to the parent ID. So it doesn't matter if the name gets changed. Even if you change the name of the folder with a billion things inside, the parent ID is exactly the same to all of those inner files. So because of that, this is a quite of a simple update. The one that will be a little bit different is delete file. And that's because we're going to have to recursively delete things. And let's export const rename file. Let's not forget that. And let's copy the entire thing. Let's paste it here. Let me find which is the new one. Here it is. This will be called delete file. so let's see we don't need any new name here so we can remove this we need the identity file file error we get the project we throw if project doesn't exist we throw if we don't have access to it and now we don't need to do the sibling check in fact I'm going to remove everything down here because it will be a little bit more complex than that. So what do we actually have to do? Well, we have to recursively delete file or folder and all of its descendants. So basically we have to traverse to find the most, how do I call it, the lowest file in the file tree structure and make sure that when a parent is deleted, all the things down to the last file are deleted as well. So let's go ahead and develop a constant delete recursive. Delete recursive is an asynchronous function which accepts a file ID which is basically a type of arguments.id or we can maybe ourselves say it's going to be this because that's what it is. You can import ID from generated data model. So let me scroll all the way down to delete recursive and let's go ahead and build it. First things first, we're going to check if the file actually exists. If it doesn't, we break the method. So let's go ahead and check the scenario. If it's a folder, we need to delete all children first. So I'm going to go ahead and open an if clause. And what I'm going to do is I'm first going to get all the children inside of this folder. So children, I'm going to use await context database, query all files with index by project and by parent. Again, I'm going to make this look a little bit prettier. I find it easier to understand this way. So the way we are fetching all children of a folder is by using our project parent index And we are using item which we fetched right here dot project ID and the current file ID as the parent ID because that file itself is a folder So we need to find all of its children, right? And now that we have that, we have to basically call this function again, because each of these children can be a folder itself. So basically, for each child of the children defined above, we need to call delete recursive once more. And then if that child is a folder, same thing will happen. That's how we're going to find every single child when we delete a folder. Great. And then outside of this if clause, if type is folder. Let's go ahead and do delete storage file if it exists. So that's four files which are binary, if you remember. We are going to have this scenario later in the project, but I think it makes sense to implement it now. So if we have storage ID, we should also clean up the storage, right? No need to have that populate our storage usage. So if we have item storage ID, go ahead and delete it. And then finally, delete the file or folder itself. So it's safely going to go ahead through all of the recursive things it needs to do. And then in the end, it's going to delete itself. So we are actually not calling this method right now. You can see it's just defined. So after you define it right here, await, delete recursive, and the first file we're going to pass inside will be the one we passed in the arguments here. There we go. And since we're finishing all the functions for files, I want to add one more. Even though we're not exactly going to be using this inside of the file explorer, it's going to be the one that, well, is going to be used later. And that is the general update file. Why do I say we're not going to use this right now? Well, because I remind you, we're just building the file explorer now. So we are only going to be able to rename, delete, and create. The update file is basically referring to modifying the content of the file, the actual code inside. So we're not going to see that now because we don't have the editor set up yet. But still, let's get ready. So the arguments it will accept is the ID and the new content that it's going to accept. And then as usual, let's go ahead and get the handler here. So we can actually copy a lot of things from the lead file here. We can copy from identity all the way to here. So let's go ahead and just add that here. Basically, we get the identity, we get the file, if file doesn't exist, we throw, if project doesn't exist, we throw, if we don't belong to a project, we throw. And now let's go ahead and define a variable called now to be date.now. And then we're just going to go ahead and await context.database.patch, files, arguments ID, content, arguments.content, and updated at to now. And one cool thing we're also going to do, so you see this right here, which says saved two days ago. It would be fun for this to update every time we do some modifications for the files table. Because right now, even though we modify updatedAt individually in the files, you can see here, for example, this doesn't update the project table last updatedAt. So let's start with the last one, the last function we developed, the new one, update file. At the end here, how about we add and also update the projects? So context database patch projects file project ID updated at now. And I feel like this is a cool thing that we can now add into other places too. So I'm going to copy this. I'm going to find delete file. How about we do the same thing? After you do the recursive delete, let's also update a project here. Date.now. Like this. So we kind of say, hey, some files were just deleted in this project. That probably means that when the user hovers over, we should say, yeah, we just saved those changes. I think that makes sense. So I'm going to copy this again. And let me add it to rename file as well. all of that makes sense if you ask me. If you want to use the exact one, you can do const now and then replace both of these to use the same one. So that's the rename file. We have create folder. Looks like this one doesn't have project ID, but it does have arguments.projectID. I'm just going to go ahead and make sure I have this. So now I'm going to use now here and here. Okay. So that was create folder. Now let's do create file. Const now. New, whoops, date dot now. Remove this and use the constant. And let's see what else. Create file. Get folder contents shouldn't update it. get files should not update it and get files shouldn't either. So I believe those are all the changes we need for our files functions. Let's check convex and let's run npx convex dev just to confirm we have no bugs when it comes to uploading these functions. There we go. All functions ready. So we are now ready to build the UI. So I'm going to go back inside of source, features, projects, components, and I'm going to go inside of project ID view. And inside of project ID view, we have the placeholder for an editor. So let's go ahead and actually make this have its own allotment panes. We're going to start by defining the constants, the minimum sidebar width, the maximum sidebar width, default, and default main size. and then let's go ahead and let's import allotment, our package that we used in the previous chapter and let's go ahead and prepare the layout of the editor. So inside of here, instead of rendering just an empty div with a text editor, let's render allotment. The allotment will have default sizes, default sidebar width and the default main size. The children are going to be an allotment.pane and this allotment pane will be our file explorer. And another allotment pane will be the editor view. And now let's go ahead and give this allotment pane for the file explorer snap minimum size maximum size, and preferred size. So now you should be able to see another split pane right here. In the preview, it shouldn't exist. It should only exist when the user selects they want to see the code. So basically, the file explorer is where they are going to see, well, the list of files, right? And the editor view you can see is larger, and in here is where they're going to write the actual code. And they will be able to expand this or they will be able to collapse it and they can always bring it back. So now we can go ahead and develop the file explorer. So I'm going to keep the project ID open and inside of project components, I will create a new file, file explorer. Inside of file explorer, let's add an index.tsx. And I will do export const file explorer. Return. A div. With a class name. Full height. Background color of sidebar. Scroll area. From components UI. Scroll area. A div. With a row of button. Let's give it on click. for now to just be an empty arrow function. A class name of group forward slash project cursor pointer full width text left flex items center gap 0.5 height of 22 pixels background color of accent and font bold. Let's see how we can write this. So we can write this as height 5.5. There we go. Inside of here, we're going to render a chevron write icon from Lucid React. We're going to give the chevron write icon a dynamic class name. So make sure to import the CNU tool. And I'm going to add the following. I'm also going to add a state is open, set is open here. Use state. with a default one set to false. And I think that's the only one I'm going to need right now. So then I can modify this div with a row of button to use set is open and set it to whatever is the opposite of the current value. Like this. So like a toggle. and then inside of here the default class of this chevron right icon will be size 4 shrink 0 and text muted foreground but if it is open we're going to rotate it by 90 degrees let's take a look at how this actually looks like the only thing I have to personally do is rearrange my imports because I prefer them this way great so we now have the file explorer and I've purposely created it inside of a folder because we're going to have many subcomponents\nhere. So let's go inside of project ID view here and let's replace the placeholder file explorer with the actual file explorer. Let's click save. There we go. So you should now have this and you can see how it kind of opens, right? When you click on it, the chevron rotates by 90 degrees. okay now what we have to do is we have to bring the project information from file explorer i mean from project id view to the file explorer we're going to do that using the project id so inside of here just go ahead and pass it and inside of here uh we have to well define the props that it accepts but that's quite easy so it's going to be the only prop so project ID is a type of project is a type of project ID yeah so just import this and now in order to load a project using this project ID we we have to call our use project hook so const project use project from hooks use projects and pass in project ID so just make sure you've imported hooks use project Great. And now that we have the project, we can actually go down here right after the Chevron write icon, open a paragraph and just render project question mark name or fallback to loading. And let's give this a text extra small uppercase and line clamp one. And just like that, you should now have your project's name printed out right here. great so now what we have to do is we have to add a button that will allow us to create new files or folders so i'm going to go ahead after this paragraph and i'm going to create a div now this div will have a following class name opacity zero on group hover let me go ahead and expand this so you can see it in one line. On group hover, but specifically on the project group, we are going to change the opacity to 100. Transition will be none. Duration will be none. We're going to have flex, items center, gap 0.5 and margin left of auto. In here, I'm going to render chat CN button. So I'm just going to move it here. Now let's go down to the button. the button in here is going to have an on click and the first thing we're going to do is stop propagation we're also going to prevent default set is open to true and let's just add set creating to true comment here let's go ahead and give it a variant of highlight let's go ahead and give it a size of icon extra small okay so what exactly is not working here button from components ui button okay i think we're gonna have to create both of those variants actually so let's go ahead and command click instead of button or go inside of source components ui and find a button here and what I'm going to do is I'm going to change the sizes here. So I need a size smaller than this so I'm going to add size 5.5 and I'm going to make it rounded like this. And for the variants I'm also going to create my own variant called highlight. It's going to have a transparent background. On hover it's going to use BGX in the foreground but only on 5% opacity and save the file. Close the button and go back. And you can see that now these are fully supported. So that's the power of ShadCNUI. We can now modify our button source code in real time. So inside of this button, go ahead and create file plus corner icon from Lucid React. And I think you can already see how this is going to look like. When you hover, you should see a button. Let me go ahead and zoom in a bit. When you hover, you should see a button here maybe I can snap this, there we go you should see a button which will be used to create new files and while we are here we can also copy this button so this one will be used set creating folder to true and the icon will be folder plus icon from lucid react so just make sure you've imported that and let's go ahead and copy this one one more time and then this one this one will be a little bit different so this one will just be reset collapse this one will be used to like collapse or expand again so let me show you copy minus icon from lucid react again and give it a class name size 3.5 let me fix the typo in the class name so let's see all the three icons that we have now So in here you can create new file, new folder, and you can kind of collapse the entire thing. So the easiest one to implement right away might actually be the collapse one. So we're going to go ahead and just add a new state here. Collapse key and set collapse key. And make the use state be at zero. and while we are here let's also implement creating and set creating from use state so we will only be able to create a file or a folder and by default it's going to be null so let's set it to null great now that we have this let's go ahead and check out the last button here which is basically reset collapse so what we're going to do is we're just going to increase the current or whatever it was, the previous collapse key by one and the way we're going to be using this is so that later when we implement the recursive tree, when the user clicks on this minus thing, it will kind of re-collapse the entire thing and user will have to load everything again. It serves as a kind of hard refresh of the entire thing. Not much purpose right now. If you click on it, nothing's happening. But you will see how it works later. Perhaps I've implemented it too early, so you're just confused now. But you're going to see. It's not too complicated. It's like we're purposely going to change the key property that we're going to pass down here to some elements. So that's how it's going to work. And we could also set is open to false at that point, I think. Or maybe not, actually. Oh, well, we are implementing this. I can show you exactly what we're doing. This, that's what we're doing. So yeah, it actually doesn't have to close the entire thing. It will literally just collapse all the folders that were opened. Okay, so now let's take a brief pause at developing the file explorer. And instead, let's go ahead inside of project hooks, and let's create use files. And inside of here, let's go ahead and add our, well, let's see. I'm trying to see what would be the most useful one for us to have. I think the most useful one would be to list files and to create files. So let's go ahead and do export const use create file. And we're going to do use create folder. They're going to be quite similar. So we're just going to copy and paste them. So we're going to pass project ID and parent ID. The types are going to be project ID. a type of ID projects. Parent ID is going to be optional and it's going to be a type of files. So just make sure you set the parent ID to optional because it's not required. And let's return useMutation. API. We need to import API. .files.createFile. so for now that's all we have to do in fact yeah I don't think we even need to pass the project ID and the parent ID and that reminds me we also have to fix that problem from the previous chapter that CodeRabbit told us about and I think we're doing the same mistake here you know what let's do this don't pass anything just very simply do this and then in here let's do use create folder create folder just keep it simple now let's go back instead of index here basically our file explorer let's do const handle create and in here the name will be string now above this let's define create file use create file create folder use create folder. So I've just imported from my hooks those two new hooks we've just created. Now that I have those I'm going to first set creating back to null and then I'm going to do if creating is equal to file I'm going to call create file. I'm going to pass in the project ID, the name, the content to be empty and the parent ID to be undefined. Creating. Else, create folder with project ID, name and parent ID explicitly to be undefined. Alright So now we have our handle create method right here And now in these buttons here let go ahead and make this one change set creating to be file. And this one to be set creating to be folder, like that. and now I'm going to go ahead outside of these two divs but still inside of the scroll area and I'm going to do if it's open let's go ahead and render a fragment and if we are creating something let's go ahead and do the create input here so the create input will have a type of creating so what are we creating a file or a folder it's going to have a level which is going to be 0 right now because this is kind of the root on submit which will be handle create on cancel which is just going to set creating back to null and that's the only thing we're going to do now because in order to fetch any file we first need to create it now let's go ahead inside of the file explorer new file create input.tsx okay so I'm going to go ahead and import chevron write icon from lucid react I'm going to import file icon and I'm going to import folder icon from a package we have to install react symbols so react dash symbols forward slash icons forward slash utils let's go ahead and let's import react symbols npm install react-symbols and let me actually check I think the full package name is forward slash icons so I'm going to show you what is my latest version here we should no longer have an error here there we go, that's resolved so I'm using 1.3.0. And from here I can import file icon and folder icon from react symbols, icons, utils. Like this. Let's export const, create input. Let's go ahead and define the types. So type will be either file or folder, level. is going to be a number. OnSubmit will be a function which accepts a new name. OnCancel will very simply be a void. Now we can destructure all of those above. So type level onSubmit and onCancel. Now let's start by defining the value. let's import use state from react let me move this at the top so value and set value let's develop the handle submit method inside of here we're going to trim the value so the user isn't able to pass like a blank space and if trimmed value still exists on submit trimmed value otherwise just cancel Great. Now let's go ahead and let's return a div here with a class name, full width, flex, items, center, gap one, height of 22 pixels, or if I remember correctly, 5.5, and the BG accent of 30. and in here another div with a class name flex items center and the gap 0.5 if type is equal to folder render a chevron right icon give it a class name size 4 shrink 0 and text muted foreground if type is equal to file go ahead and render file icon give it a file name of value auto assign property and the class name of size 4 and let's go ahead and do one more type folder here folder icon with a class name size 4, file name, actually folder name, value. Like this. And then below this div, an input. A native HTML1. Give it autofocus, type of text, value of value, on change, event, set value, event, target value. now let's go ahead and pass it the following class name flex1, background transparent, text small outline none, focus ring1 focus ring inset and focus ring ring if the user blurs we're going to consider that a submit and then a very simple on key down method which checks if the event.key is enter and submits and it also checks if event.key is escape and cancels. There we go. So there is actually one thing missing here, but it's the level thing, right? But I just want to make sure that you can see what we're doing before we implement that. So import the create input file. And now, if we've done this correctly, first things first, make sure you have this kind of opened, right? And click on this. and you should now be able to see an input to create a new file. But if you click this, you should see that it's kind of ready to create a new folder. And the cool thing is, if I go ahead and type, you know, test.jsx, you can see it changes the icon. If I do app.ts, it changes the icon. tsx changes the icon again. Same thing is true for folder. If I call this source, it will change the source. Test, same thing. I can't think of anyone now, but I don't know, images? There we go. Changes to an appropriate images file. So this package that does that magic is called React Symbols. And it's actually made by the same person who developed SVGL app, if you've heard about it, where you can basically find a bunch of SVG. It's made by this guy, Pablo Hernandez. I hope I pronounced it correctly. And icons crafted by Miguel. So shout out to those guys. Amazing, amazing work. And you can see 229 file icons and 93 folder icons. And all of these, you can see we just installed a package and we just used their React Symbols Icons Utils file and folder icon, which allow us to pass the file name and auto assign which icon should be used, right? So every single thing that you see here can be rendered as a type of file. So if I use Docker or maybe it's test.docker, actually, I have no idea how to do it. Docker file, there we go. It's Docker file. Even Cloud works, right? So every single thing you see here can be used in your file explorer. And I just want to tell you one more thing. in case you're having troubles with these icons. For whatever reason, you can still continue with the project, right? I mean, icons were kind of the last thing I implemented in this project. The reason I'm telling you is just in case you're having trouble, I don't know, I've had only the great things to say about this package, but you never know if something's not working for you or images are not loading for whatever reason, you can always not have this logic at all and just use the input, right? but I think it should work just fine for you. Great. So yes, I'm using React symbols, icon utils and again, I'm using version 1.3.0 if you want to use the same one as me. Perfect. So even if we actually created something right now, test.jsx, we don't really know what happened until we go to our convex dashboard and we go inside of the project. And here in the database, let's go inside of files. and here we go test.jsx and I even have a one from before called images and I believe this one is a folder it is great so we can now officially create folders and files the only thing that's kind of missing is this level thing this level thing will be used let me show you exactly how so this is the finished project you can see that if I want to create a new file here I kind of have to. Oh, this is a bad example because it moves everything for some reason. But yeah, you can see how it's indented inside, right? That's the level, the level of indentation, because the same component will be used here, but also in here. And you can see the level of indentation is different, right? So that's what we're going to do now. So in order to develop that thing, what we have to do is develop something called get item padding and it's actually super simple to implement so inside of file explorer create constants dot ts and let's define base padding and level padding so base padding for root level items and then additional padding for per nesting level so the further down we go, it's going to move by 12. And now a very simple function here. My apologies. Get item padding, which accepts a level and is file Boolean. Files need extra padding since they don have the Chevron So if it file 16 Otherwise zero And then a simple function to create the new padding What do I mean by file doesn have a chevron Well take a look You can see it just a file icon but a folder has a chevron icon so we need to offset by the width of the chevron icon which is 16 pixels basically to make these two look the same because right now there's a visible size difference in width right so that's what we're going to do all right so just make sure you've implemented get item padding let's go back instead of create input here and now what i'm going to do is i'm going to give this a style property because Tailwind is kind of tricky when it comes to computed values because of its just-in-time compiler. So I believe you cannot do things like this using Tailwind. So we're going to pass in level and type equal to file. I mean, we're going to do like a type check, right? If the type is equal to file, then the second argument is file, is going to be true. So now let's go ahead and test this out. And there we go. You can see that now they are briefly at the same width level. And there's an overall indentation here happening. Now I want to create a function or a hook more specifically that's actually going to help us see the files in our file explorer. So let's go inside of source, features, projects, hooks. And let's go inside of our use files. And now let's do export const use folder contents. And in here, we are going to accept project ID, parent ID, enabled, and hard coded to true. And now in here, let's go ahead and give it the types. Project ID is an ID type of projects. parent id is optional and id of files and enabled is an optional boolean and let's just return use query api.files.getFolderContents as the first argument if this function is enabled pass in the project id and the parent id otherwise skip the query and import use query from convex react there we go now we have useFolderContents now that we have used folder contents we can go back and set up our file explorer index.ts file and in here I'm going to define root files and my root files will be used folder contents from here and I'm going to pass in project ID and enabled only if is open so if we close no need to fetch because user isn't seeing anything anyways all right now let me also move the use project together with it here it kind of makes sense now that we have all of that let's go ahead and do a very simple loading indicator so if root files are undefined it means they're loading so let's render a loading row and let's give it a level of zero so you're going to see this behavior a lot of times this level thing is basically our level of children indentation and since this is the root level all of it is zero and then later as we defined in our constants each extra child or nesting level will increase by 12 right so that's why all of these are zero. So all of this calculation will result to zero because whatever you multiply right is zero and then plus the offset. So only the offset will actually be calculated. So loading row. Let's go inside of file explorer and create loading-row.tsx And in here we import cn and we import spinner. We import get item padding from constants let's go ahead and export loading row with class name and level both of them are optional as you can see and then in here let's go ahead and uh let's return my apologies what am i doing wrong here. Okay. Let's return a div with a class name, cn, first argument height, 5.5 flex, items center and text muted foreground, and pass in the class name if we ever want to modify it from outside. Now let's pass in the style attribute here, padding left, get item padding level and hard code this to true. So this isn't exactly a folder loading level, but we want to use the same offset as if it were a file. So I'm hard coding the is file property to true. Just so it looks good. That's the only reason. And in here we render the spinner and give it a class name size 4 text ring ml.5. There we go. And now in here, let's import the loading row. There we go. So now if you refresh here, for a brief second, when you click open, you should see loading, right? And then it's supposed to display all of the files. So that's what we're going to display now. Okay, let's go down here. So root files, okay, let me expand this more. This is the creating scenario. and now we're going to have the actual root files.map scenario. So question mark.map, get the individual item, and for each item, we're going to render something called a tree. And then each tree is going to have a key item underscore ID combined with what? Collapse key. Remember, we are using that so we easily reset the entire tree structure. So that's why we're doing that. Great. Let's give it a item, a level. So this one, by default, will be zero. This is the root one. And you're going to see when these levels will start to increase in a moment. And we pass in the project ID. There we go. Now let's go ahead and let's develop the tree component. So let me go ahead and close everything actually. Go inside of features, projects, components, file explorer. And let's do 3.tsx. And in here, again, let's import chevron write icon, file icon, and folder icon from react symbols icon utils. let's go ahead and import cn from lib utils and let's go ahead and import use create file use create folder use folder contents from features actually I think can we just do yeah we could just do hooks use files like this or if you prefer, you can use forward slash features, projects, hooks like that. Either will work. And now let's go ahead and also import get item padding from constants, import loading row, import create input from create input, and import document from data model from convex id as well and let's start building the tree so the tree will be a very powerful component it will be able to do everything that our index file explorer can but at an indented nested level infinitely right recursively should i say Maybe that's a better word for it. So let's first define the types. It will accept an ID. My apologies. An item, which is a type of file. Level, which is an optional number. And the project ID. Let's hard code the level to zero when we extract it. Now in here, we're going to start the same. Is open. Set is open. Use state. False. make sure to import useState from React. I'm just going to move it to the top here. Okay. Now that we have that, let's go ahead and add isRenaming. Set isRenaming. UseState. False. And let's do creating. SetCreating. UseState. And by default, null. and now we simply go ahead and we give it those three possible types. File, folder, or null. Great. So now, let's actually go ahead and pause and let's go back instead of hooks, use files. And I want to add use rename file. Rename file. And I want to add use delete file. Delete file. And as far as I know, those are the only ones we need, I think. We have create folder, we have create file, we have rename file, we have delete file. I think that should be okay. So now back inside of the tree here, let's import use rename file and use delete file from hooks. And now let's prepare them right here because we are going to kind of use all of them here. So const rename file. Use rename file. Let me fix the typo Then this one will be delete file So use delete file Create file Use create file. This one will be create folder. Use create folder. All right. Perfect. Then let's do const folder contents. Use folder contents. Pass in the project ID. Parent ID to be item underscore ID enabled to be item dot type is equal to folder. And only in that scenario, we check for is open. Great. Now let's go ahead and, well, let's check. So actually, yeah, here's the thing. we can start rendering things but we are missing a wrapper component so i'm trying to think of how do i code this so you don't because we've coded a lot right now but we aren't seeing anything and i don't like that since you know you're just getting confused like what are you coding here so let me try and do something how about we do this if item dot type is equal to file let's just return a div which says I am a file and then down here let's just return a div I am a folder then let's go back inside of a file explorer index and let's import tree from dot forward slash tree and you should now see i am a folder and i am a file if i click on a new file i should see a new one if i click on a new folder i should see a new folder right but in order to display them properly we have to develop something called a tree item wrapper so inside of the components right here. Instead of file explorer, let's create tree-item-wrapper.tsx. Let's go ahead and add all the imports. So cn from lib utils. And then let's add all the components from context menu. So the menu, menu item, menu content, trigger, shortcuts, and separator. Then let's also import the constants, meaning get item padding and document from convex generated data model. And now let's export const tree item wrapper. Now this is a component that's going to have a lot of props. So let's go ahead and extract all of them. Item children level is active on click, on double click, on rename, on delete, on create file, and on create folder. And inside of its props, we're going to define each of them. Item is a type of document file. Children are react.react node. Level is a required number. Is active is an optional Boolean. On click is an optional function, as well as all the other functions. So all of them are optional. Great. now in here let's go ahead and let's return a context menu and let's add context menu trigger let's give it an as child property inside of here let's render a button and this button will simply render the children the button will have an on click of on click on double click on double click so same named props on key down event if event dot key is equal to enter we're going to prevent default and attempt to call on rename and let's give it a class name which is going to be dynamic so the classes that it's going to have is actually very similar to the create input one group flex items center gap one full width height of 22 pixels or 5.5 hover bg accent with 30% opacity outline none and then all the focus ones and if is active bg dash accent with forward slash 30. And then a style padding left get item padding level item dot type is equal to file. This will return true or false and give our is file boolean that value. Great. So that's for the context menu trigger. Now let's go inside of the context menu content. On close auto focus here should simply do a prevent default. Class name should be w64. And then inside of here, let's check. If item.type is equal to folder, we should have a fragment rendered. And then we should have some items inside. The first item will be a context menu item, which has a prop on click and class name text small with a label new file. Then let's copy this, paste it, change this to new folder, on create folder, new folder, and then below that, a context menu separator. Otherwise, outside of this right here, still inside of the context menu content, render another context menu item with a prop on click on rename, class name text small, a rename label, a context menu shortcut element next to its label, which simply says enter. And then last item, we can actually duplicate this one if it's easier that way. this one should say delete permanently and it should have an on click on delete and its context menu shortcut will be the command icon and backspace we're going to implement the shortcuts later for now let's just have them visually some of you have already guessed what this is we've created this three item wrapper so that every time we right click on something here, we have these options. Regardless how deep they are, we're always going to have either rename or delete. So let's go ahead and continue developing now because the tree item is now finished. So we can now go back to tree.tsx. So I'm going to go ahead in here, item type file and instead of rendering a div we're going to render tree item wrapper so the tree item wrapper can now be imported from here and let's go ahead and give it some props item will be item level will be level is active will be is active uh looks like is active is we're not currently okay for now hard-coded to false on click for now shouldn't do anything we just have to remember to add it later same thing with on double click on rename should simply trigger the set is renaming to true on delete should let's add a comment close tab and then delete file and passing the id to be item underscore id what does close tab mean? basically later when we have the logic to have open and close tabs when we delete a file we should also close if it's opened in the code editor right and what we render inside well either a file icon with file name let me just see so this file name let's do const filename item.name the reason I'm defining it in a constant is because later when we add the rename functionality we're going to pass it through this constant as well let's go ahead and give this auto assign and class name size 4 and a span here filename and a class name truncate and text small. Let me fix this. Truncate. There we go. So now your files should display properly. Perfect. Now let's go ahead and do the same for folders. So I'm going to go ahead down here and I'm going to define folder name to be item.name and then let's go ahead and define const folder content open a fragment open a div give this a class name flex items center and gap 0.5 open chevron right icon give it a class name cn size size-4 shrink-0 text-muted-foreground is open rotate 90 degrees then render a folder icon from React symbols give it a folder name folder name like this give it a class name size-4 and outside of the div render a span rendering the folder name and give this a class name truncate text small and then let's go ahead down here replace this with tree item wrapper\ngo ahead and render this within a fragment. Inside of tree item wrapper, render folder content. And in here, give it an item of item. Give it a level of level. And then give it on click to be just an empty arrow function. Same thing for on double click. On rename. let's give it set is renaming to true on delete a comment to close the tab and then delete file id item underscore id and I'm just going to change this to be to do close tab and I'm going to change that here as well so I remember to search for that later so I don't forget And in here, let's also add onCreateFile, startCreating, set it to file, and onCreateFolder, startCreating, and set it to folder. So we don't have startCreating. We can very easily implement that up here. const start creating accepts either a file or a folder set is open to true and set creating to the type past. So we don't have to do the same thing twice here. Great. And now here's the thing. We're not done yet when it comes to folder because folder can be opened. And in that case, if the folder is opened, Guess what? We have to render the entire thing again. So let's go ahead and do if folder contents are undefined, render the loading row with level, level plus one. So we are finally inventing now. And if folder contents question mark map, get the individual sub item here. And guess what? We rendered the tree itself. Give it a key of subitem underscore ID, item of subitem, and a level of level plus one. And finally, the project ID. There we go. So now, we should have a very basic file explorer working. Let's try and see that. so I'm going to collapse this and open this and there we go so now when I create a new file and call it I don't know image.tsx it's right here when I create a new folder and I call it source there we go it's right here the only thing that I don't see happening is that when I click on it it should expand so let's go ahead and just see uh how do we do that oh okay so down down here tree item wrapper make sure it's the one which actually is the folder one right because we have two instances of tree item wrappers one is here inside of an if clause if item type is file not that one down here and find it's on click and simply do set is open and then whatever is the opposite of the current value and you can actually remove on double click nothing will happen if you double click on the folder you can of course modify that for yourself but there we go since we don't have any nested ones oh yeah and if you write yeah i forgot to show you that if you right click, you can now, well, you can see the delete works, yes, but none of the other ones are actually working. So yeah, you can try out the delete. It should be working just fine. The only thing you cannot test is kind of nested files. You can see I managed to delete all of my files, so that's working fine. Right. But the one we should test out and the one I hope to, I hope to add a few more features and that is to rename a file and to create a new file and folder inside. Because I think that shouldn't be too complicated simply because we can just copy and paste the create input. In fact, for creating itself, we already have everything we need. We just have to find the folder content here. So right after we define it, before we return this, let's check if is creating and do the following. Return a fragment, open a button here, a native HTML button element, not chatCN1. let's give it an on click set is open and then just do whatever is the opposite of its current value that's the first one then let's go ahead and give it a class name its class name will have group flex items center gap one height of 5.5 hover bg accent with 30% opacity, cursor pointer, actually we don't need cursor pointer since this is already a button, and full width. Then let's go ahead and give it a style, padding left, get item padding, level, and false since we know this is a folder. And inside, render the folder content. and then is open should go ahead and render a fragment and then render folder contents check if it's undefined and render the loading row give it level level plus one prop be very careful here so not folder content folder contents right you should probably go through the code and confirm you aren't doing that anywhere or maybe we can rename this to something better folder contents i don't know folder content content files for now i'm just going to leave it to be this but make sure you aren't mistaking this variable right here all right so folder content is rendered inside of the button but folder contents are all the files which belong to that folder. And then in here, let's render the create input, which we already have imported. Give it a type of creating. Give it a level of level plus one. Give it on submit to be handle create. I'm not sure we have that. We will implement it. And let's have set creating to null here. We're not done yet. We also have to check if folder contents has its children open. If it does, we have to render them using a tree. And then let's simply pass all the props the tree expects. Key, item, level, which is level plus one, and project ID. So the only one we don't have is the handle create method. So let's go up here. and let's add it. Const handle create. We'll accept a name, which is a string. We're going to set creating to null. If we are creating a file, we're going to call create file with project ID, name, empty content, and parent ID of item.underscore ID. Else, we are going to create a folder with project ID, name, and parent ID. Great. We shouldn't have any errors. And now, if you make sure you have a folder, right-click on it, and new file. And you should be able to create inner file. There we go. You should also be able to do inner folder. Just like that. And guess what? You should also be able to do inner file. And you should also be able to do inner folder. And I think you get the point. I know this was a lot of work, but you can see that now we can create infinite nested files and folders. We can also delete them. And here's the thing. sign I have one, two, three, let me see, one, two, three, four, five items here. If I delete app, I should have just one left. There we go. So our delete method is working correctly because all the nested files and folders were deleted as well. Very, very good. One more thing left to do since we are that close to kind of wrapping up this chapter and it kind of makes sense to do this in this chapter and that is the rename functionality and i just can't get over how we are calling this thing folder content let's call it folder render because it's so close to being called folder contents which is something completely different and i have a feeling this will just confuse people So let's rename this to folder render. And let's see all the places where that is supposed to be rendered. Inside of if creating as the child of this button, we should use folder render. Down here in the main return, tree wrapper should use folder render. Everywhere else, we are using folder contents. So you can see I have seven instances. Okay, well, not all of them, but five actually constant names folder contents. But yeah, technically seven instances if we also count the name of the hook. So make sure you have the same numbers and that you didn accidentally use folder render somewhere else All right one more thing to do We can do it Let start by implementing the function handle rename the same way we have this handle create So handle rename accepts a new name Set is renaming to false. If new name is equal to item dot name, we return. And let's go ahead and simply call from this rename file. Great. That's the handle rename. And now let's go ahead and we have to do it in two places now. And we actually have to create it first. So let's go ahead and do the following. We're going to go inside of source, features, projects, components, file explorer, copy the create input, and rename it to rename input. Like so. Inside of the rename input, make sure to immediately change this. And let's see the props. So most of this will be the same, but we're also going to have default value and is open. So let's make sure to add those two. Besides that, on cancel. Yeah, we should also have on cancel. So yeah, let's leave it as is. Value, set value will be the same. Trim the value. yeah I want I want the exact same logic as in my create input I think that kind of makes sense right so let's see with full flex item center gap one height 5.5 which is 22 pixels bg accent padding left all of this looks perfectly fine chevron right icon the only thing we should do here. So we have is open here. So perhaps what we should do, if type is folder, how about we go ahead and wrap this inside of CN. So make sure you import CN. And if is open, rotate by 90 degrees. So even, okay, just confirm you have this. So even in the rename input, if the folder is opened let's rotate the chevron right icon let's be consistent right because i think currently that's a bug right if i open a new folder and call it app uh actually i can't test it now yeah okay maybe it's not a bug yeah because basically i was thinking should this chevron rotate it shouldn't because we never open a non-created folder so this is actually perfectly fine. We should only have is open here in the rename input. Okay. Now, if it's type file, it's super simple. No changes here. And if it's type folder, same thing here. I think that folder icon here doesn't accept is open, so no need for that. Okay. And now for the input, I think most of it is exactly the same. Out of focus, type is text, So value is value on change. Flex one, BG transparent. Text small, outline none. Focus is exactly the same. On blur, handle submit, on key down. All right. The only thing I think I want to change is the following. I want to do on focus. if type is equal to folder let's go ahead and simply select the entire thing but else if it's file let's go ahead and do the following let's get the value let's get last dot index value last index of dot if last.index is larger than zero, let's go ahead and set selection range to start from zero and go to the last.index. Else, select the entire thing. You're probably wondering, what does that mean? You're going to see in a second, it's a small implementation which kind of improves the quality of life. so okay we have on blur on key down we have on cancel so i think all of this should be fine okay let's go ahead and render it so i'm gonna go inside a file explorer tree right here and the first place we should render this is down here if item type is file and now if is renaming we have isRenaming stored right here so if isRenaming we're going to return renameInput so make sure you import that because this is the first time we're adding it the renameInput should have a type of file because we know it's a type of file, a default value of file name a level of level and on submit handle rename and we are missing on cancel here which should very simply set is renaming back to false so let me just check how does handle rename work if new name is item.name and instead of the rename input here okay and i mean if it's new name should it also close i don't know we'll see so now make sure you have a file right click rename okay some kind of works but not really let's try this again right click rename okay so it offers me the rename does it work let's see something new.tsx It seems to work, but not as I want it to. So it kind of completely resets the current state. Why is that happening? Let's take a look. So if is renaming here. Oh, we are missing something. We have the default value, which is file name. Let's go inside of rename input here. We are never using the rename input's default value. So the default value should first of all be here. That's the first thing. And then in here, for the trimmed value, let's do either trimmed value or fall back to the default value. And then in that case, we can just call on submit like that. Let's see if that improves it. So right click rename. There we go. Do you see what this focus thing did? basically it made it behave the same way as it does in a real code editor. When you focus on a file, it's not going to highlight the entire thing. It's going to carefully highlight only until the extension. So you can safely rename it without changing the extension of the file, which is actually exactly how it behaves in this. Take a look. If I want to change this, if I right click and rename, you can see it only highlights up to the extension. So I kind of wanted it to behave the same way. I want this to be a cool project, right? So I'm focusing on those details. Great. So we have that inside of the rename input. I think this behaves quite well. I'm not sure if we have to change anything. Let's see, what if I rename and escape? So it will just kind of cancel, right? it won't do anything. I think that's fine. I think that's perfectly fine behavior. And one more place where we have to do this now is down here. So let's just copy the if clause, if creating. And let's go ahead and now do if is renaming, like that. And then, instead of a button here, we will have a rename input. and the rename input will have a type of folder. Default value folder name is open is open. Level level on submit handle rename and on cancel set is renaming to false. There we go. And let's see. Type null is not assignable. how did that happen it's not the signable type of folder that makes sense, yes I mean I think all of this is still working just fine how about we go inside of the create input and we allow it to be null I don't think it's causing any problem I think it works fine I'm not sure if maybe it ruins something else here nope let me see, I think we can now rename folders yes, we can, so if I change this to tests there we go if I change this to something else that works too one thing I don't like is the flash of a previous one we can fix this with optimistic mutation but since this chapter is already an hour and 40 minutes long I'm going to pause here I think this will be the last thing we're going to implement so basically you can see in tree.tsx we successfully used every single thing we no longer have any warnings about unused files or folders here I guess if there was one thing I would have to do before we end I just want to make sure I kind of abruptly ended the isrenaming here. I'm not sure I correctly checked if that's really all we have to do, but I think it is, yeah. We have to render the rename input. And then the rest is just the same. Folder contents loading row, level plus one. We, oh, create input. Oh, this is where it was throwing me the error. Okay, in isrenaming, we do not need the create input. Okay, we just need, to check for folder contents. No need for the rename input And I think that now instead of create input I can remove this Because we will never render the create input if we don explicitly give it a type I think Okay, I just lied because we do that here very clearly. But you can see that here, it is already kind of solved that it's not going to be null. because we check that right here okay i understand now uh okay i guess there's only like one possible erase condition if you at the same time create and rename but uh i'm not sure how that would happen so what if you do this and then right click yeah you see it's not possible because that resets it so yeah it can only be creating or renaming not both at the same time great so quite powerful file explorer from this session. We can do so many things inside of here. Let's see, can we rename? We can, amazing, amazing. I'm going to test this thoroughly before the next chapter if I can detect any bugs, but I think we did a very good job here and you can test it too, you know? All right, one thing I wanna do before we wrap up is just fix the bugs from the previous chapter. so the first thing is the invalid project id label in our layout.tsx so if we go inside of source app folder projects project id layout here it is let's change this to be a type of id from convex projects okay that's one fixed now let's go down here okay we don't have to fix this one This is just telling us that it's an incomplete implementation, which it is. But in here, we do have something that's wrong. And it's the optimistic mutation. So let's go inside of our features, projects, hooks, use projects. And let's find this, use rename project. There is no reason this hook should be accepting the project ID. Why? Because we have it in the arguments. we can just do arguments.id you can see we even have autocomplete so we definitely know that this is the one and i think that now works perfectly fine just in case we can check let's go back here if i create a new one immediately created working just fine awesome so that all seems to be working let's close this and let's go ahead and merge our changes right so i'm going to shut down all of my terminals now this is chapter nine so git add and then a dot git commit nine file explorer. Perfect. Git checkout dash b 09 file explorer. Git push u origin 09 file explorer. Perfect. Once this is pushed, you will see in your IDE the file explorer. And now let's go inside of our repository and let's open a new pull request. This was a lot of new files, 17, so definitely a good idea to get it checked by CodeRabbit. And here is the summary. New features. We introduced a file explorer with collapsible tree view for managing project files and folders. We can create files and folders with inline editing. We can rename and delete files or folders via context menu options. We've added split pane layout with the file explorer sidebar and the editor pane. Folder contents has sorted with folders first and then files alphabetically. Project timestamps automatically updates when file changes. And now in here we have some comments. First things first, it commented on my usage of convexes context.database.patch. So as I told you in the past convex actually didn't allow passing the table name right they just accepted the id right this didn't exist like a few weeks ago so that's why ai is still outdated on that usually you would just pass in the arguments id so this is actually an update a very recent one. So I gave it information about that, that the Convexes API has updated, and you can see that it read the documentation, and it updated its learnings. So very cool to see how even these production grade applications are using something like Firecrawl to update their learnings, which is exactly what we implemented a few chapters ago, right? So we keep seeing how useful tools like Firecrawl are. This is another comment on that same thing, so I made sure it knows that going forward, and now it will not correct us anymore. Perfect. And in here, yes, there is kind of a problem here. It's what I told you in the beginning. We could technically create a separate files, folders, and binary table, but I just found it to be, at least for a tutorial, very simple to keep it all together. Later, if you want to, you can keep all three separated because yes, there is technically a scenario here where we could have a storage ID, content, and a type of folder, which is something that shouldn't exist. A folder cannot have content and it cannot have storage ID. A file cannot have both content and storage ID. Basically, it's telling us that we are not enforcing this at all. So right now, the only thing that's preventing this from not breaking is the fact that we know what's supposed to exist and what isn't. But yeah, we could either consider making strict checks in our create file and create folder so that storage can under no circumstances be added, or we could look into some schema level enforcement in convex. But right now, I think we are completely okay going this way further. But I'm still very happy that CodeRabbit commented on this because yes, there is kind of, you know, some odd behavior here. I mean, it's not odd behavior. It's the fact that nothing is preventing us from creating a state which shouldn't exist. That's what CodeRabbit is commenting on. So good comment here. Yes. In here, it's commenting about the fact that we modified our layout.tsx project ID type from string to ID layouts. In fact, it's not telling us that we shouldn't use ID projects. It knows it should be ID projects. But it is warning us that next.js about parameters are always a type of string. We are only using this so we get rid of the type errors. So here is the suggestion it gave me. Keep them as a string and then cast it as an ID. The reason this might be a better solution is because you never know, but Next.js version might update, whereas if you define the wrong type in its params, because these are Next.js specific things, it might break the build. So I might actually lean towards doing this change because for long term, that made more sense. You can see it completely understands what we're doing. So this is a branded convex type that requires validation, right? But it's suggesting casting that rather than changing what is not true. Yes, next.js project ID is 100% a string. That's simply how the framework works. It's never going to be this. But technically, in our case, we know it is. So we're kind of using a hacky solution to fix our types. So perhaps we should do this in a proper way. We can even add runtime validation if we really want to, right? But convex usually takes care of that. And in here, you can see it's telling us to double check if size 5.5 actually exists. And I've told it that it does. but you can see that even I if you've probably noticed I didn't use size 5.5 in my code I used size 22 pixels and then Tailwind extension CSS told me hey you can use size 5.5 so it's obviously a very new thing that just came out and I couldn't even find public documentation about it but you can see I just set that to CodeRabbit and it learned the same way I learned this chapter the extension taught me and I just taught my AI reviewer here and you can see that it now knows that and it's not going to mess with that anymore. In here, it suggests not using both default sizes and preferred size for the allotment paints. That's probably true, yeah. I'm gonna take a look if we can remove one or another or if it causes any problems. I think as long as it doesn't cause any errors or problems, we're good to go. Awesome, amazing, amazing check by CodeRabbit here. So we're going to merge this and now we should have this branch visible right here. Here it is. Let's go ahead and do git checkout main, git pool origin main. There we go. I think that marks the end of the chapter. Let's make sure we are on the main branch right here. and inside of here I always like to confirm I've checked out number nine and merged it back to main. Amazing! So we've designed the file folder data model in context, we built a recursive tree component, we've created collapsive folder behavior, we implemented file icons and we did not implement this but we did implement create, rename and the context menu. So we actually did way more and then in the next chapter, we're just going to have to implement this, which is way simpler than what we just did. Amazing, amazing job, and see you in the next chapter. In this chapter, we're going to implement the code editor, as well as state management of our files. We're going to do that by setting up Sustand state management installing and configuring CodeMirror 6 setting up one dark theme configuration building a tabbed file switcher component and adding syntax highlighting for languages So this is how that's going to look like. This is the finished project. In the previous chapter, we actually implemented the file explorer, but what we didn't implement is the logic of what file is selected. And we didn't implement the ability to see the contents of that file instead of a code editor, which has the proper syntax highlighting for its file. So the first thing we're going to do is we're going to implement the state management. As you can see, it has a very specific temporary state, right? So right now, when I switch between different files, you can see that it's temporary. Unless I double click, then it becomes permanent. And when I click on the next one, that one becomes temporary instead of just opening a bunch of tabs. I found this behavior from the actual VS Code. You can see that right now I have this file opened, create input, but when I click on another one, it doesn't open a new tab. It just replaces the existing one until I double click and then you can see it starts opening multiple tabs. So I borrowed the behavior from the original editor because I think it's a very good implementation. So I want to bring it to our editor as well. So without further ado, let's get started on that. So I'm going to go ahead and do npm run dev and we're going to start by installing Zustand. So let's go ahead and do npm install zu stand. I'm going to show you the version that I'm using immediately, even though very rarely have I heard of any breaking changes inside of zu stand. State management is kind of solved, you know. So let's go ahead and go inside of the following. I want to kind of prepare source features projects components file explorer tree so this specific file the reason is because this file is almost finished besides a few things and those few things are let me find it here they are on click is empty on double click is also empty to do close tab right we have kind of this leftover things that we just left here, right? So that's what we're going to be able to wrap up and forever finish this tree component. We first have to implement the state management. So let's go ahead and do that. I'm going to go inside of source, features, projects, and let's create a new folder called store and inside use editor-store.ts. Let's import create from Zustand. Let's import the ID from convex generated data model. Now let's create an interface called tab state. It's going to have an array of open tabs which are just a bunch of ID files and active tab ID and preview tab ID, which are the same type, but optional. Now let's go ahead and according to that interface, create a default tab state and give it the proper interface, empty array and null null as the initial values. Now let's go ahead and let's open interface editor store. Inside of here, I want to start with tabs. So tabs are going to be a map and we're going to give it a type first arguments ID of projects and the second one tab state. Those are going to be the tabs. Then let's go ahead and let's open get let's create get tab state which accepts the project ID which is this type and simply returns tab state. Then let's go ahead and create open file. Open file is a function which accepts project ID, file ID, options in which we have one property pinned. That is the functionality I was talking to you about, right? Will it persist or will it replace on next click? So we're just writing types for the editor store now. After open file, let's implement close tab, which accepts a project ID, a file ID, and a void. Then let's go ahead and implement close all tabs, which accepts project ID and the void. So this will be per project, right? And the last one we have is set active tab which accepts a project id and a file id and returns a void let me expand this even more so perhaps you can see all of them in one line great so now i'm going to remove the spaces between them i just added them so it's easier for you to look at now that we have the interface for the editor store let's actually go ahead and develop export const use editor store we're going to use the create method from Zustand and let's give it an argument of editor store the interface we've just created go ahead and execute it and then immediately execute again and from here open another parenthesis so be mindful a lot of parenthesis happening here and again open parenthesis and immediately return an object inside like this and now in here let's start defining things. So we're now going to have a bunch of errors until we populate all of these functions and types we've added. So let's start with the tabs. That's going to be a new map. Then let's go ahead and let's implement get tab state, the second one. Get tab state will accept a project ID and it will return get, which is this method right here, dot tabs dot get, which is using get from the map JavaScript, what should I use, entity, I guess that's the word, or fallbacks to default tab state. We are using map because it doesn't allow duplicates, so it kind of does a lot of work for us in making sure no duplicate tabs are open. Great, so that is get tab state. Now let's implement a complex one. Open file. It will accept a project ID, file ID, and last but not least, the options in which we have the pinned property. So let's go ahead and start defining the things we need. First, let's get all the tabs using new map get tabs. Make sure to execute the get. Then let's get the current state using tabs.get project ID. So we are getting all the currently open the tabs for this project ID or we are defaulting to, well, default tab state. From the state, we can extract open tabs and preview tab ID. And now we can check if this current file is already open. Do open tabs include the file ID we're attempting to open? Now let's have some scenarios. So first scenario, opening as preview. So that is this. When I click on something, you can see how it opens. it's not persistent because if I click something else it changes right so we call that preview not pinned right that is case one so let's go ahead and do an if check if it is not opened and we are not pinning it so if not open and not pinned let's go ahead and reinitialize new tabs with this new preview tab. So preview tab ID, which we've destructured from the state. Let's check if we have it. If we do, call openTabs.map, find the ID, open parenthesis and check if ID is equal to preview tab ID, then use the file ID we just passed here. Otherwise, just return the ID you found. And the alternative is to simply add to the list of open tabs a new file ID. All right. Now, here's the thing. It's very annoying to look at these errors here. So I'm thinking if I remove this, does it make it easier to look at? Oh, kind of, but not too much. Unfortunately, we're going to have to go through this with a bunch of errors. And then hopefully we won't have any at the end. It's just hard to define, like, did we make a mistake in the syntax or is it just a type error? But let's just continue for now. All right. So we've just successfully defined new tabs here. The only problem is we are not using it. So let's call tabs.set project ID, open tabs, new tabs, active tab ID, file ID, preview tab ID, file ID. So an active tab is the one the user is currently seeing in the code editor. The preview tab ID is referring to the top bar here. So a file can be both active, but still in the preview phase. For example, like this. It's active, but it's in preview, right? You can see it's italic. So that's the first scenario. And let's call set tabs and do an early return. So that is case one finished. Make sure you've closed the if clause. Now let's do a second scenario, which is much simpler. case two opening a tab immediately as pinned in that case all we have to do is just open a new tab so if the tab is not open but we called it with a purpose to be pinned which could be initiated through a double click for example let's simply call tabs.set for this project id spread the state, call open tabs, spread open tabs, and simply add file ID here.\nSet the file tab ID, my apologies, the active tab ID to the file ID. Then call set, put in the new tabs and do an early return. I think that maybe we can just every now and then remove the editor store. Just to confirm, you know, you don't have any big syntax errors. Okay, now that I look at it, it's definitely easier to look at without giving it the type. like okay this we're gonna see if this actually works later or not you should have all of these errors all this is perfectly fine it's mostly just type errors right but it's way easier to look at especially when building long ones like this so that was a case two now let's go ahead and do case three which isn't going to be complicated case three the file is already open but we attempted to open it again and pin if double clicked. So const should we pin it if we specifically gave it an option to pin or if preview tab id is equal to file id. So if we attempt to open a previewed file once more obviously we want it to be pinned so tabs dot set project id state active tab id set to file id preview tab id let's go ahead and do should we pin it if we should no otherwise keep it as preview tab ID and go ahead and set tabs. No need to do an early return because this is the last if clause. Keep in mind if this is like super complicated that's kind of okay it's very hard to imagine how this actually works but once we connect it to the UI it will be easier. You can see that even I myself made these comments so it's easier to explain what we're doing here. So perfectly fine if you feel confused here. It's a big chunk of logic and it will make more sense once we have the UI to go with it. So yes, don't worry. Let's go ahead and continue developing our methods here. So I'm going to add close tab method now which will accept a project ID and a file, a file ID, right? So again, let's start. First, we get the tabs using new map, get dot tabs. From that, let's go ahead and get the state from the current project ID or fallback to the default state. We already know that from the state, we can extract open tabs, active tab ID and preview tab ID. And now let's just wrap it up by adding a tab index from the open tabs. So we are attempting to find if the file ID is one of the open tabs by using index off. And we are going to use it later. So the first thing we can check, if the tab index is minus one, it means it's already closed, we have nothing to close. So let's just do an early return. Now let's go ahead and prepare how the new tabs are going to look like. So the new tabs will be openTabs.filter and basically just filter out the file ID that we have passed. So make sure you use the opposite logic here, right? Now let's go ahead and let's define the following. New active tab ID. That's basically the complicated part here. Once we close a tab, what should become the next active tab? We could just fall back to a blank page, but why should we? We can implement a very simple kind of decision of what should be the next tab. So if an active tab ID is equal to the file we have just closed, we have to do something. So first things, let's check. Are the new tabs, meaning once we remove this file, are there any tabs left over? because if there aren't, there's nothing we can do. Let's just go ahead and pass a new active tab ID to null. Alternatively, if we are able to find some other tabs, so else if tab index is greater or equal than the new tabs.length, we are basically doing the logic. Should we make the next active tab, the one that was opened before this one we just closed or after it. So for this scenario, let's set the new active tab to be new tabs, new tabs dot length minus one, basically the last one we opened. Else, let's just go ahead and fall back to new active tab ID and then to whatever tab index we just found. Why can we do that? Well, because this tab index is no longer this closed tab. So it's just the last one left, right? Once we do that, we can call tabs.set project ID, open tabs, new tabs, active tab ID, new active tab ID, preview tab ID, preview tab ID equals to what we passed, file ID, null, otherwise preview tab ID. And finally, let's set those tabs. There we go. And now we have a few very simple methods. So the first one is to close all tabs in a project. We pass in the project ID. We call the tabs using new map. And inside, we execute get and we access the tabs property. And we call tabs.set project ID. Simply default it to an empty array with no open tabs or anything like that. and call a setter on that. And last one, set active tab for a project for this file. As always, let's go ahead and copy how we get the tabs like this. const state tabs.get for this project or default tab state if this project has no open tabs. Call tabs.set and add to this project ID whatever is the current state and simply modify the current active tab ID to be file ID and set tabs. That's it. We are now ready to give this a proper type. So go to use editor store and give it the editor store. And if you've done it correctly, you shouldn't have any errors at all. So if you do have some errors, it's most likely a typo, right? Because for example, in things like this, if I do active tab ID 2, you can see it gives me an error. But if I don't have editor store here, I don't think it's going to give me an error. Yeah, you can see how it's completely allowing me to do that. So if you have some typo here, these adding editor store will help you do that. So yeah, you should definitely have no errors once you add the editor store. The editor store will ensure that you have complete type safety here. I'm slowly going to go through the entire file once again if you want to pause and if you want to check and compare. Of course you can access the source code too but still if you want to do it this way I'm just going over everything once again so you can pause the screen and see. Great. Now let's go inside of the hooks and let's create useEditor.ts. And inside of useEditor, let's go ahead and export const useEditor. Project ID. A type of ID. Projects. And let's go ahead and add some other imports. We are going to need useCallback from React. and we are going to need use editor store from store use editor store. Now I'm going to define the store here store from use editor store. Then I'm going to grab the tab state for my specific project ID. So tab state use editor store we get the entire state and then we call get tab state but just for this one project that I currently have opened, right? This is the logic. Once I have the tab state, I can start developing things like open file. We're going to call use callback. So we don't have any reactivity problems here. So what are the arguments we're going to pass? Well, the first argument is going to be the file ID. and the second argument will be options. Should we pin this file or not? And then the logic in here is actually very easy. Store, open file, project ID, file ID and options. And the dependency array should have store and project ID. So because we did all the complex logic inside of the actual store, we don't have to do it here. In here, we just make sure that we can easily call this hook, use editor and pass in the project ID. And we don't have to worry about passing project ID in every single store function. Because right now, take a look at all of these functions. They all accept project ID. That's very annoying, right? So we are creating an abstraction over that so that we kind of have easier and better developer experience when calling these functions. Now, same thing is true for close tab, for example. Close tab use callback which accepts a file ID and simply calls store close tab passes in the project ID and the file ID So the exact same thing is here right I try to write it in the same way but I think you get the idea right This first one is just the parameters of the function, right? Okay. So that was close tab. Now let's go ahead and let's do close all tabs. Again, use callback. This time, no props at all. just store close all tabs for this project and then let's go ahead and do the last one set active tab again use callback accepts a file id calls store set active tab project id file id and these two in the dependency array now let's go ahead and let's return all the things we're going to need. So we are going to map tab state dot open tabs to just open tabs. Same thing with active tab ID and preview tab ID. And then we have open file, close tab, close all tabs, and finally set active tab. Great. So a very useful abstraction over our Sushdan store. So what I want to do now is go inside of features and actually create a new folder called editor. And I want to create store here and I want to create hooks here because what we've just done doesn't belong in the projects right so I'm going to move use editor to our editor feature hooks I'm going to move it there I'm going to update the imports even though it doesn't matter because I'm now going to move use editor store to our features editor store. I'm going to move it there and I'm going to update the imports. So let's see. Inside of our projects feature, we shouldn't have a store folder at all. We can delete it. The hooks should only have these two. You can now close the projects folder. But your new editor feature should now have use editor, which calls use editor store from dot dot slash store use editor store and you should find use editor store right here and maybe the only thing that you have to fix is this import if you even have to i don't think you do i think it's exactly the same level as we had it in projects great so that's one thing i wanted us to do so now before we can actually implement the logic so that when you click on a file it opens it well actually I think we can do it let me go ahead and do file1.ts and let's do file oops this is a folder file2.tsx so right now you know clicking on them doesn't really do much so I think we can improve that now by going inside of features, projects, components file explorer tree right here and now what i want to do is i want to add the logic to well you know open some files right so after we do these instances like create folder and such things i want to call use editor so not use editor store specifically use editor from features editor hooks use editor, our abstraction. And then we're going to get all of those functions from our source and store, but for this project ID, so we don't have to pass the project ID a million times. So in here, I can extract open file, close tab, and active tab ID. I can now do all of those. So I'm going to start with open file. Let's go ahead down here. If item type is equal to file. And let's go ahead and do const is active. If active tab ID is equal to item underscore ID. Because we now have access to the active tab ID, right? And let's scroll down here to the tree item wrapper. in the on click add open file pass in item id and pinned to be false meaning this is just a preview and then in the on double click we do the exact same method but with pinned set to true on delete let's go ahead and delete this to do one close the tab so if this tab happened to be opened by at the time we are deleting it through the file explorer let's close it right no need for it to be open great and I actually think let's see is there any other place where we could close tab so we just did it here let me go ahead down here does it make any sense to do it here no because this is a folder. I wrote to do close tab here, but this is a folder. I don't think we should do that. I think the lead file is sufficient here. And I think that finally, oh, I'm not using is active anywhere. Okay, so that's one thing I have forgot. And again, only a file can be active, because what does is active represent? It represents whether we should open a file in an Explorer or not. There we go. Is active, is active. And I think that's it. Great. So should we see any differences? Well, I think we should. Let's go ahead and see. So when I click on this file, I don't think you can see, but there's a very subtle shadow, like a background, which kind of indicates is open. So click on it and then click somewhere else. You can see how it stays selected, right? Even if you kind of click between preview and code, it stays selected. So this is the only thing we can actually see now because that's the only UI we have built. What we have to do now is we have to build the tabs, right? So let's go ahead and do that. So in order to build the tabs, there's actually one more files convex function that we have to implement and it's used for the breadcrumbs. Let me actually show you how it's going to look like, right? So when I click on globe, for example, see this source components globe JSX or source index CSS, right? No matter how deep I go inside it, it can always find its way to its root file. In order to do that, we need to traverse up the parent chain. So I'm going to go inside of convex files.ts. And we have a bunch of things here. So I will copy get folder contents because it's very similar. Let's copy it. And let's just paste it here. Let me expand this as much as I can. So I'm going to go to this copied one and I will rename it to get file path and it will only accept one thing and one thing only id v.id files so we start with the identity as usual then we go ahead and we get the file if the file does not exist with throw and then instead of arguments project id we use that file project id to confirm do we even have access to get the file path, right? Let me explain actually what this is. In fact, I made a little comment. You don't have to copy the whole comment, but I think it will help you understand. So this function builds the full path to a file by traversing up the parent chain. The input is a simple file ID, for example, ID of button.dsx. and the output will be an array of answer source from root to file. So for example, if I have a folder source components button.tsx, I will get back an array with the first one being source, the second one being components and the third one being my file. And then in the breadcrumbs, I can use it to display this. Is this the most optimized thing to do? it can probably be better why am I saying that another solution of doing this would be to maintain the path right so I can have source you know components button.dsx this could be cool but imagine if I want to rename the folder source what do I do in that case I would have to for one simple folder rename, I would potentially have to update thousands of files which all have this path cached and stored. So I guess the best solution is some kind of hybrid or some kind of background job that will take care of updating the path for all files. So in case you kind of shook your head and said, is this really the best thing we can do? Probably not. But at the current state of this project, I think it's more worth it doing this than modifying all of our delete and update simple renames to be a super expensive query. Because this one isn't that expensive really, right? And it won't really appear anywhere besides when you click. And it will be cached on top of that. So I think it's an okay deal. Later, you are welcome to, of course, modify to whatever you think is best. So I'm now going to remove everything here. I just want the checks, right? And I'm going to start by defining constant path, which is going to be an empty array. But in the array, each item will be an object which has underscore ID and name. and then let's go ahead and start with the current id. The current id is a type of id files or undefined and we start with arguments id. While we have the current id let's go ahead and do const file open parenthesis await context files passing the current id as document files or undefined Let import document from generated data model here If there is no file, break the while. Otherwise, path.unshift, so add to the array, the following object file id name file.name and then the current id simply becomes this file's parent id until there is no current id meaning we found the root one which has parent id as undefined meaning that will break the while method so a pretty brute force way to do it but it gets the job done. I've explained the complexity and the alternative moments before, right? We could kind of maintain that, but the problem is a very expensive rename queries. So it's kind of a compromise. Again, you're welcome to improve this, of course, at the end of the project as a personal challenge. Awesome. So let's now go ahead and start building the UI. So we have to go back to our source, features, projects, components, and let's find the project ID view. We have added file explorer, but we never added the editor view. So now we're going to add the editor view, and it's going to have the exact same prop passed as the file explorer. So obviously we are getting errors, and now let's go inside of editor, new file components and let's go ahead and do editor-view.tsx. Now inside of this editor view, let's export const editor view like this. Let's do a super simple type here, project ID, project ID, which is a type of ID like so. And in here, let's return a div class name, heightful flex, flex column, div. The div will have a class name, flex items center, and finally top navigation component inside. And inside of here, we just pass in the project ID. Great. so we now have top navigation before we start creating it let's import the existing editor view from features editor components editor view but we didn't do much because now we have an error here of course so i'm going to go inside of editor components new file and i'm going to call this topnavigation.tsx. Let's export const topnavigation and let's go ahead and return a scroll area from components UI scroll area and let's create a nav element. This nav element will have a background color of sidebar flex items center height of 35 pixels or let's see height 8.75 border bottom and I believe that's it and let's give this one a class name of flex1 and then in here I'm going to extract the project id and just give it the exact same prop scenario as our previous ones so let me go ahead and collapse this in case you can't understand what it is. I feel like this might be easier. Now that we have the skeleton of top navigation, let's go inside of editor view. Let's import top navigation right here. And this should be project ID. There we go. And we shouldn't see much difference besides this. There is now this black line here. And you can see what it's going to become. It will become the tab, right? But right now just a tab but just a line should be rendered right uh great we now have this now what i'm gonna do is i'm going to get all of my open tabs for this project using my use editor hook and by passing in project ID. And now that I have my open, oh my apologies, not here, so sorry, inside of the top navigation, there we go, here. Let's do it again. So inside of top navigation, I'm going to get all of the open tabs for this project. And import use editor from hooks use editor. Not the store one, the hooks one, the abstraction, right? and then inside of here I can do open tabs.map file id and then index and I can return back a tab component and each tab component will have the following props key which is a file id file id is first which is a very simple index logic and the project id and let's also make sure to add inside of this scroll area scroll bar component from components ui scroll area and give it an orientation of horizontal great now it's time for us to develop the tab component I'm going to do that here. Const tab. So first things first, what props will this accept? Well, we define them below, but let's do it here. File ID, which is an ID of files, is first, which is a boolean, and project ID, which is an ID of projects. File ID is first and project ID. Great. Now, let's go ahead and just return something so we don't have any errors. Great. And in here, first thing I'm going to do is I'm going to attempt to load the file. So use file. And I can see since I don't have autocomplete here, we most likely didn't implement the hooks for use files. So let's quickly go inside of our projects, hooks, use files. okay so I have a lot of them but a lot of them are missing too so let's see export const use file accepts file id to be an id of files or null and it will return use query api files and we have the function. Great. Get file. And if we have file ID, we pass that as the options. Otherwise, we skip this query. So now, seems like that is causing a problem. Oh, it's file ID, not file. Okay. So that was use file. And now, we need to use our newly created one, file path. So this is the new hook you need to add. Use file path, which accepts the exact same thing, and it returns use query API files get file path. If it has a file ID, it will pass it along. Otherwise, it's going to skip this query. Confirm you have get file path, and confirm you have get file. I can see we implemented this in the ninth chapter file explorer so it should be here as well if it isn't here's a very easy function for you to write so it's actually the simplest of out of all of these okay now that we have this let's call use file from features projects hooks use files we just get the one and we pass in the file id great now let's call use editor so basically use editor from hooks use editor the one from our editor feature basically in the same folder here right i just want to make sure you're not accidentally importing this one make sure you are importing that one. So use editor accepts project ID and in return we get active tab ID, preview tab ID, set active tab, open file and close tab. All of those things. So now let's go ahead and add some constants here which are going to help us style this. Is active. If active tab ID is equal to file ID is preview. If preview tab ID is file ID. File name. Did we load file.name? If not, loading. Now let's go ahead and let's give this div right here for the tab some attributes. On click, set active tab. On double click, we just pin the file which is already open. So I believe if let me try and find so open file open file open file again open file let's find it this would be scenario two no case three file is already open just activated and pin if double clicked so that is this scenario here when we open a file which is inside of our tabs it means it's opened, but perhaps it's not pinned. And now in here, we have to add a class name. So let's add a class name, cnutil. Make sure you've added the import. Now inside of here, let's start adding some classes. It's going to be some of them. So flex item center gap two. Then we're going to add some height and some padding here. So height 35 or what does it say? 8.75. PL and padding write functions. Then let's add cursor pointer, text muted foreground, group and border Y. Then let's go ahead and let add border X and border transparent And finally let add on hover border accent with a 30 opacity Then for the first dynamic one will be if the tab is active so if it is active render bg background text foreground border x border border bottom background minus a margin bottom by a single pixel and drop shadow this will just create a kind of a cool effect. You're going to see what it is. And let's just add is first. If it's first, border on the left side is transparent and put an exclamation point at the end, which then results to important. So inside of here, if file is undefined, let's go ahead and render a spinner from components UI spinner. Let's give it a class name text ring. Otherwise let's use the file icon from react symbols. Make sure you didn't accidentally import one from lucid react. So react symbols icons utils. We're going to give this file name file name auto assign and a class name size 4. Below that we're going to have a span rendering the file name with a class name, cn, text, small, white space, no vrap, and let's give it is preview, italic. I think this might be enough for us to start seeing something. So if I click on here, there we go. And you can see how they replace each other. But if I double click, you can see how it stopped being italic. And now when I click on another one, the preview is now handled in this other tab. This one simply highlights back to that one, right? Let's try creating file 3. You can see it only replaces what was previously in the preview tab. If I double click on file 3, that becomes permanent. You can see how everything is synchronized. and clicking on the file too opens a third preview tab. Great, our logic is working and if you're wondering that like minus margin bottom pixel is doing this so it kind of looks like the border is going up and down so and this doesn't have a border it's just like a little trick to make that cool effect. All right now let's go ahead and implement the buttons which will help us close these tabs. So these will be normal HTML buttons and let's go ahead and do on click here event prevent default and stop propagation and close the tab. Then let's go ahead and add on key down if the key is enter or if the key is an empty space, prevent default, stop propagation, and close the tab. And add a class name here, again, cnutil. The first one will be just the general style, padding 0.5, rounded small, hover background white with 10% opacity, opacity 0, group hover opacity 100. And then for the second one, if it's active, opacity is just a constant 100. And an X icon from Lucid React. There we go. I think that is it. So now we should be able to close tabs. Awesome. We have a fully working tab system here. Super cool. Now let's go ahead and add the breadcrumbs, which are going to help us tell where this file is located at. To implement breadcrumbs, we have to go back. So let's go back inside of editor, components, editor, view, where we actually render the top navigation. And now using this project ID, we're going to actually use our previously imported hook, use editor. so import that from hooks use editor get the active tab id and then outside of this div encapsulating the top navigation we're going to check if we have active tab id let's render file breadcrumbs the file breadcrumbs will then again have a project id as the prop now let's implement the file breadcrumbs so i'm going to do that here again file breadcrumbs dot t s x let's import okay let's import react from react file icon from react symbols let's import use file path from features projects hooks use files once again let's import use editor from features editor hooks use editor. Let's import everything from breadcrumbs from components UI breadcrumbs so item page link list and separator. Let's go ahead and import ID from convex generated data model and now let's go ahead and prepare the file breadcrumbs component. The file breadcrumbs component accepts a project ID, which is a type of ID projects. In here, two things. Extract active tab ID from use editor project ID and get the file path from use file path. This will allow us to get this in this kind of, well, I have it here, right? In an array of ancestors from root to file. And then we will be able to create this. once we have that let's check if file path is undefined and there is no active tab id in that case let's just go ahead and return kind of a placeholder so a div with class name padding to bg background padding left for and border bottom breadcrumb breadcrumb list with class name on small gap 0.5 oh it's always gap 0.5 okay breadcrumb item with class name text small breadcrumb page and now this is a special unicode unicode sign for an empty string i'm not sure if this would do the same purpose i think i didn't manage to get that result that's why i'm specifically uh doing this in case you're wondering so just a normal breadcrumb composition as a placeholder, right? This is a loading scenario. And now we can kind of copy this actually. So let me just copy this and return it here because now we're going to actually be building the real thing. So close the div. Let's indent this back. Inside of the breadcrumb list, let's get file path dot map item and the index and let's go ahead and check is it the last element so if index is equal to the overall length of the file path minus one because arrays started zero let's go ahead and do react.fragment here in this form so react.fragment give it a key item underscore id then inside we are going to render the breadcrumb item with class name text small. And now let's do two scenarios. The first one will check if is last and the other one will do well something else. If it is last, we're going to render breadcrumb page with class name flex, items center, and gap one. Inside of that breadcrumb page, we're going to render the file icon with file name, auto assign and class name, and next to it, the file name. And alternatively, we're just going to render the item name using a breadcrumb link. Nothing more. I will collapse it so it's easier to look at like this. Okay. And then let's go outside of the breadcrumb item and do if it is not last, render breadcrumb separator like that. There we go. That's it. Now that we have the file breadcrumbs, we can now go back to the editor view and we can import file breadcrumbs. Let's check it out. So now when I go ahead and I open a file, whoops, looks like something's not working, could not find public function for files get file path. Oh, that's because I don't have it running. npx convex dev. This will synchronize that new function or it will show us an error if something's wrong. Let's go ahead and try this again. I'm gonna go ahead and open a file and you can see right now it's super simple right because these are root files but if i go ahead and if i create a new file instead of here so nested file and if i click on it you can see how it tells me exactly what folder it's in and if i go ahead and create a folder 2 and if i go ahead here and create a folder 3 and inside folder 3 I create supernested.tsx and click on it you can see that it can traverse up the path until it finds the exact child. One noticeable difference I can immediately tell from the original one is that the gap between the separator and items is much smaller. so I just want to quickly investigate why is that so I'm going to go back instead of file breadcrumbs here and I think I found exactly why let me see I think I know remember how I removed this\nsmall gap 0.5 because I thought logically it's not needed but looks like you have to explicitly tell it that even on small make the gap smaller there we go you can see that now it looks better currently clicking on it doesn't do anything later you could implement something like a drop down of the current files but for our use case this is more than enough so yes it has a brief floating state but after that it actually hits cache every time. I'm pretty satisfied with how this turned out. Amazing! Now let's actually implement the editor down here. We are not going to implement any AI features or things like this. We're just going to make sure we have the basic code mirror 6 added in this project. So in order to implement the code editor down here we have to start by defining what happens when a file is not selected. So that's actually quite an easy state. The only thing we have to do first is head to Polaris Assets and in here find Logo Alternative. I found it from the exact same place where I found Logo SVG. So Logo Ipsum and Untitled UI. So now let's go ahead and add this. I just copied the raw file. I'm going to go inside of Public. and I will add logo alt svg. I'm going to open this file and just paste the code inside. If you really want to you can use the exact same icon which is your main logo and then let's go back inside of the editor view and now let's go ahead and do a scenario which is if there is no active file. So let's also do const active file to be use file from features projects hooks use file and pass in the active tab id like this. So that's our active file and now we're going to check within a div which we give a class name flex1 minimum height of zero bg background if there is no active file whatsoever or we maybe can't load it let's go ahead and create a div with a class name, size full, flex, items center, justify center, and inside we import image from next image. So make sure you add this and we give it source logo, my apologies, logo dash alt svg, alt Polaris or whatever is the name of your project, width of 50, height of 50 and the class name opacity 25. Let's take a look at that. So now if I close this you can see that I have like a nice placeholder which is branded as my project until I click on a file and it loads. You can see that then it's supposed to load something but right now it isn't loading anything. So what we're going to do is do the opposite. If we have an active file in that case let's go ahead and render code editor which we don't have but let's go ahead and just well actually let's not pass any values right now. We're going to implement values later. So now let's go inside of editor components. Let's create a new file code editor dot t s x. And let's see how do we set it up. So we're going to be using code mirror, specifically code mirror version six. And this is kind of a basic example. So let's see examples basic editor looks like this is enough. So I'm going to go ahead and do the following. I will install we're going to do all of these later and dive in like what are all of these classes. But let's start with I don't know maybe something like this npm install codemirror and let's do npm install codemirror forward slash language dash javascript let me show you what versions I'm working with I think minor versions are not as important as the major one which is version 6 so you can see my codemirror is 6.0.2 and my lang javascript is 6.2.4 basically both are on version 6. Now that I have that I'm going to go ahead and export const code editor and in order to kind of render this I'm going to go ahead and do const editor ref use ref from react html div element and pass in null then const view ref use ref let's pass in editor view or null and null here as well now let's go ahead and return div ref editor ref with a class name size full pl4 bg background. And it's actually a self-closing tag. There is nothing we need to pass inside. And it looks like, oh yeah, this is an unfortunate name here. It's called editor view and we should import it from a package codemirror forward slash view. So let's actually install that package. npm install codemirror forward slash view. And then I'm going to show you my package JSON again. There we go. 6.39.8 in case you want to use the same version of my codemirror forward slash view. Why do I say unfortunate name? well because we have editor view right here so be careful not to import this one so let's manually go here editor view not there we go this is a mistake not that one from code mirror view like that and maybe i can import type editor view so it's very strictly use as a type, I don't know. Okay, so that's the view ref and I think actually we will use it later and for now let's just call a use effect which we can import from React and inside of here I'm just going to go ahead and create const view new editor view. Document will be start document parent is going to be editor ref dot current like that editor view cannot be used as this okay so remove type from here okay before we do it let's actually check if there is no editor ref dot current return so we get rid of that error and then in here extensions are going to be an array the first one will be basic setup and we should be able to import basic setup let's see from code mirror itself from code mirror and it looks like it also imports editor view so yeah you can import that from here too so basic setup and let's import javascript and pass in typescript to be true. And where do we import that from? Well, from the package we installed previously, CodeMirror lang JavaScript. Just destructure the import like this. Okay, so the view ref, I think, will not be used. Or maybe it will. Let's see. So now, down here, after we initialize the view, let's actually do view ref.current and pass in the view. And let's go ahead and create an unmount function, view.destroy. Whoops. There we go. And I think that this should be enough for us to just render a super basic code editor here. Let's go back inside of the component editor view and import it from .slash code editor. and now when I go ahead and select the random file I should see a text start document and I should be able to type in here and you can even see that there is some kind of syntax obviously doesn't work well because this is a light themed syntax put into our dark mode app so we are going to have to change the theme, but try the indentation on or things like that. Try collapsing. You can see that it works, right? There are a few bugs here and there, like this indentation is kind of being funky right now, but we will work towards fixing all of those things right now. So what I want to do now is I want the ability to, well, it makes no sense to load the actual value now because all of these files are empty. So all of them are going to be completely empty. It makes no sense to load that. Instead, how about we go inside of code editor and we change the document to be just like a super simple implementation. Let me go ahead and see. Can I write this like const? I don't know. Counter. and then in here const value set value use state 0 const on increase call the set value value value plus 1 like something super simple on decrease and return and in here a div maybe div and in here a button which shows the current value duplicate that I don't know I'm just kind of making things up as I go on increase and on click on decrease. Actually, how about we just do one? There we go. That's kind of code that makes sense. So some default value, that's kind of a good example of a JSX document, right? So that we can work with this. Currently looks horrible. So we are going to work our way through improving it and adding it some fun things, right? so let's see what is the easiest thing we can add here well i think the easiest thing we can add is the one theme one dark theme it's called like that right so let me go ahead and expand this and let's do npm install codemirror forward slash theme one dark let's install it I'm going to show you my package json code mirror theme one dark 6.1.3 you can see that six is kind of the only important thing here once we have one dark theme I'm going to go ahead and import one dark and all I'm going to do is I'm going to add it to my extensions list like this one dark and I'm going to save and then I'm going to have to refresh here and let's see hopefully it kind of looks better now looks much much better how about this try copying your entire file from your actual editor and paste it here look at that looks pretty good you can't scroll but it looks pretty good You can fold things, right? Looking really, really good, but not perfect. So we are now going to add more and more stuff here until it looks better and better. So how about we start by giving it a full height team? This should enable us to scroll down. I think that one is an easy win too. So the place I want to develop that is within the editor, but I'm going to specifically start calling these extensions. So they aren't, you know, combined with components or anything like that. And I will call this one theme.ts and let's do export const custom theme. Editor view from code mirror, not from our components, right? And let's go ahead and call dot theme here. The first one will be and, which simply hides the outline. Then we're going to go ahead and add CM content here. And the font family, I think this is actually not the correct variable. Where do we find the correct variable? In our source app layout. So we added font enter and font plex mono. Make sure yours is the same set of source app layout. You can see it's just simple next font Google imports. Copy the variable name and now add it here. And also double check in your globals.css. So your source app globals.css all the way up here. Font mono should also be font plex mono here. Great. So that's CM content and font size 14 pixels. CM scroller, scroll bar width, thin and transparent. And let me see, let me see. I think I'm missing something because I have a feeling like none of this will actually help me with my scroll situation. Let me refresh. Oh, I didn't add this theme, so I won't see it anyway. So I have to go inside of components code editor right here. And we have to add the theme. So after one dark, I'm going to add custom theme, which we import from extensions theme. I'm going to refresh again. Let me open this. And I don't think much has improved. I mean, nothing that I can see. And I'm still having a problem with scrolling. So I still cannot scroll. I'm trying to figure out why that is. I mean, this happened to me as well. I just can't pinpoint what exactly was the fix. I was kind of sure it was going to be the theme here. But maybe it is not. So let me go ahead and kind of discover a bit. All right, so I did some research. and I actually don't really understand why we are having the issue that we're having. You know, basically, you can see it doesn't take 100% of the screen, right? So if I copy along, I mean, this is how I tried debugging. So instead of my code editor, I changed from BG background to red 500. And I saw, okay, so it's taking 100% of the space, right? so why is the editor ref not being populated 100% so then I went inside of my extensions custom theme here and in this one I gave it a height of 100% and that actually fixes it so if I go ahead now inside of this you can see there's space down here and if I go inside of code editor and I copy something long You can see that now I can scroll, but I'm not sure why, because this was not needed in my original source code. So I'm just not 100% sure why, and I'm not sure how it's going to behave going forward. Let me try with zoom in and zoom out. It seems to be working very well. It's important that no lines are being cut off. Let me try triggering search functionality. You can do command F and that should trigger the search functionality. So let me try closing it. That doesn't seem to create any problem either. So I'm very confused right now. I have no idea why in my original source code I didn't need to put height 100. But here I need to put height 100. So what I'm going to do now is I'm just going to make sure that I'm using everything the same as in my original source code. For example, I don't import from CodeMirror here. Instead, I import from CodeMirror view. So I'm just going to make sure, okay, I'm going to use that here too. Let me try. If I comment out height, does that maybe resolve it? I doubt it. It shouldn't. Makes no sense for that one because they're essentially the exact same thing. Oh wait, was it that? No, it's not. Because I can see the scroll bar is here, which means if I copy something larger, yeah, I can't scroll no matter what I do. Okay, so looks like we just need height 100 in our custom theme. I'm not sure why, so if it's not working for you, okay, you know, just continue going through this chapter and I'm gonna investigate this later so we can determine exactly what's happening here. How come I didn't need this in my original source code, but I need it now. Maybe the answer is in some other components. I don't know, but I double checked everything and everything looks fine. So yeah, let's just, you know, wing it and we're going to use our custom theme like this. Okay. And now I think that we kind of hit the limit of what we can do ourselves. Well, maybe the only thing we can improve is implementing our own language extension recognizer. Because right now, this only works for JSX or TS files, right? If I, for example, try creating, I don't know, a index.html, you can see it's using the invalid syntax, right? Well, unfortunately, this will work too, but maybe not fully, I don't know. try some other language right inside. I think we can maybe copy something here. Let's see. Do we have anything here? Like a readme? Try pasting it in here. Yeah, you can see this is not correct. See the syntax breaking, right? So how do you implement a smart recognizer of syntax? Well, step by step and one by one. So what we're going to need to do is create another extension here. So inside of your extensions, create a new file. I'm going to call this actually, okay, yeah. Let's go here. And I will call this language extension.ts specifically. And now here's the thing. You decide how many languages you want to support. so I imported CodeMirror state, CodeMirror lang javascript and now what I have to do is I have to install all of these packages so pause the screen and install all of them and I'm going to show you my versions so these are all the packages CodeMirror HTML, CSS JSON, Markdown, Python and there's so many more you can do but you have to install them all and you have to configure them so now while this is doing, let's go ahead and let's export const get language extension, accept file name, which is a string and return extension from code mirror state. Now in order to get the extension from the file name, we will split it by a dot separator, get the last one and lowercase it. and then we're going to use a switch case on the extension if case is js we are going to return javascript right and so on and so on so i just going to go ahead and copy and paste the rest Here it is So let me show you so you can pause JS, JavaScript, JSX, JavaScript with the config. Same for TypeScript and TSX, which enables both of them. HTML, CSS, JSON. For both MD and MDX, we use Markdown, Python, and by default, an empty array. Great. So that's the language extension. I didn't show you my package JSON, so here it is. If you want to see all the languages that I have, again, six, the major version is the one that's important. Now that we have the language extension, let's go ahead and initialize it here. Const language extension will be use memo. And let's go ahead and call it like this, get language extension. from extensions language extension and pass in the file name. The problem is we don't have the file name, yes. So, okay, pass in the file name and put it in the dependency array, file name. So let's import use memo from React. And for the file name, let's go ahead and create an interface, props, file name, string, extract, file name, and assign the props. And now we have that error resolved. and now inside of here after you do the basic setup let's add language extension in place of the javascript one like that let me go ahead and maybe write this in a prettier way like this remove the unused import now now let's go back to the editor view where we actually do this and let's pass in file name to be active file dot name like that there we go so now when i open tsx it should oh i have to refresh first yes always refresh first because the hot reload does not affect this so you can see this is now tsx right But you can see how here the syntax breaks, which is correct because this is an HTML file, right? So it shouldn't render the same syntax. And just like that, we've implemented our own language syntax decider. And now let's see what else can we do. So for example, our tab indentation currently sucks. It doesn't work. Let's go ahead and improve it by adding CodeMirror commands. So npm install, let me go ahead and fix this, CodeMirror commands. And once you install it, let's go ahead right here, import indent with tab from CodeMirror commands. and then down here after language extension add keymap.off and inside of here add indent with tab. And the keymap solution comes from the following. So I'm going to do a slight modification here simply so I have the exact same as my source code because I'm still bazled why it works on one example but doesn't with the other. so editor view from code mirror view and key map from here and now you should be able to indent with tab let's go ahead and see again let's do a refresh let's go ahead and try there we go you can see that now it doesn't escape the editor i can now safely indent. Who knows? Maybe it was some of these extensions which enabled the full height view. But let me try pasting again. It seems fine. It seems to work okay. I'm not sure what was the issue, but I'm just confused. Why do I need it here, but I didn't need it in my source code. But still, seems to work great. Okay, what else should we do? How about a minimap? right? You see this? How about we add that? So that is actually implemented by Replit, right? They actually have a package of their own. So basically, a bunch of community packages exist for CodeMirror. And this one is from Replit, because Replit itself decided to use CodeMirror over all the other editors that exist. So I actually gave you a very good and tested solution Once we install that, let me show you my package JSON. So replit.codemirror.minimap 0.5.2 if you want to use the same version. And we're going to head inside of the extensions here. And I will just add minimap.ts. I'm going to import show minimap from replit.codemirror.minimap. I'm going to do a simple create minimap. and the last time I mentioned minimap hopefully export const minimap with show minimap compute document and return create and the create method above alright now that we have that we can go back instead of the code editor and after key map off add minimap from the extension right here extensions mini map. There we go. Let's try it out. So copy code editor entirely. Make sure to refresh this page. And let's go ahead and select the file, paste things inside and look at it go. A beautiful mini map right here. How about we add another useful feature? For example, you can see in my finished source code, I have this kind of indentation indicators. I can see the depth of indentation. That is actually also handled by Replit. So let's go ahead and do a quick install. npm install Replit code mirror indentation markers. And I'm going to show you what package JSON version I'm using. So package, oops, here it is, package JSON, 6.5.3 version for the indentation markers. This one is much simpler to add. We just import indentation markers from Replit CodeMirror indentation markers. And let's go ahead now and just add them after the minimap. like that, indentation markers. And I believe that that is kind of the end of what we can do without modifying the basic setup. Here it is. You can see how we now have indentation markers here and how they change depending on the indentation level. So what do I mean by the end of what we can do without modifying this? basic setup. Well, here's the problem. You see this kind of fold gutters? You see this? It kind of looks weird. It's not centered. And if you try to use CSS to center it, you will very closely come to a limitation, very soon come to a limitation, because this is just a unicode this is a character it's not actually an svg icon you can see how good it looks on my finished source code here and the problem is the fold gutters do exist as something you can extend right so you would have to add fold gutters here and then you would do render icon or whatever is the syntax svg blah blah blah you would add your own the problem is fold gutters are already implemented in here in the basic setup so that's a problem you can't implement it twice and you can't extend basic setup but thankfully the creators of codemirror have that solution for us because basic setup is an amazing set of all the packages you need to have your app up and running right so using the link on the screen you can uh first i'm going to show you how to find it yourself but i also if you cannot find it you can always find this my assets folder in here in here we have the custom setup file entirely, everything we're going to need. Why am I telling you to copy this and why are we not implementing this? Well, because I didn't implement it either. All I did was I searched for CodeMirror basic setup on Google and this repository came up from CodeMirror basic setup and inside of the source I have CodeMirror.ts and here it is, the entire thing, right and here it is the fold gutter the one i want to modify the icon for so that's how you do it and it's actually intended to be used like that so the extension itself does not allow customization the i.e the idea is that once you decide you want to configure your editor more precisely you take this package's source code which we are doing and copy it into your own code that's exactly what we are doing right now so we are doing this in the way it was intended so using the link on the screen, you can find Polaris assets and in here custom setup or you can just, you know, visit the source code and you can just copy the entire thing here. So it is exactly the same as this one here. The only difference is I added SVGs for folder gutter icons and everything else is I'm pretty sure the same. Maybe I did some modifications, but I highly doubt and I kind of removed the comments and everything else to make it cleaner. So I'm going to copy this entire thing and then I'm going to go inside of my extensions again. So in here, in the extensions, new file, custom-setup.tsx, my apologies, .ts and let's paste the entire thing inside. That's it. I'm not even going to go through this because, you know, it's just a copy from the source code. So the only thing we do is we modify the folder gather because I want it to have a nicer SVG icon than whatever it's currently using. And once I have custom setup I can go back inside of the code editor and I can remove basic setup in place of custom setup From extensions custom setup And let's remove basic setup from code mirror now. All right. Let's go ahead and check it out now. And now we should have a much nicer icon to collapse things. There we go. This looks much, much better. Perfect. And see this happening? you can kind of select this. This is the reason why I went inside of my globals.css, and down here, I added a to-do add select none later. So that's why I did that, select dash none. We can bring it back now, because it actually just works better. You shouldn't be able to select things anywhere here. Everything kind of is supposed to look like and feel like an app. So that's why I did that. All right, awesome. So there are definitely more things we can do with our code editor, but I think I want to keep that for another chapter simply because those are AI related things. So it makes no sense to implement them right now because we won't be able to implement the entire thing. But one thing I want to do is the ability to preserve content, right? So I want to give you the option to save a file. Let's start by changing the code editor interface. So besides file name, let's also give it an on change and let's also give it an initial value. So this is not going to be a controlled component because if it is a controlled component, if you constantly accept the new updated value, it's very annoying because you are typing something here and it receives a new like updated value of this document and it will reset your cursor up here so it's super annoying so because of that we are not going to treat it as a controlled component we're just going to accept the initial value once we load and reset the and reset the initial value on every file change all right so we have editor view view ref we have language extension here. Let's change this document to instead be initial value. And yes, we can destructure these now. So file name, initial value and on change. All right. So we are using initial value here. Document initial value. Perfect. Let's see. What are we missing here? we are missing language extension here. And we actually don't need the initial value because it's only used for the initial document. So we don't want to track its changes. So this isn't the best thing, but let's do eslint disable next line, react hooks, exhaustive depths. And then I'm going to add a little explanation. So dash dash, initial value is only used for the initial document. I will see if there's like a prettier way of doing this, but for now it gets the job done. So we don't have to pass the initial value here. Great. That part is now done. And now let's go ahead and implement the on change. I first just want to confirm that we have the necessary files here. Too many files open. Inside of convex files. Do we have update file? We do. Great. Now, let's go ahead inside of source, features, projects, hooks, use files. And the same thing as we have use create file, let's go ahead and do use update file. And let's just call update file. That's it. And we need to add, I'm going to add to do add optimistic mutation to these things simply because they make the app feel that much faster, right? But we're not going to do it now. We don't need to do it for queries. And we don't need to do it for this because it makes no sense, right? Because this is not a controlled component. So now that we have this, let's go inside of the editor view.tsx right here. So the component, right? And what I'm going to do is I'm going to add const update file use update file a hook from features projects hooks use files. Let me reorder my imports just a little bit here. Okay and now that I have that I'm gonna go ahead and define a timeout ref because I want to create a debounce. So I don't want to update on every keystroke, even though Convex can probably handle that. Still, no reason to do so. Let's be gentle with our updates. So use ref from react, and we're going to implement our very own debounce here. So let's go ahead to the code editor right here. First things first, it should reset on every active file ID, right? And then let's give it the initial value active file dot content or an empty string. File name is already given, So on change should accept the content, which is a type of string. First things first, if we have an existing debounce timeout, clear it. Otherwise, let's go ahead and let's timeout. So we're creating a timeout for this much. So we could store that up here, const debounce in milliseconds. like that, so you can easily change it later without having to find the exact function. There we go. Like this, it will update using ID active file and the new content. Now we have to go back inside of the code editor here and actually use on change. So after indentation markers, use editor view update listener of update if update doc changed on change actually it's going to be required so you can pass in on change update state document to string and let's see on change it shouldn't be optional it's required like that and i think that should work so let's try let's copy code editor here and let's see last saved 15 minutes ago i'm going to refresh this entire project now i'm going to go here select file one still saved 25 minutes ago and i'm going to paste the entire code and i'm going to wait for a second so now saved less than a minute ago and if i refresh and if i click on file one there we go we are successfully persisting our content amazing auto save no need to save let's now see uh does it actually actually do something right i'm going i'm again waiting i'm waiting i'm waiting so updated definitely and it didn't move my cursor at least not what i've noticed so that's what i was testing right i think it works very very well Let's see how it behaves when I change different files. Works just as well. Amazing. I'm super satisfied with this. We still have one mystery, and that is why do I need this? I just want to test one more time. Is it maybe once I add a bunch of these other extensions that something magical happens here? Oh, it looks like it is. Yeah, when I add a bunch of extensions, Maybe it's like the minimap or something. You can see that at one point, we no longer need height 100. I'm not sure why. Maybe it's the minimap. Maybe it's the indentation markers. Let me try commenting this out. Oh, yeah. That's what makes it able to scroll. Okay. So, yes, when we have minimap and indentation markers on, probably just one of them, the scroll is 100%. percent. So if you have those two, you can most likely remove height 100. So however you want to proceed. I don't think this will hurt. So maybe we can keep it here. But looks like those extensions of ours do the job as well. Amazing. So I'm just trying to see if there's any last thing we can do, but I think you've worked hard enough. There is just one thing, actually. Yes, I'm sorry. And that is we shouldn't always render the code editor, right? So let's go ahead and do the following. Const is active binary. Is active file a binary file? So if we have an active file and if active file has a storage ID, it means it is. Is active file text? If we have an active file and if not active file dot storage id. Don't make the mistake of checking if we have active file dot content because we don't have to. A file is allowed to be empty. The question is if we have storage id it means this is not supposed to have any content at all. And now let's only render the active file and the code editor if is active file a textual file. And this should still work just fine. You know what? Maybe instead of doing this, we can go inside of code editor and change the initial value to an empty string because we obviously want our files to be empty in the beginning. So why don't we modify the code editor to accept that rather than this? and then we can make the initial value optional. There we go. That looks much cleaner on our side. Perfect. And then let's go ahead and do if is active file binary and we're just going to do a paragraph to do implement\nbinary preview. Great. So let's just do one more check and then we're going to open a pull request and oh yeah this is super annoying. Can we please go inside of file explorer index.tsx and just change the default value of is open to true. It's super annoying that we have to open it every single time. So now when I refresh, it's opened by default and everything here is saved. Amazing, amazing job. Works super well. We have everything here. Awesome. So let's go ahead and let's merge this, right? So we have 18 files, 19 files, right? Package, JSON package, lock files, new image globals code editor editor view breadcrumbs navigation custom setup language extension minimap theme use editor use editor store project id view index.tsx tree and use files okay let's go ahead and stop all of our terminals here chapter 10 so git add dot git commit chapter 10 code editor and state. Great. Git checkout dash b 10 code editor and state like this. So code editor state and git push u origin 10 code editor state. This will then push it. You should be able to see the new branch down here. And let's go ahead onto our GitHub. Let's open a pull request and let's let CodeRabbit review this massive PR we've prepared. And here we have the summary. We added code editor with syntax highlighting, minimap, indentation markers, and multi-language support. We added file breadcrumb navigation showing a file hierarchy. editor tabs for managing open files with preview and pinned modes we integrated file explorer for direct file opening and tab management and we disabled text selection across the application so looks like we were pretty good only three actionable comments we do have some nitpick comments but these aren't critical right so you can always see the nitpick comments right they personally define them as that and they hide it by default right for example potential stale closure on change not in effect dependencies so yeah technically that is correct but you can see it doesn't actually break anything so in here it's actually warning us about adding this select none thing to our body. So its concern is that it breaks the editor itself that you can select within the editor. I can personally still select. I'm not sure about other browsers or if you have bad experience with select none, feel free to remove it from the body. You don't need it. I just think it's like a cool thing to make it look like a native app. But yeah, maybe not the smartest solution, giving all the browsers and mobile ones, and who knows what select none will actually do. So perhaps in my next chapter, I might remove it and just revert it to as it was. So good comment here. Inside of here, we actually do have a real mistake. We never clean up pending debounced updates. We clean it up here, but we don't have a use effect with unmount to make sure the timeout ref is cleared. So this can cause memory leaks from uncanceled timers, attempted updates after unmounts, and updates to the wrong file if active file changes before the timeout fires. So a very serious issue that we have to fix in the next chapter. Thank you, CodeRabbit. In here, it doesn't like the fact that we removed the outline, but it just looks so much better without it. So I'm going to keep it removed in this case. Awesome. Let's go ahead and merge this pull request. We just did an amazing job. So almost a thousand new lines. So we have chapter 10 right here. Let's go ahead back to main, git pool origin main. We are now synchronized completely. I like to do a little sanity check. I should be on main branch and inside of my graph I should see 10 detached and then merged back here. Amazing. So I believe that marks the end of this chapter. We implemented state management, code mirror 6, one dark theme, tabbed file switcher, syntax highlighting, and a bunch of other things actually. Amazing, amazing job and see you in the next chapter. In this chapter, we're going to add AI features to our code editor. This will include implementing the ghost text suggestions, handling tab key suggestion acceptance, creating the command or control plus k quick edit model, adding a fire crawl scraping functions so users can paste URLs into those quick edit models. And we're going to add all other selection based code editing features. It's better to show you exactly what I mean. So this is the final product. And you can see that if I attempt to write my own on click method, I have AI giving me auto completion. And besides that, I am able to use quick edit and give this, for example, a prompt, rename this to on enter or anything like that. And you can see it gets the job done. So that's going to be the purpose of this chapter. We also have some things from the previous chapter from CodeRabbit. So we're going to start by just fixing those issues, such as some memory leaks. And then we're going to go right into building the AI features. So for now you can just have your npm run dev running and npx convex dev running. Let's go ahead and refresh our localhost 3000. The first thing I'm going to do is I'm going to go inside of my source app folder globals and I'm going to remove select none from here simply because I don't know how this behaves in other browsers and I don't want to give you some bad advice. It's better to have that not here, especially in global CSS. Later, we can add it to specific elements, but in global body element, maybe not the best idea. And the second problem is we have no cleanup for our debounced updates. So that is inside of the editor view, specifically talking about this timeout ref. So every time we call on change, we debounce using the timeout ref. and we do clear it if two of them appear at the same time, but we never clear it if this unmounts. So that's something we should do. We can do it very easily using useEffect. So down here, I'm just going to do a useEffect. Clean up pending debounced updates on unmount or file change. So if ActiveTab ID has changed, we're going to have a different unmount function here. If we have timeout ref, let's just clear it and make sure to import useEffect from React. That's it. That's all we have to add in our editor view. Perfect. Now let's go ahead and let's implement a new extension called suggestion. So for that, I'm going to go inside of features, editor, components, and I'm going to go inside of the code editor. And in here, we have to add our own extension. So I'm going to go ahead and do that right here. And it's going to be called a suggestion. And I'm going to pass in the file name here. Now let's go ahead inside of extensions. Let's create a new folder, suggestion. And inside of suggestion, create an index.ts. It will have multiple files inside. So that's why I'm creating a folder. and now we have to go ahead and build this so I'm going to build this in stages so it's easier to understand we're going to start with stage one stage one is basically going to be static ghost text it's going to teach you how to implement a basic widget type decoration state field key map and it's always going to show the exact same suggestion. So no fetching, no AI just yet because that's too much information at once. So let's start very, very slow. So obviously, I have to create some kind of function which accepts the file name. So let's do that. That's easy. Export const suggestion, which accepts a file name. And let's go ahead and return an array. and now in here we're going to build three different let's call them mini extensions right the first one will be the suggestion state basically that's going to be our state storage right what's the current suggestion and now let's go ahead and build it so in order to build that we actually have to add some imports from code mirror state let me go ahead and expand this here state effect and state field from code mirror forward slash state. And I don't know if you've noticed, but we actually have access to a bunch of code mirror packages that we never explicitly installed. If I search for code mirror state, it doesn't exist. And I've actually noticed that in the previous chapter, because we added this extension called custom setup, and I just pasted this entire thing and I've noticed I'm not getting any errors for this and that's when it clicked oh it's probably because we have code mirror installed and that's serving as kind of the base package you can confirm that so we should both have code mirror installed right And if you actually go inside of Node Modules and specifically search for code mirror here And in its package JSON you can see it maintains all of these dependencies So in case you're wondering how come we didn't have to install any of this, it's because we have all of them. So I think we actually don't need to have code mirror view or code mirror commands, right? Because if I search for it, you can see it's one version here and then another version here. So that's actually conflicting. So what I'm actually going to do is I'm going to uninstall CodeMirror commands and I'm going to uninstall CodeMirror view simply because I can see that both of them are maintained in here. I think all other ones are actually good, right? none of these seem to appear in here, especially not the replet ones. So just for sanity check, I'm now going to search through my code for all the places where I import code mirror commands. So one of them is inside of code editor, where I import indent with tab. And you can see in here, it works perfectly fine. It's still inside of my node modules because it was maintained by another package, right? So looks like that is working perfectly fine. If it's not working for you, you can always manually do npm install and just have it installed. That's actually what I did in the initial build of this project. But understanding the code more now, I can see that all of these dependencies are actually maintained here. So I'm not sure it makes sense to install them separately when all of them live within this base code mirror configuration here. All right. So now that we solved that mystery, let's go back inside of our suggestions index here. So I've just imported code mirror from state. And while I'm here, I'm also going to add the following imports from code mirror view, decoration, decoration set, editor view, view plugin, view update, widget type, and key map, all coming from CodeMirror forward slash view. So let's start by building something called set suggestion effect. Set suggestion effect uses state effect and we simply define it first. So it can be a type of string or it can be null. I'm going to add a little comment here so you know what this is. State effect is a way to send messages to update our state. Think of it like an action in a reducer if you've ever used Redux. And we define one effect type for setting the suggestion text. Now, the second thing we have to do is we have to define our suggestion state. And that's going to use the state field dot define. Again, it can be string or no. So I'm going to add some more comments here so it's easier to understand what that does. State field holds our suggestion state in the editor. The create method, which we're going to build, returns the initial value when the editor loads. And the update method is called on every transaction. Transaction is basically things like keystroke, cursor changed, I mean cursor position like this, right? Every time that happens, we're going to call the update method and we will recompute what the AI should suggest to potentially update the value. So let's start with the create method because that's the simple one. And in here for now, all we're going to do is just add to do implement this because right now we don't really have anything functional, right? We don't have any endpoints to call. And in the update, we have value and transaction here. And in here, what we're going to do, we're going to check each effect in this transaction. If we find our set suggestion effect, we're going to return its new value. Otherwise, we're going to keep the current value unchanged. And we're going to do that using a for loop. for effect of transaction.effects if effect is set suggestion effect which we have defined above simply return its value otherwise so outside of this for loop return value and let me go ahead and fix the typo there we go so you shouldn't have any errors in the suggestion state nor in the set suggestion effect. So that is the simple one done. You can see suggestion state is now fully functional here. Now the second one we have to add is the render plugin. So what's that going to serve? We currently implemented the state management for our suggestion text and we implement what's going to be in future logic to update the current value, right? So if I put my cursor here, AI will give me relevant information about this. But if I change my cursor here, it's going to give me relevant information about this. That's what this is doing, right? And this basically just holds the state. But we don't actually render this anywhere. So that's what we are doing now. We need our render plugin to render the ghost text. So let's go ahead and build that. Const render plugin uses view plugin from class and in here we have to define a class we have to give it decorations decoration set so everything that I'm adding here decoration set view plugin we've added all of those imports here so make sure you have all of them great we now have decoration set now let's go ahead and build a constructor that's going to initialize this dot decorations which we've defined above and it's going to use a build method which doesn't exist yet. We're going to create it and make sure to have the proper props here. Now besides the constructor we're going to have our update method here which is going to accept a view update. So basically what this function will do is it will rebuild decorations if the document has changed, cursor has moved or the suggestion has changed. So this one makes sure that a new AI suggestion is being created. And this one ensures that it's being shown again. So it's not stale, right? So that's the difference. This is the render plugin. And this is the state management plugin. That's why I'm separating them. All right. So let's see if the suggestions have changed. We are going to go through update.transactions dot sum. Find the transaction inside and I'm immediately returning transaction dot effects dot sum. Find the effect and immediately check effect is set suggestion effect. All right. Now that we have that, we are going to finally check if update dot document has changed or if update selection set or if the suggestion from the AI model is different. In any of those scenarios, we have to display something new to the user, right? So if I move the cursor here, I have to update the render plugin. If I move it here, I have to update it again. If the AI builds something new, I have to update it again. If I write something, I have to update the suggestion again. And so all of these scenarios are covered here. I just hope this is not confusing me. I'm not a big fan of this early returns, right? So let me try and kind of rework this so it's easier to look at. So basically like this. Suggestions change. Let me try to expand it even more. Suggestions change. Update transactions. Sum transaction. And then you can see I'm now opening a function. return transaction.fx.sum effect open a new function return effect is set suggestion effect right so this or this same thing i just kind of feel like this is easier to read i always find these very confusing so whichever one you prefer this or this same thing all right so now uh that we have that let's go ahead and do one more thing instead of adding all of these in a big if clause we can kind of separate them in a constant right so should rebuild and then the exact same thing that we have in our if clause and then we can just use should rebuild here so should rebuild basically means rebuild the decorations if the document has changed cursor has moved or suggestion itself has changed. I feel like this is easier to understand, right? I know this is a very new syntax, right? I doubt any of you was building code mirror extensions. So I'm trying to make it as primitive as possible. So it's easy to follow along, right? I don't want you to just blindly follow me. I want you to understand what we're doing. So that was the update method. And now we have to build this build method that's currently underlined red so let's build we get the view which is a type of editor view and what we have to currently do is get the current suggestion from the state so const suggestion view state field suggestion state which is our state from above. If there is no suggestion, return decoration.none. And now what we have to do is we have to create a widget decoration at the cursor position. So first let's get the cursor. You can do that using view state selection main head and then return decoration.set. So now we're going to display something and that's basically going to be a decoration whoops not an object an array decoration dot widget pass in the widget new suggestion whoops suggestion widget and pass in the suggestion now we also have to build the suggestion widget that one thing i missed my apologies and it going to be super simple basically we can also write this directly here but i just want to keep it separate what we're doing now in the build here is just telling the decoration okay now finally show this to the user right user should now see this so let's keep this red for now and let's just passing side to be one. So what does side represent? It basically means render this after cursor, not before, which would be minus one, right? So if my cursor is here, this is where I expect my AI suggestion to arrive. And let's also chain dot range cursor. So this should be the only a red underlined thing here. And we're not just done yet. So after this last curly bracket right here, open another set of objects, add decorations, get plugin and execute plugin dot decorations. And this basically tells the code mirror to use our decorations. all right now let's go ahead and let's build the suggestion widget so i'm going to do that right above our render plugin here so class suggestion widget extends widget type and in here let's simply define a constructor read only text and call and execute super then go ahead and call to DOM so what are we writing to DOM well an element a span element and the span text content will be whatever we pass along from the build function and you can see that that is basically going to be the suggestion right so whatever we manage to extract from the suggestion field we're now going to add to this span element now let's go ahead and make this ghost text appearance by giving it an opacity. Let's make sure this doesn't interfere with clicks so it has the real kind of unobstructed feeling. And let's return a span just like that. And we can add some comment at the top of the class widget here. Widget type creates custom DOM elements to display in the editor. To DOM is called by CodeMirror to create an actual HTML element. There we go. and now let's go ahead and yes the file name is still unused because we're going to need it later let's actually go inside of components code editor here and let's import suggestion from extensions suggestion and let's try it out so don't expect too much right but it should when you hover somewhere so let me find something where i have some code wherever your cursor is, you should have hard-coded to-do implement this, and it shouldn't interfere at all, right? But you can see how it re-renders exactly where I put my cursor or whenever I type, right? So that's what all of those updates are doing. They are keeping track that we have a new display of our ghost text suggestion. Obviously, right now, it's not very intelligent, right? It's just going to add this everywhere. But I wanted to introduce you to building extensions through this very primitive, easy way. I feel like it's easier to digest it this way rather than just building the entire thing and you just building along with me without understanding what we're actually doing. So that was the suggestion state and that was the render plugin. How about we make it even better by adding the acceptSuggestionKeyMap plugin. And that's pretty easy, actually. So that is tab to accept, right? Because right now we can see the suggestion, but there's no way of accepting it. So I'm going to go ahead right here and I'm going to build it. So const acceptSuggestionKeyMap is keymap off. go ahead and find the tab key run view let's go ahead and find this suggestion using view state field and suggestion state from above if there is no suggestion let me go ahead and copy it in that case we simply return false basically that means no suggestion found, let tab do its normal thing, which in our case will be to indent. Otherwise, if suggestion was found, let's go ahead and find the cursor position. And then let's go ahead and let's call view.dispatch. And inside of view.dispatch, First things first, insert the suggestion text. So changes from cursor, insert suggestion. Insert the suggestion text. Then let's move the cursor to the end of the suggestion length, right? So we have to move our cursor now after we've accepted that new thing. And finally, we have to clear the suggestion. So we simply call set suggestion effect of null. And last thing, return true, which means we handled tab don't indent. We are kind of overriding the native tab functionality. Well, not exactly native, but which is the one? Keymap indent with tab, right? We are overriding this. So now, if we've done this correctly, when you hit tab, you can see this is actually inserted now. right? Try refreshing. After that, suggestion is lost. So you need to refresh. So try, you know, going somewhere like this and press tab and you can see it's inserted now. So we are successfully accepting a suggestion. The problem is suggestion right now is quite dumb, right? It doesn't do anything. It has no idea where we are. It has no context. It doesn't do any AI stuff. But this is kind of the simplest extension I could think of to help you build this. So now it's time to go to stage two. In stage two, we start differently. So let's go ahead. Where am I? All the way to the suggestion state. In stage two, instead of returning a pre-made string, we are going to start with null, right? So we start with null because we aren't going to have anything in the beginning. And then we have to create something called debouncing and suggestion generation. So I'm just trying to find a proper place to do this. So I'm going to do it after the suggestion widget here. So stage two will basically be creating the debounce functionality. Start with debounce timer, which can be a number or null, by default null. Is waiting for suggestion will be false, and debounce delay will be 300. Now, what we have to do is we have to build a fake suggestion. So still no AI calls yet. Let's just do generate fake suggestion. Text before cursor will be one thing we're going to accept. So now we're building a kind of more advanced text suggestions, simply because we will have some information, for example, like text before cursor. And we're going to trim that. So whatever we have passed here, trim the end. And then let's just check. For example, if trimmed ends with constant return my variable. So if I start typing const, ghost text will suggest my variable, right? So somewhat smarter, right? We're just learning how it works. And you can add as many examples here. It doesn't really matter. You don't have to add all of these. I'm just adding them so it's easier to understand the state of our current app. And the last return null here. So we are mocking suggestions, Rauer. We are making them up. We're kind of pretending how AI is going to behave. now down here let's go ahead and let's create the debounce plugin so let's create create debounce plugin which accepts file name and return view plugin dot from class and let's go ahead and define the class here again so we're going to have a constructor a view type of editor view calling this.triggerSuggestion with the view. Then besides the constructor we're going to again have an update which accepts update which is a type of a view update. If document has changed or selection has set call this.triggerSuggestion and pass along update.view. And then finally let's go ahead and let's implement the trigger suggestion here. So this accepts the editor view and first things first if an existing debounce timer exists let's clear it. So this is our debounce protection. Now let's go ahead and change the variable is waiting from suggestion to be true and now in here I'm going to define my debounce timer to be window set timeout asynchronous method and in here let's go ahead and do a fake suggestion and I'm going to just add a little parenthesis here delete this block later in stage three so this is now stage two right so let's go ahead and start by seeing where is our cursor then let's go ahead and do what line we are currently at and let's find text before cursor line text slice starting from zero and going to cursor minus line dot from and then we are going to generate a fake suggestion using this text before cursor so this will return something right it will either return my variable my function log or null We are kind of learning what AI will be doing in this example So right now this is just well fake Let now change is waiting for suggestion back to false. Let's call view dispatch effects set suggestion effect dot off suggestion. like so and in here add a debounce delay which we have defined above looks like we are still not using this is waiting for suggestion anywhere but we will uh later for now let's go ahead and just finish this so after debounce dot delay and after this function here let me see i'm trying to figure wrote where I might so this is a class let's just see I think we should do it here destroy let me see is that working here if the bounce timer is not now clear timeout the bounce timer all right I'm trying to figure out if I did this in the correct indentation. So this ends the trigger suggestion and after that we call destroy. I think this should work just fine. Now that we have this create debounce plugin, let's go inside of the render plugin right here. Update is fine as is, but build should be modified now. So what we're going to do here is we're going to call if is waiting for suggestion return decoration dot none so if the suggestion is currently uh in process of being created do not return anything so that's one thing we have to do and then let's go down here to our export const suggestion and it should be a little bit different now so we should still have the suggestion state but we should also now have create debounce plugin and pass in the file name like this so what that's going to do is it will trigger suggestions on typing so if we've done this correctly, I'm just going to do a quick check through the code. If we've done this correctly, we now should not see the default suggestion only when we start typing something. More specifically, only when we start typing, and yes, file name is still unused, that's fine. So let me find where is the generate suggestion function. I'm trying to find it. So if you type const, you should get my variable back. Or if you type function or console or return, let's try all of them here. So I'm going to go ahead at the bottom of the file. I can see that no suggestion is happening on cursor change. Good. If I type gibberish, nothing is happening. But if I type const, there we go. I get my variable. If I get function, I get my function. If I add return, I get null. If I write console, does it work? Is it console? Oh, it's console. Okay, console. I can see out a suggestion for log. So yes, obviously it's working. Perfect. Now the biggest problem is it's still not calling any AI. This is all still just fake. So what we have to do now is we have to start building the actual endpoint for this, because the AI suggestion cannot work without calling an endpoint. So in order to build that, let's go inside of source, app folder, API, and in here, I'm going to build suggestion. And inside, let's add route.ts. let's start by preparing some imports such as generate text and output from ai let's go ahead and import next response from next server let's go ahead and import z from zod and then import your provider. For example, Anthropic, not from ingest, from AISDK Anthropic, or Google from AISDK Google. So whichever one you chose to use. Now we have to build the suggestion schema, which is going to be a ZOD object, which accepts a single property called suggestion. it's a type of string and we describe it as the code to insert at cursor or empty string if no completion is needed now what we have to do is we have to build the suggestion prompt suggestions suggestion prompts are always kind of tricky and it definitely makes no sense to type it out here so you can just go ahead and visit the polaris assets folder using the link on the screen and in here I have prepared, first of all, you can find both my suggestion extension here and the fetcher. So if you don't want to go through me going through the stages of the suggestion, you can just find the full code here. But if you are going along with me, you need the prompt. So here it is, the suggestion prompt. It's not really any magical. I just kind of built it and changed it until it works. You can see later we also have another prompt here but for now this is the one we want. So let's just add suggestion prompt. You are a code suggestion assistant and what I'm using here is XML type of syntax because that works very well with Anthropic. I have no idea how well it works with Google. In fact I don't know what works well with Google providers, if you know, feel free to change this and alter it. Maybe ask Gemini to kind of modify this prompt for Gemini. But for Anthropic, this works great. It's basically instructing it what it is. Feel free to use this prompt for Gemini. I think it should work just fine. Now let's export asynchronous function post. We get a request. Let's go ahead and open try. and let's also prepare a catch here. So in here, I've started extracting some things and that's the following. File name, code, current line, previous lines, text before cursor, text after cursor, next lines and line number, all from await request.json. And if we don't have the code, which is the most important part of this request. We're simply going to go ahead and throw a next response JSON with an error. Code is required and pass along a status of 400, meaning whatever came from the front end is not good. We cannot work with that. Now what we have to do is we have to create a prompt from the suggestion prompt by replacing file name to be the actual file name. Then we have to replace code to be actual code. And then the exact same thing for all other things. Current line for current line. Previous lines for previous lines. Or fall back to an empty string. Text before cursor. Text after cursor. Next lines. Or fall back to this. And line to number. Make sure you transform it to string. So this is the prompt. Do you need all of these? Probably not. You can maybe just do, I don't know, text before cursor. The more context you give it, the better it's going to perform, obviously, right? But you can do fun things with just code and file name, right? But I think at least current line and text before cursor, I'd say like these ones are the most important ones. So otherwise, how does it know what to even suggest to you, right? and now that we have our prompt we can go ahead and create this output using await generate text model now in here it's going to be anthropic or it's going to be google and if it's going to be google you obviously have to give it whatever model you're using for example gemini 2.0 flash output.object schema suggestion schema and prompt and let's return next response.json suggestion so this is important right what you respond is important because that's what we're going to look at at the front end and simply pass in my apologies output.suggestion so you can see this is hard-coded.suggestion I mean this is typed.suggestion why because we gave it uh where is it we gave it an output dot object right so right here we have the suggestion schema which accepts a suggestion so when we pass that here suggestion schema it knows that's what it has to return so i'm not sure if this is the proper google model to use i will try with google first because i know a lot of you are using the free api key but then later i will switch to anthropic simply because the results are much better but i think we should get some fun results with google one as well. And let's also just catch the error, right? If we do have an error, let's console.error, suggestion error, and let's pass the error, and let's return next response.json with an error failed to generate suggestion. This will also get logged to sentry, so no worries. let's go ahead and end it like this. There we go. So that is our end point. We can comment out on tropic now. So that is our end point for suggestions. What we have to do now is we have to create a fetcher. So we can do this in a very, very simple way, but I just like some level of type safety when it comes to calling these things. So to make it easier and less error prone, I'm going to go inside of features, edit or extend,\nsuggestions, suggestion, and in here I'm going to create fetcher.ts. And in here, let's go ahead and install a package called ky. So ky is like a lightweight alternative to fetch and axios, maybe specifically axios. It's not an alternative to fetch. It probably definitely uses fetch inside but yes like a lightweight alternative to axios so let's go ahead and install import ky from ky and let's import z from zod and now what we're going to do is we're going to create the suggestion request schema using zod so what is this well remember how we had route.ts in here we had all of this so now we have to validate all of them here in this object so let's go ahead and create that file name is a string code is a string current line is a string previous lines text before cursor text after cursor next lines and line number which is a type of number and we also have to define what do we get back well a very simple suggestion which is a type of string. So let's define that. Suggestion response schema is an object with a string back. Now we can go ahead and infer from those ZOD definitions to create actual types. So z.infer type of suggestion request schema and suggestion response schema. And now we have nice types which we can use here. Now let's export const fetcher. It's an asynchronous method which accepts a payload which is a suggestion request and a signal which offers the option to abort signal we can use that in case the user starts typing again so we can easily abort the request we just started to create and thus we can save some tokens so we don't create unnecessary uh ai things right let's open a try and catch here and the first thing we're going to do is we're going to validate the payload. So validated payload is suggestion request schema.parse payload. So if whatever we pass to this fetcher function doesn't pass this, we're simply going to throw an error. And now let's go ahead and get the response by doing await ky.post forward slash api forward slash suggestion json validated payload passing the signal timeout is 10 000 and retry will be zero and let's go ahead and chain dot json and execute it and give json a type of suggestion response so we know exactly what we're getting back and then let's do const validated response suggestion response schema dot parse response. So we do another check. Did we get back what we expect to get back? And then finally, return validated response dot suggestion, like so. Or if we don't, we can get null, but I think we should always do it. You can leave it like this. It should be fine. So in the catch, the only thing that's important is to differentiate if the error is because we canceled it with the abort signal. So if we did, so if error is an instance of error and error name is abort error, don't do anything. Just return null. Otherwise, return null as well. Great. So now while I'm here, I also want to import toast from Sonar. We already have Sonar installed. It comes with chat CNUI. You can see 2.0.7. And I just want to throw an error here, toast.error failed to fetch AI completion. Simply so the user is aware this is not working. But in order for the toast to work, we have to quickly revisit, not the environment file, the app file layout file right here. And let's just add toaster from components UI sonar like this and let's just move it here that's it that's all we have to do great so that is the fetcher but now what we have to do is we have to implement stage three so let's go ahead inside of features editor extensions suggestion index.ts so what should we do first well we should import our fetcher so let's go ahead and go at the top here and import fetcher from dot slash fetcher right here once we have the fetcher we should start doing some changes so we no longer need the generate fake suggestion we can get rid of that but let me just add to do clean up this fake function because it is fake, right? It's not doing anything. It's pretending it's AI. And now we are going to have a proper AI method. So let's start by doing const generate payload. And that will accept a view, editor view, and a file name, which is a string. and now let's go ahead and start by getting the entire code so view state document to string that's the entire code there's a chance the code is completely empty so if there is no code or when we trim the code the length is zero return null no reason to make an api request over something that small now let's go ahead and define the cursor position using view state selection main head then let's go ahead and let's get the current line using view state document line at cursor position then let's go ahead and get cursor in the line which means cursor position minus the current line dot from now let's go ahead and get the last five previous lines so prepare an array like this so we're going to get five of them previous lines to fetch math minimum five and then current line number minus one. And then in here, we're going to do a simple for loop to achieve that. For let i be a previous lines to fetch, i is greater or equal than one, and simply go down to zero. I mean to one. And previous lines, and then for each line that you find, go ahead and push it to the previous lines array. So view state doc line, and we find it using current number minus the current iterator i dot text a bit complicated so pause and make sure you write it correctly and then we do a very very similar thing for next lines right so first let's find the total lines using view state document lines then let's go ahead and define which lines we should fetch next. So math min 5 total lines minus the current line dot number. And then again, we do a for loop here with the iterator starting from 1 being less or equal than lines to fetch and increasing. So then we do kind of the opposite thing. In here we went up and in here we go down. But the logic, I can see, highlights it's exactly the same. Except this is plus the iterator and this is going minus the iterator. and the logic is in reverse this is i minus minus and this is i plus plus so just be mindful of that you can always pause to double check or if you're just unsure you can always use the assets here to just see the complete suggestion extension how it looks in its final form i will share that with you again later and you also have the fetter just in case since i know this is a bit complicated and it's an important feature. I want you to have it working. So I'm going to make sure that you have the answer. All right. And now what we have to do is we have to return all of those things. So things like file name, code is simple, but then we get to a bit more complicated things. For example, current line will be current line dot text. Previous lines is an array. So we are using previous lines dot join with a page break like this so we are preparing this for ai consumption text before cursor is current line dot text slice starting from zero and going to cursor in line text after cursor is current line text slice cursor in line next lines in is an array again so we join it using page break and finally we have the line number which is current line dot number great so now let's go ahead inside of create debounce plugin and now after we do trigger suggestion here after we do this let's go ahead and do an abort controller so let me just see okay how we have to go right here let's go let's go above generate payload actually let me i'm trying to find the best place to do this i want to do it here with a debounce timer so just here go ahead and add let current abort controller to be a type of abort controller or null and default it to null like so and now let's go ahead let me see abort controller exists. Let's go ahead and use it down here in the create debounce plugin. So after a trigger suggestion, I started to leave some space here. So if current abort controller is not null, call current abort controller dot abort. It seems like I'm having some problem here. abort does not exist on type never so let me quickly check what that's about i think this is perfectly fine it's because we never actually give it a type of abort controller so it confused about what it has to abort and we do that here so this is a fake suggestion so we can delete this block and we are now in stage three so okay let delete this entirely and just leave this and instead we going to add payload using generate payload the method we defined above which accepts the view and the file name which we pass in here so we have access to view from trigger suggestion and file name from the created debounce plugin here. Awesome. So that's the first thing. And now we have to check if there is no payload. We have to switch is waiting for suggestion back to false. And we have to update our view dispatch and set suggestion effect of null. And then let's go ahead and do an early return and then outside of this if clause assign to the current abort controller to be new abort controller and then finally the new suggestion will come from await fetcher payload and passing the current abort controller dot signal here so basically it's going to accept two things, the payload, which we generate, and the current abort dot signal. And in here is waiting for suggestion is again false. The view dispatch is exactly the same. Very good. Okay, now what's missing is in the destroy method here. So we should also, besides doing the debounce check, we also have to check if current abort controller is not null. Let's make sure to abort it so we don't have any memory leaks. Great. So now I'm just checking, you know, is there anything important I've missed here? And I think this might be it. Let me see. What is the warning about here? Generate fake suggestion. Yeah, we no longer need it. We can remove generate fake suggestion. We can keep all of these close together. And let's check it out. So again, I have no idea if it's going to work the first try, simply because I've set up to use Gemini and I'm never familiar with their code. So let's try picking something like this, right? Or you can just copy one of your components. So if I just go ahead and add on something, let's see if it will give me any suggestion or it gives me an error. Okay, I think this is an error because of an invalid model. This happens almost every single time, but at least the toaster is working on failed to fetch a completion. So I'm going to go to aisdk.dev simply because I want to show you how I fix problems like this. I mean, this only happens with Gemini for me, so like no one else. Let me go ahead and try going inside of providers and I'm going to try and find where is google here it is google generative ai what did they use gemini 2.5 flash okay i don't know why this is like that so let's go inside of app folder api suggestion also make sure you, you know, you can see suggestion here and in here API suggestion. Make sure you didn't accidentally misspell the folder name. It should be inside of API and it should have route. So I'm going to change this to be 2.5 flash. Maybe we'll have more luck then. Let me refresh again. Let me go inside of this one. Let me write on change to and will it auto suggest something or not? There we go. It works. If I start writing const, let's see, will it think of something? Sometimes it can decide that, you know, nothing should be added here. Sometimes it just fails. Let me go ahead and see why it has failed. So the problem is, I never know. You can see it's telling me I have exceeded my current quota, but I'm almost 99% sure that's not the case. I just think it works funky. Sometimes it works, sometimes it doesn't. Let me try writing const on click here. Will it work or not? Looks like not, but let's simply check. If I go ahead inside of my route.ts suggestion, and if I change this to Anthropic, so, I mean, for you, Google might work perfectly fine. But for me, it's always funky. I don't know why. I mean, we are talking about their free tier, right? So that's obviously different. And yeah, for something like this, when it comes to Anthropic, you should kind of use one of the cheaper models like Haiku or something like that simply because this shouldn't be too expensive, right? I mean, in my original source code, it seems like I have used this, 3.7 Sonnet. Now, I'm not sure what specific version, right? But let me go ahead and refresh and see if this will work better. So I will delete this and do it again. Const on click. Let's see. There we go. You can see how Anthropic works. Very nice. Const on enter. Let's see. There we go. And if I move, it resets. That's what's important too. We have to test all of those things. And when I come back here, I can still accept. Very, very cool. Make sure your indentation is still working fine, right? Let's see if I go ahead and just add a new line here. Will it maybe suggest some extension here? And sometimes it just won't do anything. And that won't be an error. It just decides I shouldn't do anything. Let's see if I add a console here. will suggest maybe a log here it is cleaning up editor view a very relevant log so it completely understand the lines coming before and after our code so it definitely works uh i'm just not sure how do i make it work reliably with google i mean you saw for a second that it does work right so obviously something is working here but i just have so little awareness of like it's models that i don't know what's good and what's bad let's try gemini 2.5 bro i don't even know can i use this in free tier if i add on key down here this should be a relatively simple autocomplete but i don't know it seems like it's just not uh working like i like i i want to give you the option to use free uh ai models but they're just not reliable for me i don't know uh if maybe for you they are working i mean it's telling me i have exceeded my quota so i think that might also just mean that I'm using a Google model that should not be used for the free tier, but I'm not sure. I have no idea. If you have better knowledge of Google, you might solve this for yourself, right? Oh, there we go. That works. Let's try const on click. Let's see. Will it suggest something for me or we throw an error again you can see it does work obviously but i guess it's just the fact that it's free tier is not too powerful so it very easily hits some limits i guess but yeah it definitely works and it works quite well you can see how it suggested just a single line and nothing more than that but then very soon it starts to fail right so because of that I'm going to switch to Anthropic. Again, I understand a lot of you are not able to do that, but I'm not really sure what to do in this situation. Feel free to try other models. I mean, there's XAI, right? So you can import XAI, just have to install AI SDK, XAI. And I think they offer free tier two. There's DeepSeek, right? Feel free to research because any model can work with this. It's just that the free ones are not reliable when it comes to rate limiting. So that's why I'm using Anthropic to demonstrate and prove that our code is actually working, right? It's just the fact that the models that we choose will depend on the results. See, Anthropic works perfectly. Immediately throws something relevant. Excellent. So amazing, amazing job. So what can be improved here? One thing that immediately comes to mind is this. We are not doing any validation on the server, but we are doing it here in the fetcher, right? So let me see if it would be easy to maybe share these suggestion request schemas and then validate them. I mean, I don't know. We are validating them here in the fetcher. So we know that this JSON is pretty reliable, but it could be a good idea to also verify it here. so it shouldn't be too complicated to add that you know but for tutorial purposes i think this is just perfectly fine we do have a validation here obviously if you expect someone else to call this endpoint that's not your app well then you should probably uh you know limit this and validate it and things like that right but for a purpose like this where it's just our app it's fine right But obviously, you know, this API suggestion should only be hit if you're logged in. So that's one of the things we can actually do here. I think we can just do user ID await out from clerk next JS server like this. and then if there is no user ID, return next response dot JSON and pass in. Let's see, how do I return errors like this? Right. Just to make sure that no one, no malicious actors are trying to access this. Okay, can I do it like this? and this would be unauthorized with a 403 like that so now only logged in users can do this and then later when we enable billing we can also super easily check that the user is also on a pro plan right So no one can spend your tokens who isn't paying for your SaaS. So yes, that's one thing I would recommend doing. And since I'm logged in, I would expect being able to do this. Let's see. There we go. Still works perfectly fine. Awesome. Great. So let's see what we have to do next. So I think it makes the most sense to immediately implement the second custom suggestion we're going to have, which is the suggestion tooltip. So let's go ahead and start by going with the API route, because this time we won't be going through stages, right? We won't be doing any mock things. We're just going to go ahead outright and build it. So quick-edit and inside route.ts. So this time I'm going to be using Anthropic simply because I'm having problems with Google. But again, you can go ahead and import Google and use Google if you prefer. Besides that, we're also going to import Firecrawl using at lib Firecrawl. Let's go ahead and define the quick edit schema, which is going to be a ZOD object and it accepts edited code. the edited version of the selected code based on the instruction. So this is a bit different because the user will directly select and highlight a line of code it wants to modify. Let's define the URL regex. You can google this if you don't know which one it is or you can pause the screen and now we need to define the quick edit prompt. So you can head to my Polaris Polaris suggestions, my apologies, Polaris assets, and you can find prompts.ts. And in here we have suggestion prompt and here we have quick edit prompt. So let me go ahead and copy it. Now let's go ahead and let's paste it. So here it is, quick edit prompt. You are a code editing assistant. Edit the selected code based on the user's instruction. Return only the edited version of the selected code. maintain the same indentation level as original. Do not include any explanations or comments unless requested. If the instruction is unclear or can't be applied, return the original code unchanged. All right, and now let's export asynchronous function post, accept a request, and let's open a try and catch. So what we're going to The structure here from the request.json is simpler. Selected code, the full code for the context, and the instruction. And let's check if there is no selected code. Simply throw selected code is required. If there is no instruction, instruction is required. And while we are here, we can also do user ID from await out from clerk next JS server. Let me go ahead and just move this here. I like to order them by length. And now in here, I'm also going to do the exact same check. But just if there is no user ID, unauthorized. All right, so now that we have checks for all of those things, let's see if the user gave us any URLs. URLs is going to be an array of strings and we're simply going to go over the instruction the user gave us and match it using the URL regex which we've defined up here. Otherwise, fall back to an empty array. And then let's go ahead and define the documentation context. So if the URL's length is larger than zero, let's go ahead and scrape the results. So const scraped results await promise all URLs dot map asynchronous. Get the individual URL. Open a try and catch block. and in here attempt to get the result using await fire crawl scrape url in the markdown format if result dot markdown return and then the syntax i'm going to return here is very specific again works very well for claude for anthropic it's in format of xml if you want to you can literally just return result.markdown. But this will kind of make it serve better as context when it comes to when it comes to, I'm not sure what I'm trying to explain, understanding that this is an additional documentation, something that was scraped, right? This exact format, like a XML syntax, this is a document, this is the URL, and this is the result of the URL. But again, you can just return result.markdown. Otherwise, we are returning null. And in the catch, we are returning null as well. And then let's go ahead and only grab the valid results from that. So let's see. Scrape the results.scrape the results.filter boolean. They now turn into valid results. And then we're going to push all of these if they are more than zero. We're going to add documentation context to be the following. Inside of the documentation XML tag, we're going to add a page break. And then we're going to join an array of these using more page breaks. So basically, it's going to be like documentation and documentation. and in here URL nextjs.org proxy.ts right and then in here blah blah blah what is proxy and doc and just a bunch of doc URLs that's how the result is going to look like. So again you don't have to use XML but it works very well with clause so if we have any valid results we simply append the documentation context with that XML tag and we join all the valid results using double page break here and we end with another page break in here to format it nice for the AI. Awesome. So now, outside of this, if URL's length is larger than zero, let's go ahead and modify our prompt. So that's going to be quick edit prompt and let's start replacing some things. So the first thing we're going to replace is selected code with the variable selected code. Then full code or an empty string. Then instruction with instruction. And finally documentation with the documentation context. And then let's go ahead and define the output using a way to generate text. Again I'm using Anthropic and Claude model. You can use Google here. Just make sure to use proper Google model. You can use XAI, whatever you want. I'm using Anthropic because it's very reliable for me. And now I'm just going to go ahead and return next response dot JSON edited code output dot edited code. And you can see that is the edited version of the selected code based on the instruction which we have defined in the quick edit schema and in the cache here we're just going to go ahead and throw some errors so make sure to grab an error here and go ahead and just throw like this edit error and next response that json failed to generate edit awesome so that is forward slash quick edit now what i would like to do is i would like to go inside of projects my apologies inside of features editor extensions i'm going to copy and paste suggestion and i'm going to rename this to quick dash edit and i'm going to go inside of fetcher here and just modify it so it works for fetcher so it's not going to be called suggestion request schema it's going to be called edit request schema with selected code full code and instruction. And for the edit response schema, we're just going to have edited code z.string. That's also going to change these two types. It's going to be edit request type of edit request schema and edit response type of edit response schema. So let's see, this will now be edit request and this will now be edit request schema like that. The API endpoint will go to quick dash edit. We can increase the timeout to 30,000 because we allow this one to think longer. And let's use edit response here. And let's use edit response schema. And this is edited code or null. and this will be fail to fetch ai quick edit there we go so that's a very quick fetcher modified and now we go to the extension of the quick edit so the extension of the quick edit is a bit different let's go all the way down here i i personally think it's a bit simpler so don't worry and we won't go through all the stages we're just going to build it outright. So export const quick edit. We're actually not going to be using the file name, but maybe that's not such a good idea. Maybe we should use the file name. For now, I'm just going to leave it here as a prop, but I'm not going to use it anywhere. So the first thing we're going to do, we can remove all this, is going to be quick edit state. So I'm just going to remove everything up to the state. That's the only thing I'm going to leave. There we go. We obviously are going to still use the fetcher and I'm just going to remove all the comments simply because I think I removed something other than a comment right because we explained how extensions work in the previous one and I think now we can just focus on building one so quick edit state let's go ahead and start with setting the effect so it not going to be set suggestion effect it going to be show quick edit effect state effect define Boolean Then let go ahead and let define a few things here Editor view to be a type of editor view or null And the current abort controller to be abort controller or null right here. I'm going to make it easier and just delete everything like this. So let's go ahead and define the quick edit state. The quickEditState will be a stateField.define, which accepts a Boolean. And we're going to start with a simple create method in which we are going to return false. We're then going to create an update method, which has a value and transaction. And let's go ahead and search for const effect of transaction.effects. If effect is showQuickEditEffect, return effect.value. And then after that for loop, if transaction.selection exists, let's go ahead and get the selection. And if the selection is empty, return false. So if the user didn't select anything, return false. Otherwise, just return the value. Great. So now our quick edit state is finished. So now what I want to do is I want to add quick edit tooltip field. Let's go ahead and build this one next. I'm just trying to see. Okay, yes. So in order to build that, we have to define the following function. Create quick edit tooltip, which accepts a state of editor state and returns read only tooltip, I mean an array of tooltips. And let's start with getting our selection. If the selection is empty, return an empty array. Then let's go ahead and see is quick edit active, right? So that's why we are returning false here, because by default, it's not active. That's how this is going to serve. And if it is not, again, return an empty array. Otherwise, return an array and an object inside. And now in here, the position is select dot two, above is going to be false, strict side is going to be false, and then we're going to call the create method. the create method will create a div element we're then going to give that a class name so we're just styling it now so this class name right here that you're seeing let me show you how that's going to look like so it's easier so if i switch to localhost 3005 here this is my finished product here so when i select something and click quick edit this is what we're building so this is cancel submit this is the div the input right we currently do not have that if i select something nothing happens and if i press command k nothing happens so that's what we're doing now now after that dom class name we need to define a form element and we need to give that form some class names flex flex call and gap 2 then we're creating our input element where the user will actually type what they want we are going to give it a type of text placeholder of edit selected code let's go ahead and give it a class name of background transparent border noun none outline none padding on x axis 2 on y axis 1 font sense and a width of 100 then out of focus on true. Then let's create a button container, which is going to hold our two buttons. So another div with flex item center justify between gap two. Fun fact, this is how we build components before react. So now let's go ahead and do a cancel button here. And let's go ahead and give it some properties type is button text content is canceled. Now let's go ahead and give it some class name font sense, padding one, px2, text muted foreground, hover text foreground, on hover bg foreground with the 10% opacity, and rounded small. Now let's go ahead and define what happens when we click the cancel button. So when we click the cancel button, if we have the abort controller, let's make sure to abort and return back to null. Otherwise, I mean still, if we have editor view, let's call editor view dot dispatch effects show quick edits off and set it back to false. So now let's go ahead and do very similar to submit method. I mean submit button. Submit button is another button. Submit button type is submit. Submit button text content is submit. Let's go ahead and add a class name font sense padding one px2 text muted foreground. basically I think identical to this one. I don't think there are any differences. And this one won't have its own onClick because this is a type of submit. So instead we are going to access our form element on submit. And the first thing we're going to do is make sure to prevent default so it doesn't refresh the page. In case we cannot access the editor view we're going to break the method. Then we're going to attempt to trim the instruction and if it is not available after the trim, it means it's empty. So we return it. Then we are going to go ahead and get the selection from the editor view state selection main. We are going to get the selected code using again editor view state document slice string selection from selection to. These errors are fine. We're going to fix them later. Then let's add the full code editor view state document to string. Then let's go ahead and make sure that while this is submitting, we set the submit button to disabled and change the content to editing. Let's go ahead and assign the new abort controller here. And finally, we can go ahead and get edited code using our fetcher. In the first argument, we send the payload, which is selected code, full code, and instruction. And in the second argument, we send the abort controller signal so that we can abort if user changes its mind. So now let's go ahead and open if the edited code was received. We now have to call editor view dispatch and we have to modify it. So we first have to define where. Changes from selection from to selection to insert what the edited code we just got back from the API. then let's go ahead and make sure to modify the selection so we move it to the end of the edited code and return this state to false so it's no longer opened like that else if edited code was not received we're simply going to make sure that we reset submit button disabled back to false and this back to submit which most likely means something went wrong and finally current abort controller is null. And now what we have to do is we have to append these things to button container. So cancel button and submit button. And then we have to append all of those things to form. So append the input and append the button container. And finally, append the form to the DOM. And to make out of focus work, let's do a simple set timeout trick. And then in the end, let's return DOM like this quite a long function but a very useful one I mean most of it was just building the UI this would be way easier to write if we had access to JSX and React but we are in this environment where we have to write code this way but yeah this is how people wrote before React actually alright so we have that now we can define quick edit tooltip field so const quick edit tooltip field state field define read only tooltip and then an array like this let's go ahead and add create here and let me just see okay the tooltip needs to be imported yeah let me fix all the imports actually so from codemeter view we're going to need tooltip show tooltip keymap and editor view and from state we're going to need state field editor state and state effect and then the fetcher okay let's go down here where we started building the quick edit tooltip field so we have create here now let's create an update which accepts tooltips and transaction and in here if transaction document changed or transaction selection return create new quick edit tooltip basically we need to define where to render the tooltip to render to allow user to write something. We can't just render it anywhere. We need to be aware. Is the cursor here? Is the selection here? That's what this update is doing. It's kind of like doing the visual thing. And then outside of this if clause, do a for loop. So again, searching for an effect in all of our transaction effects. If we find one with show quick edit effect, we return again, create quick edit tooltip with the current transaction state. And let's finally go ahead and return tooltips. And then in here, let's add provide field and return show tooltip compute n. First argument is an array of field. Second argument is a function which accepts state and returns state dot field and passes in the field. Like that. Great. And now there are a couple of more we have to implement. so they're just easier than all of these so the next one is quick edit key map this will basically allow us to use a shortcut to open this so quick edit key map here open an array insert an object here the key will be mod and then the letter k and what that's going to do is it's going to run something with the view so we get the current selection\nwhere it happened. If the selection was empty, there's nothing we can do. But if it isn't, we call view.dispatch effects show quick edit effects off and pass true. And then we return true simply to override if mod k was any other method already. Great. So that was a simple one. and the last one we need is capture uh the last one we need is capture view extension this one is the simplest capture view extension editor view update listener off update editor view update view i think this just refreshes the uh state like when we type and things i will double check i'm not 100 sure uh it is my first time building code minore extensions too so i'm trying my best with that first one to go through stages so we both understand how they work from primitives but with this one i think you know we can just go ahead and build it uh okay i think yeah and you can see all errors actually went away as we built so all of this should be fine file name is expected to be like that here so now let's go inside of our components code editor here and after suggestion pass the quick edit from extensions quick edit and simply pass the file name here too even though we don't use that so now what you should be able to do is select the thing like this using control key or command key should open this edit selected code and i should be able to say rename this to folder name. And I should be able to press enter and submit and it was just successfully renamed it to folder name. So if yours is not working, make sure to try control key, command key, you know, whatever your action key is, whatever your modulose key is, mod key is. You can also manually change this. If you want to just try it out, you can try, I don't know, the letter B. I think this might work. So if I highlight this and do Shift-B, it opens. Shift-B, try that. Or if I use the lowercase letter B, I think it's then going to pop up every time I press letter B. Yes. So every time I press the letter B, it opens. So let's bring it back to mod K. And now to wrap this up, there is actually one more thing we have to do. I mean, you might be satisfied with this as it is, but it might be like cool to have, let me go ahead and show you, it might be cool to have an ability that when you highlight in general, it shows you the option to quick edit, right? So let's go ahead and implement that. This one actually doesn't include any complicated logic. It's just tedious to write the components that way. but let's go ahead and do it so one thing i already know we're going to have to do is inside of the extensions inside of quick edit here index we're going to have to export i think this one i think we're going to have to export quick edit state and also yeah so export show quick edit effect and export quick state make sure you export both of these two. And now we're going to go ahead inside of extensions and we're going to create a new one. And this one, actually no need for a folder. This one is simpler. So we can just call it selection-tooltip.ts. So selection tooltip. And let's go ahead and start with export const selection tooltip, which will accept selection tooltip field and capture view extension. we can go ahead and immediately fix the capture view extension it's editor view update listener of update editor view update view the exact same one we had from before i mean in this one right if you scroll down here you will see at least i think it's exactly the same yeah so for the imports of selection tooltip let's go ahead and import tooltip show tooltip and editor view from code mirror view, state field and editor state from code mirror state and show quick edit and quick edit state from quick edit. And now let's go ahead and start developing this, right? So I'm going to define editor view to be editor view or null. And then I'm going to go ahead and create a function, create tooltip for selection. It's going to accept state and it's going to return a read-only array of tooltips. We're going to start with a selection and if selection is empty we're going to return an empty array. Then let's check if quick edit is active like this. If it is active we're going to return an empty array. Otherwise we're going to go ahead and build UI. So let's go ahead and again start with the position which is selection two above false strict side false and then we go ahead and build the create so again we start with the dom right we then go ahead and give the dom class name bg popover text popover foreground z50 rounded small border border input padding one shadow medium flex item center gap two and text small we then go ahead and start with add to chat button which we will see how we will implement this functionality later but for now it's just going to be visual so let me just properly indent this there we go so add to chat button is a button with text content add to chat and class name here and then we have the familiar one quick edit button again document create element button let's go ahead and give it the following class name feel free to pause the screen to copy the class names now let's go ahead and give the quick edit button the span element quick edit so text content is quick edit let's go ahead and create a shortcut element so quick edit button shortcut is another span element the text content is again this is my mac os control sign right so command k text small opacity 60 you can also write you know control plus k here whatever you want and now let's go ahead and let's append these elements quick edit button append child quick edit button text and append child quick edit button shortcut and now on quick edit button on click if we have the editor view simply dispatch and change the state of show quick edit effect to be true and that will then trigger this what we've just built previously we did this entire thing before right all right and then we just have to append all of these to the DOM, add to chat button and quick edit button, and finally return DOM. Great. So what's left is the selection tooltip field. So let's go ahead and develop that here. Selection tooltip field, state field, define a read-only tooltip. Let's go ahead and call create in which we are going to return tooltip for selection and pass the state along. As always, we're going to have an update function, which accepts the tooltip and the transaction. If transactions document has changed and transaction selection return again, create tooltip for selection. You can see this is a pattern, right? We keep doing this right here, right? So we are, no, okay, this is the state, not that one. Let me scroll down here. There we go. See, every time kind of the document changes, we have to recalculate where we are going to build the UI. Outside of the if clause here, let's do a for loop. For const effect of transaction effects. If effect is show quick effect, show quick edit effect. Again, go ahead and build it. And then right here, return tooltips. And then let's add provide again, which is a field. show tooltip compute n field in array as the first argument state state field field as the second argument there we go and seems like no errors here so i believe this should work just fine so now if you go inside of your components code editor right here and if you just add selection tooltip from extensions selection tooltip and let's execute that. It should work. So now when I highlight, let me just confirm I am on my real-time project. Okay, it's localhost 3000. We are getting so close to the finished project that I don't even know which one is the one. And there we go. Add to chat does nothing but quick edit triggers this. Rename props to globe props. For example, let's see there we go amazing amazing job we implemented so many amazing features we have shadow text we have a minimap amazing so that is 13 files and now it's time to get code rabbit to review all of this right so i'm just gonna go ahead and shut down all of my terminals here. Let's see chapter 11. So git add dot git commit 11 AI features. Git checkout dash b 11 AI features. Git push u origin 11 AI features. And then let's go ahead and open a pull request. A lot of changes. I'm very interested in the amount of files we have created this time. I mean, the lines of code, but less than the previous pull request. But still, let's go ahead and review it. So what we've added in this chapter are AI quick edit functionality with keyboard shortcuts intelligent code suggestions displayed as ghost text while typing selection action tooltip for selected code and we did some improvements. We enabled text selection throughout the editor by removing select none from the body. We added toast notifications for user feedback. And we do have quite some comments here, so 10 comments. Some of them we already are aware of, like the missing Zod schema before the structuring, which I told you you can do of course but you know for tutorial purposes I'm going to keep it this way but yes obviously a good comment by CodeRabbit. In here I forgot to change to 401 or 403. It definitely shouldn't be status code 400 if I'm throwing unauthorized but still not anything that will break the app right. In here we are not using FireCrawl's timeout feature so yes if a single URL hangs indefinitely, it blocks the entire promise all, causing the request to time out at the client's 30 second limit, resulting in a poor user experience. So we could definitely add a time out to each of the requests here to improve that. Then in here, we have a very interesting comment. Yeah, this is quite fragile. Simply because if the user literally types in things like this, inside of their instruction, we are going to replace it, right? So yeah, it's kind of brittle, but I mean, for most use cases I've tried, it works correctly, but still important for you to know if any input field like file name code or current line contains a placeholder like this one, which we have defined, the replacement will incorrectly modify the user's actual content corrupting the prompt. Though I'm not sure, since we are directly modifying the constant here. So I'm not sure how this can modify our... Oh, maybe if it is inside of here somewhere. Not sure. Okay, but as I said, for most use cases, I think this is fine. And now this is interesting. Yeah, so in a couple of places, it's telling us about this module-level mutable state which can cause issues with multiple editor instances. So right now, not an issue because we don't have multiple editor instances. But if you in the future plan to implement this, you could probably use a different architecture to not use these global definitions of editor view and abort controllers. So in here, it recommends using a weak map keyed by the editor view, storing state in a custom state field using editor view facets. So that's one of the solutions here. in here actually this is a new tailwind utility in version 4 no documentation is present but the class name works so let's make sure code rabbit learns that in here it noticed the unused file name parameter which we are aware of we will see if we will do something non-functional add to chat button same thing same a comment as before so global state shared across multiple editor instances right so it's basically telling us that this can cause race conditions if multiple editors are ever a feature incorrect cancellation wrong ui state right basically a bunch of problems so we need to make sure that each editor instance has its own isolated state i will see if this is something that we can easily fix maybe in the next chapter or so you can see it's using yeah like scoping it to this instead of just using yeah interesting and definitely a good comment here but as I said for our use case this is okay and it kind of says that down here if you read it so the current situation is okay but if multiple editor instances exist simultaneously it will cause a problem. While the current UI architecture only renders a single editor at a time, the implementation is fragile. So if a split view or multi-editor feature is added in the future, these globals would cause race conditions and unexpected behavior. So I didn't really build this with multi-editor feature or split view in mind, but it's good that you are aware of this. So this is why it's always good to have someone review the code. I will see how easy it is to fix this, I mean, judging by what they've recommended me here, it's not too difficult, really, looks like there are some built-in mechanisms to solve this, maybe this can be a good challenge for you if you want to go even further, but for our use case, this is perfectly fine, I didn't encounter any problems at all. Great, ironic saying that I just received a failed fetch here, but that is from a different project that's from the finished project so yes okay uh amazing so i'm gonna go ahead and merge this pull request we are aware of those global shared instances and i will you know review that just to know check if it's not something that will cause problems in our current implementation but i highly doubt it will great now that we've done that let's go ahead and get checkout main git pool origin main right here and once we've done that we can always double check we are on the main branch let's go ahead and open the graph here it is we detached 11 and merged it back here so in this chapter we have successfully implemented ghost text suggestion handle tab key suggestion acceptance let me go ahead and mark it implemented mod k quick edit model firewall scraping functions, and all other selection-based code editing features. Amazing, amazing job, and see you in the next chapter. In this chapter, we're going to build the conversation system. We're going to learn how to store conversations and messages, build a chat sidebar with user and assistant messages, create past conversations history dialogue, handle message sending and cancellation functionality, display the thinking indicator while processing, and many other things. Let's take a look. So this is the final product and take a look that when I send a message for example hey how are you the chat turns into processing state. So that's what we're going to implement. You can see it's now thinking and I have an option to cancel this and then I get back an answer. We're not exactly going to be focusing on how the AI model works nor how it response. We are more going to focus on the system architecture behind storing the conversation and the messages itself. So we will also have this history dialogue where we will be able to go in the past. We will be able to of course cancel a message like this. You can see I have canceled my request so we spare tokens and we will be able to create new conversations. So that's the goal. we will see if the actual AI responses is something we can build in this chapter or if it's logical to move that into its own chapter so for now we're just going to focus on making the UI of the chat sidebar as well as the actual back end of maintaining storing and displaying those messages so let's go ahead and do npm run dev in our project here and let's refresh local host 3000. So right now our conversation sidebar is completely empty and that's perfectly fine because I'm not going to build a UI yet. I want to start with the back end. I feel like it's always easier that way. Let's go inside of convex schema and let's build the conversations table. So make sure you also have npx convex dev running as we will need to synchronize our new schema here. so I'm going to go ahead and define the table for conversations. Each conversation will have its project stored in project ID and then it's going to have a title which is a string created at which is not needed. Creation time exists by itself but it will have updated at and we're only going to have one index which is going to be by project. Now, besides conversations, we're also going to need messages. So let's go ahead and define the messages table. Each message will have to belong to a certain conversation, meaning it's going to need to have a conversation ID. And for easier traversing through messages, and right now, if I want to find out if this message belongs to a project ID, I will first have to fetch the conversation and then fetch the project. similarly to what we had to do with our files a lot of times, specifically for the owner ID, right? So to avoid that, let's add the project ID directly to messages because it's going to be very useful later. So for the role, the message can either belong to a user or an assistant. So we are creating a union of those two literals. Then for the content, it's just going to be a string. And then for the status, it will be, again, an optional union of three states, processing, completed, or canceled. And let's go ahead and keep it at that. That is good. Now we're going to have to add two indexes. So right here, I'm going to chain by conversation, which is basically using the conversation ID, and by project and status which will use project id and status this will help us query the messages faster great so we now have messages and conversations let's go here and confirm all functions are ready table indexes have been added and no errors have been found perfect now that we have this let's go ahead and new file here conversations.ts and inside of here we're going to go ahead and import v from convex values mutation and query from generated server and verify out from dot slash out we're going to start with a very simple create mutation so let's go ahead and give it an arguments of project id and title and let's go ahead and give it a handler i'm just trying to close this function so we get rid of the syntax error there we go. So the handler needs access to context and arguments so that we can properly get our identity using verify out We are then going to get the project using arguments project ID And as always let do a quick check If there is no project project is not found If the project owner ID doesn match the identity subject this is unauthorized and we shouldn't allow them to do that. And now let's go ahead and build this by using await context database insert into conversations project ID title. And the only thing we're going to need is updated at which can be date dot now that's it and the conversation id here is what convex context dot database dot insert will return this is actually quite important so make sure that you return the conversation id because we are going to be using it later on the front end when we invoke this mutation we are going to be using it to immediately select that as the active conversation So that's why it's important to return the newly created conversation ID. Because usually in mutations, we just do this, right? So just make sure you return it this time. And then besides creation, we're going to have some quite normal functions such as getById. So getById will have the argument's ID, which is belonging to a conversation's ID. and then as usual we're going to go ahead and get the identity we're going to get the conversation itself from context database get conversations arguments id if there is no conversation we're going to throw an error otherwise we're going to check we're going to try to fetch the project using the conversation project id if the project doesn't exist we throw an error otherwise we throw unauthorized to access this project if the owner ID is different than the currently logged in identity subject. And last thing, return the conversation. So if it passes all of these checks, it means, okay, we can definitely access this conversation. Perfect. So nothing we haven't built yet, right? So now let's go ahead and prepare this again. So I keep copying this because it's always the same code, right? For the getByProject, obviously, we are accepting project ID. And in here, we are going to fetch all conversations that belong to that project. So we start with our identity and we grab the project using arguments project ID. We do our usual checks. If there is no project, we throw an error. If project's owner ID is different, we throw an error as well. And then we're going to do a simple query using our by project index, right? So we have defined by project index here. So now we can do return await context database query conversations with index by project query equals project ID arguments project ID order by descending. So the newest one r on top and collect all the data. Excellent. So I'm going to start copying entire functions at this point. You can do that too. The next one will be get messages. And we're going to get all messages in a conversation. So change the argument to be by conversation ID. So instead of fetching the project, we can only fetch the conversation right now. So const conversation, context, database get conversations using arguments dot conversation id. Then let's do a usual check. If there is no conversation, let's throw an error, conversation not found. And instead of using arguments dot project id, we're going to do conversation dot project id. And the rest of the checks are exactly the same. And now we have to modify the query, right? But again, the query is quite easy because we have an index already. So inside of our schema, you can see we have by conversation. So we are simply going to query the messages and use the by conversation index. Let's go ahead and do that here. Very simple. Query messages with index by conversation using the conversation ID, arguments conversation ID, order by ascending simply because the messages are loaded in reverse and collect all the information. There we go. We just built the entire backend for conversation and messages. So now let's go ahead and immediately create our features for conversations. So features, conversations, let me fix the typo here, conversations, and let's go ahead and create hooks. And inside of here, use-conversations.ts. So I'm going to start by importing useMutation and useQuery. I'm going to import API from convex generated API and ID from data model. Let's start with a very simple use conversation which accepts the ID of conversations or null and it returns use query and calls our newly created API conversations get by ID. In case you're getting an error here it means the conversations functions have not properly registered. Make sure you have created them within the convex folder and make sure you have no errors inside of this. If you do, let's go ahead and do npx convex dev again and you will see the most latest error message appear here. For example, you can see I have none, meaning the convex functions are ready and synchronized. Your error will be written here in case you have any and then you can debug. So just make sure you have conversations inside of the convex folder. And then you should be able to query api.conversations.getById. It's important to allow id to be null so we can skip this query if we want to do so. Then let's go ahead and do the very same to use messages. So use messages hook again accepts conversation id which can be id conversations or null and it returns a use query api conversations get messages and again checks for the conversation id. Then let's go ahead and create use conversations hook which will simply load all conversations for a project so get by project we are calling it use conversations because it makes more semantic sense to be used that way when it comes to our ui blocks so use query is using api conversations get by project that's important and then let's go ahead and create another one use create conversation and let me just go ahead and close this. There we go. Use create conversation will not accept any props actually and we're just going to go ahead and return use mutation API conversations create and I will add to do add optimistic mutation here simply because it's not required but it makes a much nicer user experience when it's instantly created, right? We actually have a lot of mutations where we have to add optimistic mutation but once you learn how to add them in one place it's almost identical in every other place. Perfect. So those are all the hooks we need to have. And now we are ready to revisit our features, projects, components, and I think it is project ID view. In here, let's see, I don't actually think it's ID view. I think it's ID layout right here. You can see we have a div conversation sidebar. So let's go ahead and this time render an actual component conversations sidebar, conversation sidebar like this. Passing the project ID and let's go ahead and create it. So I'm going to create the conversation sidebar inside of conversations here. So I'm going to create components and then I will create conversation-sidebar.tsx. Let's go ahead and quickly create our props. So conversation-sidebar props accepts project ID. And let's go ahead and return, I mean, export conversation-sidebar function. In here, I'm just going to return a div conversation-sidebar. There we go. Now let's go back to project ID layout and let's import conversation sidebar from features let me just move it here and let's also do one more thing we can move this allotment this style CSS away from here and we can add it to our route layouts so this way wherever we decide to use it it's added maybe it should be before globals, maybe after, I'm not sure. Just make sure it works, right? So nothing should really be changed here. You can see that the only thing has changed is the text here. I think I added an exclamation point now. Great. So how do we develop the conversation sidebar? There's so many elements we have to add. Well, we are in luck because AI SDK, yes, the toolkit we actually used to add providers and various things, has a thing called AI elements. Using the link on the screen, you can visit that or you can just see the command you have to run. So they actually offer you two ways of adding this to your project. You can use npx ai elements or chatcn cli. Since we're using chatcn, I'm just going to use chatcn. So what that's going to do is going to add a bunch of elements to our project inside of components here. So let's go ahead and run that in our project. Chatcn, well, okay I'm going to use the latest for now if you want to you can use the exact same version you did at the beginning of the tutorial and it's just going to add all AI elements so this is using chatsy and registry so they are kind of verified components they're not just the random components so you have to be scared of what's being added to your project so there's quite a lot of them so maybe this will take a moment so let's just go ahead and wait for it to install if you're being ask if you want to overwrite the button, select no, simply because we added some custom changes to that button. So it's detecting that it already exists, but we don't have to overwrite it. So select no. If you select it, yes, it's not a big problem. You can always just revert that using git or you can just copy the button like command Z to what it was before. Basically, we added icon extra small variant and the highlight variant to our button. But you can see besides that, everything else is fine. Let's take a look at all the components that were added. So you can find all of them in one place AI elements Keep in mind that some of them might be a little bit buggy when it comes to type errors I noticed that myself but most of them should be completely fine The reason I telling you that they might be buggy is because this will maybe cause build errors when you try to deploy, right? So just be mindful of that. But we're not going to concern ourselves with that right now. We're going to go ahead and build. So you can see I have 40 unsaved changes here. and you can see besides layout, schema, conversations, and some generated files here, all of other ones are source components, AI elements, right? And use conversation, conversation cyber. Okay. So you should have, well, you should have the exact amount of changes, but you know, maybe you did something that I didn't, but yeah, that's how many files have been added. Also, maybe in the future, more files or less files will be added. So yes, don't take this number too seriously. Let's go ahead now and go back inside of the conversation dash sidebar. So that's inside of features, conversations, components, conversation sidebar. And in here, I want to start importing some things. So we have ID imported, but now I'm going to add from components, AI elements, conversation, conversation, content, and conversation scroll button. Then I'm going to go ahead and add message, message content, response, actions, and actions, and action from AI elements message. After that, I'm going to import prompt input, body, footer, submit, text area, tools, and type prompt input message from AI elements prompt input. So I'm going to remove all the gaps between the imports now because we know what they are. And since I'm wrapping up the imports, I'm just going to add all of them. So besides this, I will also have ky package, toast from Sonor, use state from React, and from Lucid icon, I'm going to have copy icon, history icon, loader icon, and plus icon. All right. And let's see, I don't think we need to use client here because it already is within a client component. Let's go ahead and add button from components UI button. And I think that that's it for components. Now let's go ahead and import all hooks from use conversations. Use conversation, use conversations, use create conversation, and use messages. So this is a hook we've developed before we started building the conversation sidebar. So you should have all of these here. Great. Let's see. What should I do next? Inside of convex, I'm going to create a constants.ts. And I'm going to prepare the following default conversation title, I'm going to make it new conversation. That's what I'm going to do. So just save that and nothing more. All right. and now I'm going to go ahead and build the UI so conversations dash sidebar conversation that sidebar and we're going to start by giving this div a class name flex flex column height full and background sidebar we're going to open a new div here with the class name height of 35 pixels and we've already learned we can write that as 8.75. Maybe I will learn at the end of the tutorial. Flex items center justify between border bottom and that's it. And another div inside in which we are going to simply render the default conversation title which we can import from convex constants. So default conversation title. And this will have a class name, text small, truncate, and PL of three. Let's go ahead and start actually taking a look here. Oops, this is the final product, not that, this. so now you can see i have a text which says new conversation at the top i'm going to snap this and i'm going to expand this so i can focus on this as much as possible so there we go new conversation is now written at the top because that is right here the constant we are exporting great and below this let's open a new div with a class name flex items center px1 and gap1 and in here we're going to render a button this button will be a history icon renderer like so again i'm just going to snap this, expand this. I can focus on that. And let's start giving this button some props. So it will have size icon extra small and the variant of highlight. And the history icon will have a class name size 3.5. So that's what it should look like. Then I'm going to duplicate that. and the bottom one is going to have a plus icon from lucid react and then after that we are let's see um let's keep it like that okay so yes we're gonna have two of them this is kind of the the history and this is the plus button. This creates the new one. So now, outside of this div right here, in the place of the text, let's add the conversation component. Let's give it a class name flex1 and inside let's add conversation content. and then inside of here we have to basically render the messages so how do we render the messages well before we can do that we have to display the the prompt input that will allow us to create new messages right so the only thing we also have to add within the conversation composition is conversation scroll button and it's a self-closing tag like this and then outside of conversation create a div with a class name padding three and in here we're going to add prompt input like so the prompt input will have on submit and it's just going to be an empty arrow function and it will also have a class name margin top 2 rounded full let's go ahead and add this actually this rounded full doesn't do anything I think when I save I think it's exactly the same besides that let's also prepare value of empty can I do that? Okay. That's not where I do that. Instead, inside of prompt input, we render prompt input body. And then I do prompt input text area. And this is where I can add a placeholder, such as ask Polaris anything. Let me go ahead and expand this a bit. let me see if I did the composition correctly we have prompt input prompt input body and then prompt input text area okay we are here also going to have on change which for now is just going to be an empty arrow function value which is going to be an empty string and disabled which will be explicitly false and this is actually a self-closing tab there we go then within outside of prompt input body we will add prompt input footer and prompt input tools which is a self-closing tag simply because usually you can open it and put things inside but in this case I'm just using it to fill the area, the empty area. And prompt input submit is also a self-closing one and you can leave it as the default but for now let's just go ahead and make sure disable this false and status is ready simply so we are aware that it can have different props here. Okay, so we just developed this. Now let's go ahead and let's create a function that will help us create new conversations. So I'm going to go ahead and add our use create conversation. So const create conversation, conversation use create conversation hook. and then I will also prepare use state here something called selected conversation id and set selected conversation id and by default I'm going to make it null so since these are long words I'm just going to collapse the state like this and the type will be basically an ID of conversations or null. Let's go ahead and keep that. Okay. And now let's go ahead and implement constant handle create conversation. It's going to be an asynchronous method. we're going to open a try and catch let's go ahead and grab the conversation id from await create conversation inside of create conversation let me go ahead and expand this a little bit we're going to pass in the project id and the title which will be default conversation title there we go and then immediately after it has been created set selected conversation id to be the new conversation ID. Let's call this new conversation ID and then we can pass that here. There we go. And return new conversation ID from this method as well.\nIn the catch, we can just do toast error, unable to create new conversation and return now. All right, so that's a method handle create conversation. And that's why I was telling you that it is important that in API conversations create, we return the conversation ID because this is where we are using it, right? we need it here so we can set the selected conversation id to the newest one all right so let's go ahead and now add this to our button which has the plus icon so in here i'm just going to do on click handle create conversation so the ui actually won't be too noticeable right now so i would rather we go to dashboard convex.dev and head into our project database in here you can see I have conversations and the table is completely empty so if I go ahead and click on the plus button right here as I said nothing much changes here but you can see something obviously changes here and to prove that in the UI 2 let's go ahead and go up here where we render the default conversation title and do the following. Try and load active conversation. Actually, let's see. We don't have... Okay. One thing I forgot. I thought I could do it from this, but I can't. So now that we have selected conversation ID, what we can do is compute the active conversation ID and we can make that either selected conversation ID or we can get all conversations using use conversations for this project ID. So we already have that hook and then grab the first conversation. So question mark first in the array and then grab its ID or fall back to null as the last resort. So what is this? If the user manually selects a conversation, we're going to compute that. If the user just refreshed the page, we're going to fall back to picking this project's latest conversation. And if that doesn't exist, we're going to fall back to null. So make sure you've added use conversations for this project ID hook. use conversation which accepts project id and let's go ahead and see it calls a function get by project so that's the one we are calling here so we get an array of conversations now that we have active conversation id we can get the actual active conversation using use conversation and passing the active conversation ID. Let me go ahead and active conversation ID. There we go. And now by using active conversation, we can go ahead and try and use active conversation dot title or the default conversation title like this. So let's recap what we just did. We are now computing an active conversation ID from three sources. The priority is the selected conversation ID. This means the user directly used the history dialog to select a conversation. But if the user never did that, meaning they just jumped on this project's page, we're going to fall back to loading all conversations for this project and simply picking the latest conversation that the user engaged into. And if that doesn't exist, we are going to fall back to null. No conversations available. Once we get a computed active conversation ID, we use it as a param for use conversation hook, which fetches by ID. And then we finally have active conversation in here. And then if it exists, we render its title. otherwise we render the default conversation title to create an illusion that a new conversation is already created but we actually save some space in our database. So it seems like this was just created but it actually wasn't, right? But now if you go ahead and do the following so do a hard refresh, okay? And okay, there's an easy way to test. Change this to empty, like this. And let's refresh. Why does it still say, oh, okay, create a new project. And go into that project. You can see it says empty right now. So this is the default state, right? Not a single conversation for this project. But when I click on the plus button, it says new conversation. because that is the newest created one. So if I go ahead and say a convo about things and save, it immediately reflects here, a convo about things. Great. And you can see that the next time you visit, so a refresh will immediately, you can see for a brief second, it said empty. And then it fell back to this, right? So it will always speak the recent conversation that just existed. So now you can bring this to default conversation title. So we create an illusion that every time, you know, the user creates a new project and goes into it, we already have a conversation for them. We actually don't. We didn't create any third conversation, as you can see here, but we give the user an illusion like, oh yeah, we did it, right? But we save time and it looks good. It's a good user experience. All right. So now that we have the active conversation, which can actually do more things. For example, we can load all messages. So down here, how about we add all conversation messages using use messages, active conversation ID. And now that we have conversation messages, let's go ahead and do another computed value. Is any message currently processing? So is processing will amount to true if any of the current conversations messages have a status of processing. So if it does, if they do, we're simply going to not allow the user to send any more messages until that is resolved. And the user will be able to cancel a request. So if it is in processing status, the user will always be able to manually stop that status. So it's never kind of locked in that state. So since we already are here, we can compute is processing, even though we're not going to use it right now. Great. Now that we have the conversation messages, we can actually go ahead and develop the conversation content because we stopped here, right? Because we didn't have anything to load. So conversation messages question mark dot map, get the message message index. And in here, render the message composition. let's go ahead and give it a key of message underscore id and from message dot role then a message content so we're just continuing with the composition if message status is equal to processing in that case we're going to go ahead and open a thernary and we're going to add a div oops a loader icon and span thinking as in we are loading this message right the ai is still processing it still hasn't given us the correct answer and for this div we're going to give it flex items center gap 2 and text muted foreground and for the loader icon class name size 4 and animate spin. And now to finish the ternary, in the alternative way, we're going to use message response to simply render message.content. The message response actually uses streamdown, so it will automatically handle markdown or anything else that AI throws at us. So it's such little work for a great experience. Great. And now let's go ahead outside of message content and let's check if message.role is assistant. In that case, let's also check if message.role is assistant and message.status is completed. And if message.index is equal to conversation messages, question mark length or fallback to 0 minus 1. and let me expand further so you can see how it looks like. Okay. Render message actions, actions, then a single message action with a copy icon. So if the AI has responded, let's go ahead and give the user an ability to copy the answer. This is a common thing we can see, right? So navigator, clipboard, write text, message, content. And give it a label of copy. Great. There we go. So right now, this is ready to render, but we are not actually creating any messages. So let's go ahead and see what we can do right now. I'm trying to make the most out of the things we've already built. Let's go ahead and use this is processing to see it behave visually. So in the prompt input submit, if is processing disabled will be false. Otherwise, it's going to be true. And the status will check if is processing streaming mode on otherwise undefined. So now if I go ahead, let's go ahead and just focus on this button here. if I go ahead and find is processing constant and if I manually change it let me go ahead and change it to true you can see it will kind of have a button that indicates to the user they can click to stop this Right But if you revert it obviously it not going to have anything Great. Now let's create an ability to actually submit a message. So in order to do that, we need to add a new state here. So input, set input, use state and empty string. and let's go ahead and allow the user to fill that input in the prompt input text area. So set input, get the event, oops, event target value. The value will then be input, disabled will be is processing here. Great. And now that we have input, we're going to modify the disabled mode. So if we are processing, it's going to be false. Otherwise, it's not just going to fall back to true. Only if input is not here, it's going to fall back to true. You can see now it's disabled. But if I start typing, it's enabled, right? So that's the logic we are trying to achieve. All right. Now that we have that, we have to develop handleSubmit method. So change this to handleSubmit. and now let's go ahead and implement that. So I'm going to do that right below our handle create conversation, const handle submit asynchronous method and in here I will accept a message to be a type of prompt input message. Let's go ahead and do actually no need to do this right now. So if processing and no new message, this is just a stop function. So yes, if the user just sent a message and we are processing and the user attempts to click on the stop sign here, that will trigger handle submit. So the message will actually be empty. So we can detect that by checking if is processing and if there is no message dot text. In that scenario, we know to do await handle cancel. We don't yet have this function, but that's what we're supposed to do. And then we are going to reset the input to an empty string and return. So that's a scenario for later. It's gonna make more sense then. Now let's go ahead and see to which conversation should we submit this message? We're defining a conversation ID here, and by default, we assume it's going to be the active conversation ID. But the active conversation ID can be null. So if there is no conversation ID, we're going to create a new one. So conversation ID will be await handle conversation, handle create conversation, a function from above. And if even then there is no conversation ID, we are simply going to break this method. So this is a scenario if the user just got here in a new project and doesn't click create a new conversation. So right now it's sending a message to nowhere. So we just go ahead and create a new conversation for them. And this handle conversation method will automatically select it as the active one. So it kind of does a lot of job for us. Now, finally, once we have that, we can go ahead and trigger ingest function via API. So now we open our try and catch method with toast error message failed to send. and in here we do await ky.post api messages which we have to develop and the payload we're going to send is the conversation ID and message of message.text like this. So now we have to develop api messages. So now we have to implement this api route. Let's go ahead and do source app api and I'm going to open a new folder called messages. And inside of messages, create a route.ts. And let's start by importing Zod next response and out from clerk next.js server. Let's go ahead and quickly define our request schema using Zod. So we expect conversation ID and message. if you take a look at conversation sidebar you can see that that's what we send conversation id and message so since we are not doing any zod validation on the front end let's this time do it on the back end if you remember during our extension creation we did the opposite so i just want to show you how you can do it on the back end here this is the request schema so now when we define our post method here asynchronous post request and we go ahead and grab our user id using clerks await out. We throw an error unauthorized. If user ID does not exist, we can go ahead and extract body from await request JSON. And then we can go ahead and parse request schema.parse body. And in here we get almost certain conversation ID and message because this will throw if it doesn't pass the validation. So if this isn't a string and this isn't a string, it's going to break. Great. So now we kind of have a problem because what we have to do in this API post request is somehow call convex mutation and convex query and also invoke ingest background jobs because ingest background jobs are going to serve the purpose of being the agent who is going to call tools within a loop to give us an answer. And Convex is our database. So how do we do this? Because we are now in an API route of Next.js and we know that usually we access Convex through their set of hooks. Well, Convex actually offers something called Convex Client. So if you go inside of source lib and create a new file called convex-client.ts, you can import convex HTTP client from convex forward slash browser. And then in here, all you have to do is export convex new convex HTTP client and the only prop it accepts is next public convex URL. So just make sure that inside of your .environment.local, you have next public convex URL. Perfect. And now, for example, let's attempt to load the conversation using this conversation ID. So let's call a convex query. How would we do that? const conversation will be await convex which we can now import from lib convex client dot query and in here we can call our API as usual. The problem is what do we call? Well the way I like to do this is by creating another set of convex functions specifically designed for calling Convex from a third-party source, right, from not using their hooks. So I'm going to go ahead inside of Convex here, and I will call those system functions. And now, these system functions are a bit tricky. For example, I want to implement getConversationById. So I'm going to import query, I'm going to define the arguments, I'm going to prepare an asynchronous handler here. The arguments will be conversation ID, which is convexValuesID conversations. Right, so nothing unusual so far. We accept context and arguments. And then from here, return await context database get arguments conversation ID. And that seems simple enough, right? So I saved this file. I'm going to check here. I can see all of my convex functions are ready. So nothing unusual, right? And in here, I should now fully be able to do api.system.getConversationById. And in here, I can pass conversationId to be conversationId from above as we have to cast the type ID of conversations. So is there anything wrong with this? Well, in theory, no, right? Just make sure you import this ID. But here's the thing. I'm not comfortable with the idea of there being what's essentially an API route. You can think of convex functions as API routes. I'm not saying it's exactly the same thing. I'm just personally not comfortable with the idea of having a fully unprotected route like this. And you might think, okay, so we just do our verify out, right? Well, not exactly, because we will be calling these system functions from various things. Some will be API routes. Some will be background jobs, right? And even if we are able to extract the user token and then we pass it along here, still think of this as something an attacker can get access to. Imagine your attacker getting access to this API. It's obviously not that simple because we initialize the convex with our environment keys and everything, right? But just always think about it like that. We shouldn't allow them to just enter a user token here. And even if we did, user tokens can expire, change, and things like that. So the solution I have thought of is to implement something called an internal key. And we're going to use that as simple as this. After we check one authentication we going to check for the internal key Do we have process convex internal key And if we don we going to throw internal key is not configured I'm not going to allow you to make requests to this api.system functions, right? So now let's go set of dot environment dot local here and let's define the convex internal key. So I'm going to go to the top here and I'm going to add convex underscore internal underscore key and I'm going to give it your secret key here value. Please change this to something else especially in production. This is just for a tutorial. In fact just smash your keyboard instead of writing your secret key here. Yes, this will work simply because it's a string, but please don't forget and then publish this, okay? And now that you have that, copy it, save your environment local, go inside of your convex, go inside of your settings, environment variable, add, and just add it here to and click save. And make sure it's the same. Okay. And now let's go ahead inside of our system.ts and let's implement a simple helper function called validate internal key. Again internal key using process.environment convex internal key like that and if the key that we pass to this function doesn't match what we have in our dot environment throw an error. and now that we have validate internal key we can finally use it inside of our mutations right here so now I'm going to add internal key here to be a type of required string and now whoever attempts to call my get conversation by id which is in my system here so i'm purposely separating this from all others because others are using normal alf checks this one is using a special internal key validation check and you can call this key whatever you want right if you want you can give it a little prefix like p as in polaris internal key right so you know this is not something that you should expect. In fact, that might be a good idea, like Polaris, convex internal key. I'm purposely going to change it to this, and I want you to do the same, simply so you see all the places it's being used, okay, so change it here in route, we changed it in system, we are now going to change it to kind of .environment.local, so Polaris, convex internal key, and then last thing, go ahead and change it in your environment variables. Click on edit and change this to Polaris Convex internal key. There we go. This way, even if Convex decides to add a variable like that in the future, it will not conflict because this one has a prefix of our own project here. So we know this is something we use, something that's important for us. So this is no longer just a random unauthenticated function. And now we can safely, in my opinion, safely call and query convex through various third-party apps. So let's pass in the internal key. There we go. And now we actually have access to this conversation. Great. And let's go ahead and do the usual checks now. So if there is no conversation, I'm going to return next response. Jason, conversation was not found. and now that I have the conversation I can get the project id so what I want to do now is I want to create the user message I'm going to do to do check for processing messages we're going to do that in a moment but I just want to show you how like hey let's go ahead and create a user message. To create a message, we again need to visit our system here and let's go ahead and create a new mutation. Create message is going to accept a whole bunch of arguments. Internal key, conversation ID, project ID, role of the message, content, and the status with all the options it accepts. Make sure this matches exactly what you have in your schema define table. So the status needs to match. Otherwise, your function will be able to create an error in the database, which is not something we want, of course. And then let's go ahead and create a handler. Let's go ahead and validate the internal key. And once we do that, let's simply create a new message using await context database insert into messages conversation id project id role arguments content arguments status and created at do we actually have created at we don't yeah no need for that okay do not pass that and then let's also update conversations updated at if you want to, right? Remember how we did this for projects. I personally think that, yeah, every time you send a new message, the conversation should be considered updated. We currently don't have much purpose for showing updated ad of a conversation, but, well, actually we do because every time you refresh, the conversation we load is the latest updated conversation. So, maybe it makes sense? Yeah. And remember to return the message ID. Very important. All right. So now we are able to create the conversation. And let me just see. Oh, I didn't import mutation. Make sure you import mutation from generated server. There we go. And now we can go back instead of our route here. And we can first go ahead and create a user message. Await convex, which comes from our convex client lib. there we go convex mutation api system create message pass the internal key the conversation id the project id which we fetched from above so we don't have to cast it because it's already a correct type the role this is the user and the content this is the message and then let's go ahead and immediately create the assistant message placeholder. So we are sending to await convex mutation. Let me go ahead and maybe move it like this. It's easier to look at. Convex mutation, API system create message, internal key, conversation ID, project ID, role content and status processing like that there we go i'm going to add to do invoke ingest to process the message all right and let's return next response dot json success true event id for now is going to be zero let's add a comment to do later use ingest event id and message ID will be assistant message ID. And let's add a comma here so we can actually go further. All right, so the to-dos we have to do are check for any processing messages and stop them if there are and invoke ingest to process the message, which right now isn't happening because we are not going to work on that now. We just want to work on the UI and the database storage, right? so let's go back inside of our conversation sidebar make sure you actually develop this inside of app api messages route.ts so this route works and let's try it out let's see if this will work or not so if i say we can refresh we can do whatever perhaps it's best to test if everything works by creating a new project clicking on it and then let's do hello world let's go ahead and press enter and there we go hello world has been sent and you can see that in here it's immediately set to thinking and if you look at our data we should have two messages one from the user and one from the assistant with the status of processing right here perfect and you can see that while the message is processing the user is not allowed to send any new messages they will only be allowed to stop, right? So they can cancel the current request. This is kind of a protection, so not too many costs occur. Now let's go ahead and create another system function. So I'm going to go inside of system here, and I'm going to implement a mutation called update message content. Here it is. Update message content is a mutation which accepts the internal key, the message ID, and the new content. We do the usual. We validate the internal key, and we do await context database dot batch. And now let's create the ingest background job to process a message. So we're not going to do any AI processing. I just want to simulate how it's going to look like. And we're going to need this mutation for that. So let's go ahead inside of source features conversations and I will create an ingest folder here and then inside process message.ts let's create an interface message event which is going to define what is the payload of this background job a message id conversation id project id and the message in a form of a string So let's export const process message, which will use the ingest client and the create function. In the first argument, we're going to define the AD to be process message. And then let's go ahead and add, let me just see, like this. Okay. Event message forward slash sent and more importantly how about we add an ability we do this here actually called cancel on Cancel on is actually a super cool feature which allows you to cancel a function a running background job simply by invoking a very specific event, which in this case will be message cancel. but only if we match data message id so we can simply trigger this from ingest client and if the message id is a match then it's going to cancel whatever processing of that message id we had so a super cool built-in feature from ingest and i just took a peek and looks like it's deprecated so let's use if instead and i believe it works the same let's see if asynchronous data user equals event data user id all right not a hundred percent sure so i'm just going to go ahead and research just a bit. So if is definitely the new one and it uses a common expression language where this is how you would do it. Event.data.messageId equals asynchronous.data.messageId. Event refers to the incoming cancel event and async refers to the original event that triggered the function. So a much more explicit language. Alright, now let's go ahead and do the following. So we have to open async event and step like so and let's start by extracting from event.data which we can cast as message event let's extract message id conversation id project id and message like that let's get the internal key and we are just going to do a slight modification here so we know that this is polaris convex internal key then if there is no internal key let's throw a non-retriable error which you can import from ingest and just do polaris convex internal key is not configured. And now what I want to do here is simply do await step.sleep and let me actually find the proper syntax for this just a moment. Basically I'm just using sleep to pretend some AI processing is happening like let's wait 5 seconds for example. and then what we are going to do is we're going to create a step update assistant message and in here we're going to call await convex which we have to import so from lib convex client dot mutation api which we have to import from generated api dot system update message content. Pass in the internal key, the message ID, and the content in here will be AI processed this message to do like that. So a super simple step and we're not using these so we can remove them from now. Let's just go ahead and get rid of all of these which we are not using so it's not confusing us here. And I guess we can also remove them from the message event as well. So a super simple step which we can cancel if the message id matches and which fails immediately if it's non-retriable and it pretends to do some ai processing so now let's go ahead and register that method inside of api ingest route so i'm going to add process message from features conversations ingest process message. Let's go ahead and do npx ingest CLI latest dev. Let me just try and expand this. Let's make sure this is running. Let's go ahead and visit localhost 8282 just to confirm all is well. And so that we have that new function. okay looks like I have a bunch of them here I think it's because I have my other app opened just a second all right here we go so now I have demo error demo generate and process message make sure you are running your app with this npx ignore scripts false ingest CLI latest dev since that's the official instructions yeah great and we now have that method here process message and now that we have it let's go back inside of our api messages route and in here after we create the user message after we create the assistant message id we can go ahead and invoke ingest to process the message. So we need to know the event's name, which is message.sent. So that's what we're going to call here. So let's do const event await ingest. Make sure to import that from ingest client. I'm going to move it here. ingest.sent name message forward slash sent. data and simply pass in the message ID, which is assistant message ID later, we're going to have more info. But for now, that's the only one we are working with. So now we can go ahead and pass the event dot IDs, and then the first one in the array here. I mean, we're not really using that, but you know, just more info doesn't hurt. So what's supposed to happen now is that when you send a message, it shouldn't get stuck in the thinking phase, let me open a new conversation. Hey, how are you? So now I should send it. It should think for four seconds and then it should update the message content. There we go. AI processed this message in parentheses to do. So that is the general idea of how this is going to work. We are mixing convex database access and ingest and we are doing it all very securely with the internal key here. Amazing. We can also very easily implement a native failure message. So instead of process message ingest function, if we go ahead and add on failure, we can make this an asynchronous method, get the event and the step. And in here, we can extract message ID from event.data.event.data as message event. so we have some type safety and then we can again attempt to get the internal key here polaris convex internal key and we can update the message with the error content so for example if we have the internal key call step.run update message on failure and call convex mutation API system, update message content with internal key, message ID and content, and then some error message. My apologies, I encountered an error while processing your request. How do we test this? Well, let's purposely throw an error here. So I'm going to go ahead and do await step dot run, throw error, async throw non-retriable error purposely through this. Or maybe I can just do it like this. How does this work? Non-retriable error. Oh, throw new non-retriable error. So await step.run throw on purpose. and let's go ahead and add that here. So if we try this again now, let me go ahead and refresh and if I say this will not work, after four seconds, it should throw on purpose and it should update the content of this message with this. My apologies, I encountered an error while processing your request. let me know if you need anything else and if you haven't noticed already this method you okay make sure to remove this throw on purpose it was just to test it out when we call this update message content we also set the status to be completed right i don't think i brought your attention to that right that's what makes this display a message instead of infinitely spinning and let's try one more time this will now work so the only thing that doesn't work well is this set input doesn't reset so let's quickly go inside of the conversation sidebar and in here set input to an empty string so i'm talking about the handle submit method right here so let's go ahead and refresh and let's do another try there we go now it clears it the cancel is not yet working we will implement that it's a little bit more complicated but I want to keep that in the next chapter since we are already more than an hour in so let's go ahead and merge all of these changes there's a lot of them So let's see, I'm going to shut down all of these files and I will create git add, git commit 12 conversation system, git checkout b12 conversation system, git push u origin 12 conversation system. There we go. You can see I am on conversation system. And let's go ahead and open a pull request and review our changes. There's a lot of them, so if CodeRabbit comments on our components file, we're mostly going to ignore\nWe're just going to focus on if there are any serious issues in what we've currently implemented that isn't in a temporary state because we will be working further on this system. We still have to connect it with AI, of course. So as I suspected, the release notes are mostly skewed due to the fact that we've added 40 components from AI elements. So let's take a look at the new features, but do understand that it took context of all the new components we added, so perhaps some things might not make sense. We added conversation and messaging system for multi-turn interactions. We introduced comprehensive AI UI components for content visualization, code blocks, citations, reasoning panels, and image display. We added message processing with ingest integration for asynchronous handling. we implemented prompt input with file attachment and speech to text support so this is the functionality inside of the prompt input component we are not using it but it exists the code is there that's why it's adding these things which seem like we didn't do this but we actually did by adding all of those components majority of the comments here are actually referring to those for a few of them are for our things like this route which is missing error handling we can use safe parse here I think so it throws the error or we can just implement try catch to fix this but you can see majority of the comments are for this I think the only one I actually found is at the end here which is simply warning us about the fact that we are sending this placeholder message to the public and to confirm that this will be changed later as well as the status should transition to something like completed uh let's see well update message content actually already does that it's just not clear in the name and in here it's telling us to do a runtime check on this missing environment variable if it's missing. So mostly nothing serious and majority of the comments are about all the components we've added. Amazing, amazing job. So let's go ahead and sync git checkout main, git pull origin main. So we are up to date. And as always, I like to confirm by opening my source here, graph, and I've checked out for 12 and merged back in. Amazing, amazing job. We've set up conversation and messages. We can send both user and receive assistant messages. We didn't do the past conversations history dialogue. We're going to do that in the next chapter. We handled message sending, but we kind of prepared constellation, but not fully. And we did display the thinking indicator. So good job. Some things will be left for the next chapter and then we will be able to immediately combine all of that with AI functionality. Amazing, amazing job and see you in the next one.",
  "transcript_chars": 530153,
  "transcript_filled_at": "2026-06-06T16:10:35.999907+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}