{
  "video_id": "CAr02YlEJUc",
  "title": "Build and Deploy a B2B AI SaaS Support Platform | Next.js 15, React, Convex, Turborepo, Vapi, AWS",
  "url": "https://www.youtube.com/watch?v=CAr02YlEJUc",
  "transcript": "What if you could build your own AI-powered customer support platform? Something that feels like intercom, but runs on your stack, with voice agents, AI, and multi-tenant B2B baked in. This is Echo, and in this full course, you'll build it from scratch. The AI handles support on its own, and knows when to escalate. If a customer gets frustrated, Echo flags the chat for a human. And if the issue resolves itself, Echo closes it. All of this is powered by convex agents, retrieval augmented generation, and any AI model of your choice, including OpenAI, Gemini, Anthropic, or Grok. Inside the dashboard, support teams see everything in real time. They can take over conversations, respond directly, or resolve manually. You'll also get full context, like user info, device, and session history. Echo can learn your product by reading your documentation. No model tuning needed. The AI generates embeddings and uses retrieval augmented generation to ground its responses. Echo supports voice agents, phone numbers, as well as inbound and outbound phone calls, all powered by Vapi. And you'll learn how to white-label a single-tenant voice API into a B2B SaaS using bring-your-own-keys method, along with AWS Secrets Manager to store API credentials securely per tenant. Let's hear the voice agent in action. Hello, you've reached VapuBank customer support. My name is Tom. How may I assist you today? Hey, yeah, I want to check my account balance. Could you provide the last four digits of your phone number so I can verify your account? Sure. One, two, three, four. Thank you, Jondo. Your current account balance is $2,534.67. Is there anything else I can assist you with today? That's it. Thank you. You can invite team members, assign roles, and manage workspaces, all isolated, secure, and powered by Clerk. Thanks to Sentry, our app can track errors with full-stack visibility. We can get real-time alerts, replay sessions, and trace logs across front-end and back-end, as well as a seamless integration with Convex. This brings our app one step closer to a production-grade application. Echo runs in a TurboRepo monorepository with separate apps and shared packages. We're going to develop not one, not two, but three different apps. The dashboard for our operator dashboard, the widget for our chat box, and an embed application, which will be our dedicated developer toolkit to preview our widget script. Along those three apps, we're going to share our Convex package and our Shatzian UI components, making this a true monorepo architecture. Each chapter in this tutorial wraps up with a real pull request, reviewed by CodeRabbit, ensuring that our code has no bugs or serious security issues, allowing us to ship production-grade code. This isn't just a tutorial. It's a blueprint for a real SaaS business, with AI, voice agents, billing, and ALF, ready to serve real customers. This is how you build a modern SaaS product in 2025. And now, without further ado, let's build Echo. In this chapter, we're going to learn how to set up our project. This will include learning how to install PNPM version 10, how to set up Turbo repo using ShadCNUI, Next15, Tailwind version 4, and React 19. We're then going to learn how to use the mono repo by adding the second application and learning how to add a simple internal package. And then we're going to show a more real world example of adding a new ShadCN component that can be shared across those two applications. And at the end, we're going to wrap it up by creating a GitHub repository to keep track of all of our changes. In case you are confused by some of these terms, I've created a little box here to help you out. PNPM is a package manager similar to Yarn, NPM, or Bun. TurboRepo is our monorepo build system and ShadCNUI is our component library in the simplest terms possible. So let's start by installing PNPM version 10. So why version 10? Simply because at the time of me making the video that is the latest version and the version I have built this entire project with. So you can use the link you can see on the screen or pnpm.io and visit their page. After that go inside of the documentation, installation. Depending on the OS you use, go ahead and follow the Windows manual, PowerShell manual, curl, anything you want. If none of this fit your needs, you can also use NPM or Homebrew, as well as many, many other options. So after you've done that, go ahead and do PNPM version. Now what's important here is that you use PNPM version 10. I don't think it's strictly necessary to use the exact same minor versions as me but there is a way to do that and I will show you how but just before we do that confirm that you have PNPM installed whatever version it doesn't matter right now I'm going to teach you how to change the version scroll a bit down here to find compatibility. So since we are using PNPM 10, the node version, minimum node version, is node 18. So go ahead and run this as well. As you can see, mine is 22. So I fall into this category right here, which means I am okay. PNPM 10, node.js 22. If you get a version lower than 18, or if you get an error, please upgrade or install node to the latest long-term support. and now about this exact version so if you are watching this video far from now I don't know a couple of years from now it is possible that a new version has come out so what you can use is self update command and in here you can specify the exact pnpm version and then you can use the exact same as me if that's something that you want but again if you're watching this video you know pretty early from when I released it. As long as you're using PNPM 10, I think that's perfectly fine. So after you have confirmed those two things, let's go ahead and let's install our Turbo repository template. The way we're going to do that is by using a ShadCN UI template. So ShadCN is our component library. And what you have to do is visit ui.shadcn.com or use the link you can see on the screen and go inside of the documentation. After that, go inside of monorepo right here. And you can see that even they admit that until now using ShadCNUI in a monorepo was a bit of a pain. So because of that, they have created a command line interface to help you out. And now we're going to run this to create a new monorepo project. And now I have to warn you again, use pnpm. If you try developing this project with npm, yarn, or bun, I cannot guarantee that you will be able to complete it. So pnpm has a very specific workspaces way of separating packages. I have no idea if there's anything similar in pnpm, my apologies, in npm, yarn, or bun. In fact, when I try running this command, I get an error. So it only works with pnpm for me. So please, if you want to complete this tutorial, use PNPM. Now, in order to make this even easier for you, I won't be using the at canary here, canary, however you pronounce it. I will be using the exact version. So how can you check the version like this? So why am I using the exact version instead of just a tag? Well, the reason is so you know what I'm using, right? Because I don't know if you're watching this six months from now or five years from now. This version right here is basically, will basically determine your Next.js version, your React version, and your Tailwind version, as well as all other internal packages. So that's why it would be a good idea to use the same version as me if possible. So now let's go ahead and run init here and let's set up our Turbo Repo structure. So make sure you select Next.js Monorepo here. And I'm going to call this app Echo and I'm going to call it Tutorial. And after that, let's go ahead and sit down, relax and wait for this to install. And after this was completed, you will see a success message. Project initialization completed. You may now add components. And now we immediately go ahead and do CD and go inside of this package right here. Inside of here, you should be able to see the apps, the package JSON, packages, pnpm lock file, pnpm workspace file, tsconfig, and turbo. Now let's go ahead and set up our IDE. I'm going to be using Visual Studio Code, so I'm going to go ahead and click open here and I will select Echo Tutorial. If you get prompted with this, you can press yes. So you can see that the structure is a little bit different than what we are used to in normal Next applications We have this weird apps folder that has the web folder And then inside of here we finally have that structure that we are used to the app folder with the layout and the page. But we also have another thing called packages here. And in here we have the slint config, the TypeScript config, but also the UI. And inside of this UI, you will recognize that these are ShadCN packages. This is the popular CN package from ShadCNUI. And in here we have the globals.css that you usually find in the root of your Next.js application. So you can see that there is definitely a little bit of a difference in this application right here. So what I want to do now is I want to make sure that you have Turbo installed globally. so head to turbo repo.com or use the link on the screen here and in the docs in the installation here you can skip this part because we are not building turbo from scratch but in here you can find a guide to install turbo globally which brings flexibility and speed to your local workflows so go ahead and use the pnpm selection to add turbo globally and once you've done that once you run that command, you should be able to type turbo in your project like this. And after that, go inside of your project here and run turbo dev. And you will see that we have a bit of a familiar interface here, but something new here. So here on the left side, we have tasks. Basically, when you run turbo dev inside of your root folder in the turbo repo, it will find all apps that need to be run as well as all packages that need to be run. At the moment, the only thing we need running is the web application. So when you visit localhost 3000, you will see good old hello world with a button. Now, let's go ahead and look at our versions. So since I used Shadzian 2.9.2, my NextJS version is 15.2.3. The only reason I'm telling you this is so you are aware of what version I'm working with so you don't have any huge changes, right? And you can also see that the package JSON works a little bit differently in TurboRepo here. So there is a global package JSON in which we have these dev dependencies, which are the shared sLint config, TypeScript config, and then some specific packages used for the entire application. You can also see that we have a very specific package manager version set here to be PNPM at 10.4.1. So this is set by ShadCN template. So what does that mean for us since we are using a higher version than 10.4.1? Well, it doesn't mean much because our higher version is backwards compatible with this version. So it would be a problem if this version was higher than our version. But since it is not, I don't think it's any problem whatsoever. Other than that, you can go inside of your app web app page.tsx and you can say hello apps web. Save the file and it will change here immediately. So now what I want to do is something fun. Let's shut down this app. And let's go ahead and do the following. So let me just see what I've changed. I've changed this. Perfect. Let me close all of this. I'm going to go inside of apps here. I'm going to copy web and I'm going to paste it. And I'm going to rename this widget. The reason I'm calling this widget is because we will have another application called widget. So why not name it immediately? Now, what I want to do is go inside of widget and go inside of package.json and just change the name here to be widget as well. I believe that everything else can stay the same. And the dependencies, everything else can literally stay the same. There's nothing here that we have to modify. These two are both going to be independent Next.js applications which share the same packages from ShadCN UI as well as the same Eslint and TypeScript config. So what I suggest you do when you do this is just run pnpm install just in case. So because we just added a whole new application, so I want to make sure that everything is up to date. You can see that mine says already up to date, but just in case, you know, it's always good to do that. And when you're running pnpm install, make sure you are in the root of your project. So what happens now? Let's do Turbo Dev. And now you can see something different. you can see that our widget is now running but if you use arrow keys up and down you can change between these two applications and you can see that now one of them is running on port 3001 because obviously port 3000 is taken. Now I don't know which one of yours will run on what port it's honestly just dependent on whichever one compiles first right But I will show you how to put fixed port numbers so you don't have to guess which one will be which. But what's cool now is that if you go to localhost 3000, let me just refresh this. It says apps web. So let's do this. Let's go inside of apps. Let's go inside of widget app page and change this to hello apps widget. And you can see that if I go on, let me just highlight the widget dev. So it's running on 3000. I am on 3000 now. I'm going to try and enable developer mode so you can see the URL here. I'm on 3000. So it says, hello, apps widget. But if I change my port to 3001, it says, hello, apps web. Now, I'm not exactly sure why one is in light mode and one is in dark mode. It doesn't matter because we're going to remove that theme provider altogether. But you can see the point in Monorepo. We are building two applications at the same time while keeping them separate. They are two Next.js applications in the same repository, but they are separate. They are run separately and they will be deployed separately. This is the exact structure that we are going to need for our project. Now, we don't really have to do much anymore. In fact, we can shut down our app. and what I want to do is I want to do the following. I want to create an internal shared package. The reason I want to do that is so that you understand what this is because it's quite easy to understand what apps is. We've just run them and we've just modified them. Pretty easy to understand, right? But these packages are a bit odd. You know, what are these files? You know, what is this exactly? What is this? I mean, we know it's chat CNUI, but how exactly does it work here? I personally managed to understand this a little bit better by going inside of getting started, my apologies, instead of crafting your repository and creating an internal package. So let's go ahead and learn how to do that. So first, let's create an empty directory inside of our packages called math. So I'm going to go inside of here and I will create a new folder called math. Now inside of here, I'm going to add a package adjacent specifically for pnpm. So always make sure pnpm is installed. And I'm just going to copy the entire thing from here. So inside of here, I will create package.json. And I will paste it right here. Now I'm just going to change this to not use a repo and to instead use workspace. And I'm going to do the same thing here. so the reason we are using workspace is because chat cnui template is set up to use that if you go inside of your packages instead of ui and click on any package json you can see that this is how we call them we alias them on the workspace in their example they alias them on the repo repo however we pronounce it right. But you can see that we pretty consistently use workspace. So it makes no sense to just change that randomly. So let's go ahead and learn how to add this using their example, but just slightly modified for our example. So what I did to remind you was I changed these two instances of repo to workspace. And nothing should really change, right? I see no errors, nothing wrong. And just to let you know, so this workspace tags right here, these are very specific to PNPM. You can see that if you click on Yarn or NPM, it's different, right? So that's why I told you, please use PNPM. I'm pretty sure that some of you are more familiar with mono repos than I am, and you might think to yourself, oh, that's not true, you can actually use Yarn. Sure, feel free to use it, right? I'm just trying to make this easier for everyone to follow because I know it works using this package manager. So that's the only reason I'm doing so. Great. And in here, we can break down package JSON piece by piece. So the scripts. Inside of the script, we have the developer script and we have a build script. And basically what's happening is that both of them compile the package using the TypeScript compiler, which is this tsc command. The dev script will watch for changes to the source code and automatically recompile the package. Basically hot reload is the same thing as running next dev to get your app running. In the dev dependencies, you can see that we have the TypeScript individually set, but we also have a TypeScript config shared with our entire application. So why can we do that? The reason we can do that is because we have TypeScript config already in our packages. So what we've done is we've abstracted this TypeScript config and we are sharing it across the math package, across the web app and across the widget app. So all of our individual modules inside of this monorepo are sharing the same TypeScript config. but you still need to install the actual TypeScript package in the app or the package that you're going to use. So this is a little bit more complicated, but it gets quite logical as you move on And I believe this is what they explain here So TypeScript and repo TypeScript config in our case, workspace TypeScript config, are developer dependencies. So you can use those packages in the, in our case, workspace math package. In a real world package, you will likely have more dev dependencies and dependencies, but we can keep it simple for now. Now in the exports, in here we define multiple entry points for the package so it can be used in other packages right so whenever you define a package you also need to define the exports and once you define the exports we can then import those packages inside of web and inside of widget for example so if i'm correct we should be able to visit our packages ui and if i go inside of package json let me see if I can find the exports I can. So you can see the exports here. globals.css, postcss.config, lib and everything inside of it, components and everything inside of it, and hooks and everything inside of it. So this allows us to reference this internal package and use all of these things across other packages or apps. So be mindful of that whenever you're creating internal packages. You need to export whatever you wish to be able to import later on. Perfect. Now let's go ahead and let's add tsconfig.json. So I'm going to copy this. Inside of math, tsconfig.json. Let's paste this here and change this to workspace. And when you command and click on this, you should be able to open it, right? So I think that if I just leave this as repo, you can see that I cannot open it, right? That's because mine are referenced under workspace. So that's why we are doing this. If you have it like that, everything should be just fine. So we've done four important things here. We have created a shared... We didn't create it here, but we are using the shared base JSON inside of our TS config. And then we have created the out directory to tell the compiler where to compile. And the root dir to ensure that the output in the out dir uses the same structure as the source directory. So this specific thing isn't going to be too important for us. It is more important for this exact package. But still, it's a good practice to do this. And now let's go ahead and go inside of the math folder. And let's create a source file. and let me just close this error and inside create add.ts I'm just going to copy this very simple addition function here and then what we're going to do is we're going to go inside of one of our apps here and let me just check if I have just a second I want to make sure that I didn't miss a step here where we are required to run PNPM install. Okay, this is what I'm going to do. We just added the source add.ts and now let's go inside of vet package.json here and let's add add workspace forward slash math workspace colon asterisk like this. There we go. Exactly like this. And in here we have a warning. You just change the dependencies in your repo. Make sure to run your package manager's installation command to update your log file. So let's do that. Make sure you're running the root of your app here and do pnpm install. And you can see that it actually added one package. And I believe that this package is, let me try and find it here. You can see it basically created a proper link to workspace math, right? So whenever you add a new dependency inside of a package JSON, I think specifically, where is my apps package JSON? Here it is. You have to run pnpm install. And once you have added that, I believe you can already use it. If we go instead of apps, web, app, page, let me try and import add from workspace math add. And in here, what I'm going to do is just add two and two together. And now let's do Turbo Dev here. No errors. That's great. And you can now see that we have a third task running our workspace math dev. So the reason this task appeared, but our other packages are not appearing here, because you've probably noticed that, right? This isn't the only package we have, but for some reason, math is running, whereas UI isn't running and TypeScript isn't running and Essendon isn't running. The reason for that is we added the dev script. so it notified it noticed that this script here and decided to run it right so that's how turbo decides what to run in these tasks here so let me see where my web is running so make sure web is highlighted and click on that link it will either be localhost 3000 or or 3001 and there we go two plus two is four and that's how you create an internal package I believe this is the exact thing that we just did we just use workspace here and in here it also tells you to edit turbo json so let's go ahead and do that turbo json is located in the root of your application here now let me just check this so tasks build outputs tasks build outputs let's go ahead and let's add dist and then everything inside of dist. And now let's do turbo build to see what happens. Now keep in mind that there is a chance our turbo build fails for a very simple reason that they are using a different template and setup than ours. So I will follow this tutorial to the end, but just because it fails doesn't mean that we did anything incorrectly. It just might mean that in the next chapter, we're going to explore why the build fails. Let's go ahead and do Turbo Build. And you can see that now it's going to build three apps, Workspace Math, and then Web, and then Vidget. So it's building three things because it detected three build scripts across those packages and apps. And you can see that three of them are successful. So basically what happened is that in each of these packages here, it found the build script and run it. And in our apps, it found the build script and run it. And same thing for the widget. So that's how Turbo Repo works, right? And now the cool thing is you can go inside of your widget, go inside of package.json, and you can add it here as well. So workspace forward slash math workspace like this. And here's a thing I don't know, for example. I'm not really sure of the difference between an asterisk and this caret. That's one thing that you can write in the comments if you know the answer to or research yourself. So I'm also exploring this Turbo Repo as we go along. And the reason I'm saying this is because I don't want you to be discouraged if this is new technology for you. It is also new technology for me. And I learn best by building inside of it. And I hope that you will too. So once I've added this dev, oops, should I add it in dev dependencies or dependencies? Yes, I should add it in dependencies. My apologies. So I'm doing that here now in the widget. The same exact thing I just did in the web, right? and remember once i do that and save this file i have to do pnpm install and there we go everything's up to date but you know just in case and now let's do turbo dev again and actually here's what we can do open the package.json of the widget and open the package.json of web. And now what we're going to do is we're going to change the port of each of them. So in the dev script, when you run next dev through boback, also add dash dash port and for the web, set it to be 3000. for the widget, set it to be 3001. So this way, whenever you run TurboDev, you don't have to guess which one will be which. You can see that this one is strictly on 3001 and the web is strictly on 3000. So now I'm going to open the widget one here, which shouldn't have any calculation here. but if I go inside of widget app page I should be able to do the same thing there we go add from workspace math add two and two together and there we go four here as well perfect so we now learned how to add an internal package so what I want to do next is I want to learn how to add a chat cn component but I just want to finish reading this here so best practices for internal packages one purpose per package when you're creating an internal package it's recommended to create packages that have a single purpose it isn't a strict science or rule but it is a best practice depending on the repository it's easier to understand and it's reducing dependencies per package. Here are some examples of what that means. Repo, you can read this as workspace in our case. UI, a package containing all of your shared UI components. Exactly what we have. Tool specific config, a package for managing configuration of a specific tool. We have this exact thing with eslint NTS config And here another one example Perfect And as always there are exceptions to the rule but for this tutorial this is just fine So I hope this kind of cleared up how internal packages work inside of Turbo Rebo. And just to wrap this up before we start a new GitHub repository, what I want to do is add a new component to the project. So what you have to do is you have to go into one of your projects. So go inside of CD, apps, and let's go inside of web. And let's go ahead and use PNPM, DLX, ShatCN 2.9.2. And I'm going to add, and let's, for example, add input. I think that's one we don't have. And you can see that it added it to packages UI source components input.tsx. So you can see what it says here. The CLI will figure out what type of component you're adding and install the correct files to the correct path. For example, if you run npx chat cn add button, the CLI will install the button component under packages UI and it will update the import path for components in app web. But if you add a chat CN block, then it will add the components under packages UI, but the block under the exact app you have added. And now what we can do is basically this thing, which we've already done. So I just added input. And now I can go to, for example, a widget. And I can extend the input here now. Oops, my apologies, that's not how you use chat CN. Input from components input. And let's go ahead and add an input here. Let's go ahead and do TurboDev. Oh yes, make sure that you exit to the root of your application and then run TurboDev. Because if you are inside of a specific directory, it will only run that directory. So here it is, my widget is 3001 and I should now see a little input here. So it works. That's how you add shared ShadCN UI packages across two different applications. Perfect. This is exactly what I wanted us to achieve in this tutorial. I mean, in this chapter, we got familiar with TurboRepo. And we also learned, you know, why do we need PNPM, how to change PNPM versions, how to set up TurboRepo with ShadCN template. and we learned how to use Monorepo by adding a second app and creating one internal shared math package as well as a more realistic example of adding a shared Shatsian component. Excellent. So now it's time to wrap up this chapter by creating a GitHub repository. So head to GitHub and go ahead and click on create new, new repository. I'm going to call this echo tutorial. I'm going to set this to private. and I will click create repository. And now what we have to do is we have to use the second option, push an existing repository from the command line. But just before we do that, we have to resolve this 22 pending changes here. So what I'm going to do is I'm going to go inside of my source control here, and under my changes, I'm going to click plus stage all changes. And then in here, I'm going to add feet, add turbo repo. and I'm going to add commit. And what happened now is that we have locally saved and staged and committed these changes, but we didn't publish this anywhere. So if you lose access to this computer, this also, well, disappears. So what we're going to do now is we're going to copy these three lines, make sure you have done this first and you have no outstanding changes. Go inside of your terminal, you can shut down the app. Make sure you're in the root of your application and run those three commands. I use main as my branch name. In your case, it might be something different. So it's not a mistake. You just might be using something else. Go ahead and run this. And after that, when you go back here and refresh, you should be seeing the Shadzian UI Monorepo template readme file. And in here, you will see your apps, web, and widget. And you will also see your packages here, math, slin config, typescript config, and UI. Perfect. So I believe that marks the end of this chapter. We've learned a lot. I hope this made it easier for you to understand what we're building and how we're going to build this application. It is a little bit different. It might be outside of your comfort zone, but I believe we're going to go through this together and build an amazing app. Great job. See you in the next chapter. In this chapter, we're going to add Convex as our backend. And thanks to Monorepo, we're going to add it as an internal package so that we can share it across two different Next.js applications. But just before we do that, I want to resolve a lint error coming from the build command. So it's not exactly preventing the build from happening. As you can see, it's successful. But if you scroll a bit up, you can see that we have this odd warning. So obviously it didn't break the build, but when it comes to building your app, it would be nice not to have these types of warnings. So let's analyze this. I'm going to go ahead inside of my packages, eslint config next JS. This is where the error is reported to happen, as you can see. and it's specifically talking about eslint forward slash js package. Now a very easy way you can check if you have a package installed or not is by pressing down the command or control button and hovering over your packages. You can see that whenever I hover over an installed package I can directly go inside of its node modules but when I try that here it's not working and of course you can just look inside of your package JSON and convince yourself that that package truly is missing. Now, there is a very easy way of fixing this. What we have to do is we have to use pnpm and filter option to add that package to one workspace exclusively. So pnpm, and let's go ahead and add forward slash f. And after this, you have to type the package or the app where you want to install this. So this is F as in filter. So slint config, and then just as usual, add save dev slint js. Make sure you didn't add any typos here, otherwise it will throw an error. And now if you go inside of here, you will see that a package JSON was updated, but not just any package JSON, specifically the one inside of packages slin config with slin forward slash js and you can see that now we have the same effect when i hold down command or control key meaning this is now installed and more importantly if i try turbo build it still succeeds as it did before but now no errors perfect so i think this was a nice little lesson in pnpn filters and fixing a bug Perfect. Now let's go ahead and let's add Convex. So head to convex.dev or use the link you can see on the screen to let them know you came from this video. So Convex is the open source reactive database for developers. And the thing I like the most about this is when they say open source, they truly mean it. I've seen people self-host Convex. They're not just using it as a trendy word. The other thing I absolutely love is their sync engine. So in here we have a very simple to-do example and it's nothing new. We've seen to-do examples a million times but what actually happens when I add a new to-do? You might be thinking oh cool so they refresh all to-dos when I add them. That's not what happens. The moment a new record is written to the database their sync engine automatically updates the UI. And this was the wow moment for me because I just think of building a chat application and now think of building a customer support application. Think of all the web sockets that you have to handle, all the messages that you have to transfer from the chat box widget and then to the operator dashboard. And then think of all the statuses of the messages that we have to maintain. And now think of all the potential WebSocket issues that could happen. Think of all the mismatched keys. All of those things are no longer a problem because this is an amazing sync engine that works out of the box. No configuration needed for real time. This is what made Convex such an incredibly powerful tool for me. And I think for this kind of project, it is absolutely perfect. The developer experience will be at minimum 100 times better than if we were to try to do this without Convex. So let's go ahead and for now, just create an account. Nothing more, nothing less. And after that, you're going to enter their dashboard here. And depending if you've used Convex before or not, you might have some apps here like I do. You don't have to create any app right now because we're going to do that in a moment. So what we have to do now is we have to add Convex to our project. But how exactly do we do that, right? Because we want to do it as an internal package. So what I'm going to do is I'm going to be using this documentation again as a reference. I'm going to be using this. You can see the link on the screen to visit this. It's their TurboRepo, Monorepo template. So I'm specifically focused on their backend package here, which is convex. and I'm also going to be referring to this, packages UI. So I'm going to learn from all those three sources to create our new backend package. So the way I'm going to start is by adding backend package here. And what I'm going to do is I'm going to copy our recently created maths package JSON. I'm going to go inside of this package JSON and I will rename this from Workspace Math to Workspace Backend. And for the exports, I'm going to simplify it by changing it to be forward.\nslash convex and I'm not going to open an object. Instead, I will just directly go to forward slash convex and then I'm going to match every single TypeScript file inside. For the dev dependencies, I think this is okay. I don't think we have to modify this. Now, our scripts will be a little bit different and you can do that by going inside of this shared package. You don't have to go inside. I'm just showing you where I find my information from. I'm going to go inside of package.json and in here I have two scripts that I need to run. So I'm going to replace these two scripts with these convex dev and convex dev dash dash until success, which is our setup function. Notice how there is no build script, meaning that this package will not run when we do turbo build, which is exactly what we want. and now let's go ahead and see what we have to add so looks like I need to add the convex dependency and it needs to have a TypeScript dev dependency and I think that is solved by these two dev dependencies now so what I recommend you do is save this file leave it as it is don't add anything new here and now go ahead and do pnpm install that's it I just want you to do that and you can see it's already up to date nothing new has changed but I think it might I'm not sure if you have to run this or not let me just try and see there we go you can see that when I run this we have a new lock file package backend resource so that's why I told you to run it right even though nothing really changed you can now see that we are using the linking for the typescript config and the typescript into our package json in the backend package so now let's go ahead and let's add convex here the way we just learned how to do so using the filter backend add convex just like that and now you can see that in our dependencies we have convex so my version is 1.25.4 just in case you want to use the same version as me perfect so let's go ahead and see what else we have to do here i'm gonna go ahead inside of this repository again and i'm going to borrow this so they do npm run setup workspace packages back end so let me try and do this using pnpm here and let me try filtering to the back end and let me try run setup. Can I do that? There we go. It works. So what happened now? I ran PNPM and I filtered to our backend package and I said run setup. So what PNPM did is it filtered to the backend package and it used the package.json here to find the setup script. So now let's go ahead and I'm not sure if you are getting the exact same prompt here. I'm getting this prompt because I already have Convex connected to my account. In your case, you might be prompted with press enter to connect your Convex account. And once you press enter, it's going to open Convex website. And in here, you're going to have to confirm and then go back inside of your terminal. And perhaps you can try and run the command again. And then you should be seeing this kind of prompt right here. Now you can choose between an existing project, or as I said, you can create a new project here. So I'm going to go ahead and I'm going to call this echo tutorial like that. And I'm going to be using the cloud deployment and that will create a new convex project. And after the functions are ready, it should automatically close. Perfect. And you can see that now I have a new echo tutorial project right here. I shouldn't have any data. I shouldn't have any functions. I shouldn't have any files, nothing, a completely empty convex database here. And now in here, we have some new files. So the first file we have is .environment.local here. You can see that in here, it set our convex deployment to this specific name and convex URL to its specific convex URL. So you should definitely have that in here. And now we also have this convex folder. And inside of here, we have this underscore generated folder. This is what's important that we can share across our applications because that's where the type safety is. But we also probably noticed something immediately. There is no git ignore in here. Well, there is, right? But it's not exactly ignoring everything here. So what I'm going to do is I'm going to, I'm not sure actually if I have to add node modules here, because I think node modules is omitted by global git ignore. Yes, I think I don't have to manually add it here. But there are some things we can do to ensure that our backend package is set up a bit more strictly, right? because I think this is okay already, but you can see that in our math, for example, we have the tsconfig, which we don't really have here in the backend package. So what I'm gonna do is I'm gonna go inside of here and I'm just gonna check if they have it here. Looks like they don't. Let me see what's inside of their gitignore. So just the environment files, that's fine. Let me see their package JSON. So they have those two files. I think we're doing okay here so far. So here's what I'd like to do right now. I'm going to close everything here. And let me just do a little reload window here. Okay, the reason I reloaded my window is because you can see that my node module is only now grayed out. Because a second ago, it wasn't grayed out. So I was thinking, oh, is that being committed? And yeah, I can see now both of these are grayed out. So that's what was worrying me. I was thinking that maybe they are getting committed. Okay. So what do we do next? What we should do first is we should go inside of backend convex and we should create a schema.ts. And now inside of your schema here, let's go ahead and let's import define schema from convex server. And let's import define table from convex server. We can actually combine them. And let's export default define schema here. And let's add users to be define table. And let's give each user a name using a V from convex values. This will be a very simple string. Like that. And save this file. And right now, nothing should really happen. until you run turbo dev. And here's what you're going to notice now. We now have a new workspace backend package being run here. So right now what we're doing is we are running simultaneously our widget Next.js application, our backend workspace, our web Next.js application, and even this watch script for our math utils, which we added just as an example, right? But we use that example and that knowledge to now put convex inside of our package. Perfect. And after that, you can see it said schema validation complete, which means that if I now go inside of here, you can see immediately I have my user's table here. Perfect. And basically, whenever you develop with convex, you should keep TurboDev running so all changes are synchronized because that's how you develop with Convex. So if I now go inside of the Convex folder and create users.ts and if I export const query here my apologies get many and call query let me just I kind of forgot how to write queries so where you import query from is actually query from generated forward slash server like that and then in here you define the arguments and in the handler here make sure it's an asynchronous function. It's not an object, my apologies. So it's an asynchronous function. And in here, let's go ahead and simply do users to be await. And you have to add the context here. Context.database. Let me just check. database.query users.collect and return the users. So what we've developed now is a very simple getMany query which will simply fetch all users from our schema. And you can immediately see that here in the functions. We now have users getMany and we can run this function if we want to. It's now returning empty array. So this is another super cool thing about Convex. Debugging is super easy because literally everything you write in your backend is immediately synchronized with the dashboard here. So if it's easier for you to keep track of how many function calls you have, how the code even looks like, you can see we have the exact same code, all the logs coming from here or in worst scenario, errors, or if the execution time is too long, you can track all of that from here. so what I want to do now is I want to find a way to run this query in my front end here so right now let's go inside of web app page.tsx right here and I do want to keep just the apps forward slash web just so I always know what website what app I'm on so let me just go to localhost 3000 here there we go apps web so i know that i'm on that because you can see that my web dev is running on port 3000 that how i know perfect and now what i want to do is i want to find a way to fetch my database But there is a problem right So what I have to do is I have to import useQuery from convex React. This is a problem because I don't have convex installed inside of my web app. now I'm not sure if there is some tricky way to reuse the convex package from my backend internal package I mean my dependency here and honestly I'm not sure if that's what you're supposed to do I think what you're supposed to do is very simply pnpm filter web add convex I think that's what you're supposed to do. You're supposed to add a package convex, which you can see immediately resolve this error now. Because right now, if you're going to sort of package json for apps web here, you can see we have a new convex here. Perfect. So that now works, right? I should now be able to get my users from use query. But what exactly do I query? How do I get the type safety? Well, usually what you do is you import API and then you write API.users and then .getMany, the function that we just wrote. But how do we get the API here? Well, the first thing that we have to do is we have to go inside of apps, web, and we have to go inside of package.json here. And then what we have to do is we have to add at workspace, just like we did with math. We now have to add our backend dependency here, workspace, colon, asterisk. Let me just double check I didn't misspell workspace. And once you have added that, let's go ahead and let's just do pnpm install in the root of your applications. Looks like everything is already up to date. But as I said, I think pnpm log file has now been updated to link the workspace backend package properly. So make sure you do that every time you add a new dependency manually. And now, if you can set a page here, let's try and import API from workspace backend convex underscore generated. And as you can see, it's not exactly working, right? Now, I'm not 100% sure. let me just try rendering the users here let's do this JSON stringify users so right now if I refresh whoops I have to do throughbo dev if I refresh now I'm not sure what will happen okay so it looks like it's not just types we are actually not able to get to the generated folder where our type safety and API routes are so now we have to fix that The first thing I want to do is I want to go inside of my packages, inside of my backend, and in here I want to recheck my package.json here. I want to make sure that my export is correct. So what I'm exporting is my entire convex folder, and what that is mapping to is this convex folder, and then every file which is a TypeScript file inside, which should be the users, which should be the schema, which should be the generated as well. but it's obviously not working so it looks like we are missing something so one thing I know for a fact that we are missing in this package backend is the tsconfig so what if I just tried to copy the one from math let's go ahead and try and add it to backend here I don't think this will exactly resolve much the good thing is we've added the proper extents TypeScript config here. But for example, this compiler options make no sense for us. We don't even have the source directory, there will be no build, so there is no need for the dist directory. So what I think is that we can remove both of this here, and that we can remove this here and just leave node modules in here. And I want to go inside of UI, as I told you in the beginning, that I will do because I'm going to borrow their logic. So if I go instead of tsconfig here, let's check how this works. So what they're doing is they're adding base URL here in the compiler options. So I'm going to do the same. I'm going to add base URL and I'm going to just add a dot. That is my base. And then I'm going to add paths here like that. And I'm going to set the workspace forward slash backend here. In here, they are targeting source. So I'm going to extend this to be like that. And in my case, let's see. So they're targeting source. Can I just do this like that? I'm now going to just double check with my source code. I'm purposely not trying to, yeah, just this should be correct. So this is how I've set up my tsconfig file in my original source code. So let's see if this changed everything. So what's happening now is our tsconfig is exactly the same as our UI packages config, which kind of makes sense because the backend package by nature is more similar to the UI package than to any of these three here. So let's see inside of our app page, did anything change? looks like it did not change but I think there is one more thing that's different here and that's inside of web here so inside of here they have their own tsconfig and you can see that they have a path for workspace UI so now if I go ahead and do the same thing here so at workspace forward slash backend like that and if I try this again so forward slash forward slash packages and then I go inside of backend and then this let's see if this is doing any change here so looks like it is still not working. Let's see. Workspaces backend forward slash. It should be okay. Not exactly sure why this didn't resolve it. There is a chance that we might just have to restart our TypeScript server or reload the window. Looks like that's not it. And the issue is very funny it's missing a forward slash api i just forgot the proper import we successfully did it with this ts config that we just modified in the apps web i believe so now if i go ahead and comment this out for example and save you can see that it immediately throws an error here. So let's recap everything we need to enable our backend package to share the types to our frontend Next.js application. So let's start from the web application that we want. Instead of our web application, what we've added is we've added a package JSON with workspace backend. And after that, we have modified our TS config to add and support workspace backend the same way it supports workspace UI. The only difference is UI uses the source folder, whereas we do not use the source folder. And inside of the package itself in the backend, What we did is we added the package JSON. What's important is that we are exporting the entire convex folder from here. And in the TS config, it's important that we add the paths here, workspace backend forward slash, and we're just importing everything from here. And looking at this now, oh, we also have this include source. I'm not really sure if we need this. the reason well we definitely not don't need source because there is no source here but we could be just targeting the convex folder that could be one solution so we could treat convex as our source folder so what if I just changed the include here this is inside of the ts config in the backend package? What if I change the include here to convex like that? And let me actually compare again the tsconfig with the UI tsconfig here. So what they're doing is they are directly entering the source folder. What if I directly entered the convex folder and just targeted everything inside? And in the include, I just put a simple dot. the exclude can only handle node modules that's perfectly fine and let me just double check inside of their packages ui okay the node modules is outside of the source folder so that doesn't seem to be a problem perfect and now that i've done this i'm just thinking out loud here and i i've read some comments from you that you like to see me debug things so that's why i decided to kind include this here. Well, I hope you find it interesting. This is how I debug things. So now I'm going to go back instead of UI and I'm comparing their package JSON. So basically, I'm using things that I'm sure of are working with my examples and I'm trying to find the differences here. So let's see how they are exporting. So they have individual files being exported here. But a good example of what we need is an entire folder like this. So what they're doing, so what I'm doing is I'm doing .slash convex here, and I think this is the exact same thing. I think there is nothing I have to modify in here, so I'm going to close both of this package at Jasons. I'm going to close this, and I'm going to close the packages entirely, and now I'm going to go inside of the web again, and I'm going to focus, I think package.json is perfectly fine, but the tsconfig is the one I a little bit worried about so I think that I have to go inside of forward slash convex here and then another forward slash And then I think I also have to go inside of my app folder page And now this isn't working because I think I should be able to remove this. There we go. So now I feel like this is more natural. What I've done is I've modified my code, my TS files and my package files in such a way that my backend convex folder is treated as the source folder. Because why should I be able to import TS config and git ignore and package JSON from my workspace backend? This way I have remapped the alias workspace backend directly to the convex folder. So I think, I personally think this is the correct way to do it. And in here, yes, we still have a problem, but thankfully not related to convex. It's because this needs to become use client now. So just make sure instead of your apps web app page dot TSX, you have added this. Oh yes, we are missing the convex provider. Absolutely. I completely skipped the convex setup because I was so focused on Turbo Repo and adding this as a package. But this is super simple thing to do. Inside of your apps web, inside of your components, you should already have the providers here. And now what we're going to do is we're going to import convex provider and convex react client from convex react. like this. And then I'm going to remove the next themes because we really don't need them at all. And I'm going to add convex provider here. And I'm going to create a constant convex to be new convex react client process.environment next public convex URL or an empty string like this. And I'm simply going to pass the client to be that convex here. Now, don't worry, I'm going to show you the exact documentation for doing this. I'm just doing this because I've done it so many times, so I know it by heart. But I will show you the exact documentation where you can find it. But the reason I didn't want to approach this by looking at the documentation is because we are in a very specific environment and our goal was to add this as a package and not really think about the convex setup because what I wanted to do first was just add it as an internal package and use our knowledge from the maths folder and the UI package. And basically what we have to do now is we have to add next public convex URL inside of our web app here. So go inside of web here and let's check if we already have any environment files. We don't. So inside of here, inside of web, add .environment.local here and add next public convex URL. What is the URL you might ask? Well, it's the exact one as in packages backend environment.local. So the difference is in here, they don't have to use the next public prefix because they are the backend. They can read this directly. But in our front end, we need to expose that. So this is okay to be exposed. This is just the API URL. But so you have to put the next public prefix instead of next JS applications so that you can read it safely. And already it should be working. You can see a completely empty array here. And if you now go inside of your database here, and if you click add, and I'm just going to add a new Antonio user and click save, you can see it was immediately created and immediately reflected here. So you should now see an array with a single object inside name Antonio. Perfect. So now let me just go inside of here, support convex documentation. Let me show you how I know how to set up convex. So instead of a convex documentation in here, you have quick starts for next.js. And this is basically what we did. We did npm install convex, but in a very specific way, right? We did it using pnpm and we filtered to our backend package. And then instead of using just convex dev, we have a very special backend script here called convex dev until success. So we use that as the initial setup where we connected to our project. And we just did this. So they provided us with sample data here. We created a simple get many query from generated server. And this is what I just did. Convex client provider, right? But instead of creating a whole new convex client provider, what I did is I repurposed this existing providers.tsx because they have it for whatever reason. If you don't have it, you know, it's super simple. Just create a components folder and add it here. And after that, go inside of your app folder layout, import it from the providers folder and wrap it around your children. Just like that. Perfect. So that's, and that's exactly what they instruct you to do here. Perfect. So we're almost done with this chapter. What I want to do is I want to add this package to both of my apps now. So I'm going to go instead of app page.tsx and I just want to make this a little bit better. So let's add flex column here and let's add a div around these two and let's give this a class name of maximum with small with full and MX auto like this. And now they should load instead of somewhat of a reasonable container. I think I can also add null to, there we go. So now you can see each of them here. And let me add gap Y for and flex, flex call. Actually, no, this is a JSON. My apologies, this will not work. Okay, so do it exactly like this. And now we can copy this page.tsx and close the web app and go inside of your widget app, page.tsx and replace it entirely. And obviously, we get a bunch of errors right now. And if you actually try to go inside of your widget, localhost 3001, you will get a bunch of errors. So the first thing we have to do is we have to add our workspace backend here. So let's do that. Instead of widget app this time, go inside of package JSON here. Instead of dependencies, you can just go ahead and copy this one and give this backend. That's the first thing you're going to do. after that what I suggest you do is just run pnpm install and this way yarn lock will be updated once again linking the new workspace backend to the not apps web but apps widget there we go it's it's here somewhere apps widget packages backend so make sure you do that and after that you're gonna have to do pnpm f and this time widget add convex. So just convex like that. And now instead of your widget package JSON, you should have the convex and workspace backend added. But what's missing right now is the tsconfig instead of widget. So I'm not sure I remember it correctly. So I'm just going to keep the tsconfig from the widget open and I'm just going to quickly go inside of web tsconfig here and I'm just going to borrow this and add it here like that and I believe that this fixes all of our issues you can now go back instead of apps widget app page and you should have no problems at all and just change this to be apps widget and let's go ahead and let's do turbo dev and let's refresh localhost 3001 oh I forgot my apologies we also have to copy components providers here so copy the providers content go inside of widget components providers and replace with convex react and this and one thing we also have to do not gonna forget this time is copy the environment.local the same as in our web and paste it inside of widget So now you should have three environment.locals, one in apps web, one in apps widget, and one in apps backend. In here, it doesn't use next public, but in other two places is the same URL, but it has to use the next public prefix. And there we go. You can see that now our apps widget can load our database using convex and type safe APIs from both the widget and also from the other app. Amazing, amazing job. So that is the true power of monorepos. We are able to share a single convex backend with multiple Next.js applications. And I think that learning how to add the math package in the previous chapter helped a lot with this. but obviously without the UI example that was provided to us I don't think we would have figured it out but this just goes to prove that as long as you have a few working elements in your applications you can pretty much fix everything right you just have to compare with things that are working I just think it was quite interesting to let you watch me debug usually I just pause the video and show you the fix but some of you have commented that you want to see me do this so yeah this is how I debug things in this specific example great so one thing that we can do to wrap up this chapter is just go inside of backend convex users for example and let's do export const mutation and you also have to import it from here like so arguments let's go ahead oh it's not from here just a second it is from here you just can't name the constant the same way so I'm going to call this add like this and let me call this add open an object inside the first one will be arguments And in here let put the name of the user to be AV Actually, you can just set the arguments to empty for now and just go immediately in the handler here. And what you're going to do here is just await user, and my apologies, const user ID to be await context database insert into users and just add a name here because that's all we require in our schema. We just require a name which is a string. And you can return user ID. And here's the power of type safety that you're going to see now. So now that we've added this new function, the first thing that's important is that you have your backend running so the functions are now ready as you can see because we just modified them. And now, for example, if you go inside of your apps, web app page, and just above here, add a button, which you can import from workspace UI components button, another shared package. Let's click on add. And let's add a const add user here to be used mutation from convex react API users. Oh, I called it add. Let me just change to add. And this is super cool. So you can command or control click here and it will take you to the actual package here. So that's why you needed all of those TS config and stuff, right? It's well worth it to have that. And now button on click here. I'm just going to an arrow function, add user, and that's it. So now if you go to your localhost 3000 here, you can see that you can just add new users here, right? This is what we just achieved. And you can do the exact same thing instead of widget app page. So just add user here, import, use mutation, and a button from workspace UI components, add, on click, add user. and now you have the same type safety and functionality across two different apps one apps widget whoops and other apps web amazing amazing job so this was again a complicated chapter but i think it was well worth it so we resolved the lint error we set up our convex we create a schema of functions, all of these things. Now it's time to create a pull request to review all of these changes here. So I'm going to go ahead and do the following. I'm going to go ahead and stage all of these changes here. And I'm going to add a commit. So the way I add my changes, my commits here are dependent on the chapter. So this is zero to convex package. So in my case, it will be 02 convex package like this. But I'm not going to push it just yet. I'm going to create a new branch here, 02, and I'm going to call it convex package like this. And I'm going to publish the branch instead like that. And once you've done that, you can head into your GitHub repository. So inside of your GitHub repository here, you will most likely have a prompt like this. 02 convex package had recent pushes. And you can click compare and pull request from here. This is because we switched to that new branch, 02 convex package, and we just pushed the changes to there. And now if this is not showing to you for whatever reason, you can go manually inside of pull request, click on new pull request, set the base to be main, and then select the compare to be your new branch and click create pull request. So I'm going to create this pull request now to simulate a real Git workflow, right? And now we're going to go ahead and review this pull request. And now you're probably wondering, what is summary by CodeRabbit? And why don't I have this? Well, this is the AI review tool that I use to review my pull requests. It is especially useful because I work solo and having CodeRabbit is like having a teammate who gives me sanity checks on my code and confirms or warns me about serious security issues in my code and similar bugs. If you want the same thing, you can use the link that you can see on the screen or visit coderabit.ai and you can go ahead and connect it to your GitHub. And I'm also going to show you a very cool 100% free VS Code extension that they have that does the exact same thing inside of the editor. But I always found their pull request review tool extremely useful because of their summary, because of their walkthrough, and because of their sequence diagrams. So let's see, what was this pull request about according to CodeRabbit? And one thing I want to emphasize here is how well CodeRabbit's context works. So we introduced backend functionality for user management, meaning our user schema and the backend feature, implementing fetching and adding users. We integrated Convex as the backend service for both web and widget apps. So it recognized that this is a form of monorepo allowing us to add Convex as the backend service for multiple applications that we have. And of course we added a new provider set up to enable to connect to Convex. In here we have a more in-depth walkthrough which basically shows either a cohort or individual files and the change summary for every single thing we did. So if there's anything unclear happening, you can pause the video at any point or simply add CodeRabbit to your own repository and you will have these in-depth explanations of what you just did here. And here's a sequence diagram. So from the front end page, we are calling useQuery using API users getMany, which then reaches the Convex provider. and then we fetch all users from the convex API and then we return that users list and we provide the users data and a pretty similar thing happens here on a button click we add the user Antonio and we return it back here so you can see how well it recognizes what's going on in our app. It also tells you the estimated code review effort here so you know how much time to allocate to something like this. And here's another super cool feature. It won't let you get away with bad code. You can see that obviously I've just added this code here as a demonstration for the users and ad user. But CodeRabbit is not playing around. It definitely wants me to add a proper loading method. And here's a cool thing. It understands that I'm using Convex. Convex has a very specific loading state where only the undefined state is loading. That's how you know that CodeRabbit recognized our backend service is convex. So that's what makes CodeRabbit stand out in comparison to all other pull request tools. In here, it's obviously forcing us to write better code, but since all of this is just a demonstration and something that we're going to remove, it makes no sense to change all of that now. But when I scrolled down here, I noticed something. So the first thing it warned me about is inside of my backend package, package.json, it told me that I'm using an invalid wildcard and file extension. So that is pretty interesting. I think I can easily change this to be JavaScript because we do have JavaScript files inside of there as well. But I was just following the example from UI package. So I will research this a little bit. I'm not too certain myself if this is correct or not, but a good thing that I have to review this because it is warning me that it will break consumers running the package with plain node resolution. And here is the thing that we should probably refactor. We have a version skew between Eslint and our newly added package Eslint.js. so Eslint must match the exact Eslint version so 920.1 and 932.0 minor version mismatches have broken preset loading before so it's telling me that this kind of thing has broken apps before and that I should pin both to the same version like this to avoid invalid config errors so that's something for us to do in the next chapter thanks to this amazing tool. Again, if you want to have the exact same pull request review, you can use the link on the screen or visit coderabbit.ai. And after this, I'm just going to go ahead and click merge pull request and confirm merge. I will not delete my branch simply because I want to have a history inside of my repository like this, So I can always go back to that. And after you've done this, head back inside of your editor. And down here, you can see your branches. So just go ahead and find the main branch. You will see that you have two main branches. One is origin main and one is just main. You can select the one you used 12 minutes ago, but you could also select the one from five hours ago. It does not matter because our next step is going to be to press this. to push and pull changes like this. And now if you go ahead and click inside of your source control again and click inside of graph, you will see that we had a feature where we added TurboRepository and then inside of here, we just branched out for convex package and we merged that back inside of main. So just for sanity check, double check inside of your packages that you have backend with convex and schema and users and if you do it means you've done everything correctly here and you can click this button synchronize changes as many times as you want it will basically make sure that you are up to date with your github repository here perfect so what we've just did is we merged that detached\nbranch, convex package, back inside of main, instead of our main branch, and then we synchronize the main branch locally here to ensure we have those changes. I believe that marks the end of this chapter. Amazing, amazing job. You've learned, may I say mastered how to add packages inside of Turbo Repo by adding a very complex one, and I believe the rest of this tutorial will only get easier as we go along. Amazing, amazing job. In this chapter, we're going to be adding authentication to our project. We're going to be using clerk for that and we're going to follow special instructions to make sure it works with convex inside of our monorepo architecture. So just before we do that, I think we have an interesting issue from the previous chapter that our AI code reviewer alerted us about. So our S-Lint dependencies do not match. If you go inside of packages, slint config package.json, you might remember that we added this new package to fix the build error that we had. Now the issue is the slint version is lower than its dependency version here. So here's a little trick how you can find out what version this is supposed to be. So what you have to do is copy your slint version and then go inside of your terminal and do pnpm view slint at and then paste the exact version. And in here you will see all the dependencies and their versions. And we can find our slint.js version that needs to be 9.20.0. So go ahead and find slint.js here and change it to 9.20.0. And that will ensure that for slint 9.20.1, you are 100% certain that this is the correct dev dependency of slint.js it needs to have. Obviously, this isn't exactly breaking our project, but it is a useful lesson for us to learn how to do. And after you have changed that, make sure you are in the root of your application and simply run pnpm install so all log files are updated. And you should have two changes, one in the package JSON here and one in the log file. Now let's go ahead and let's add authentication to our project. So the first thing I want you to do is create a clerk account. you can use the link on the screen to let them know you came from this video. And after you've created an account, you're going to enter your dashboard here. I'm going to go ahead and create a new application and I'm going to call it Echo. As you can see, you can choose from a pretty impressive amount of providers here, but I'm going to stick with Google and email because they are the ones I use in my personal applications. After you have created your application, what I would recommend is that you don't touch anything right now, but instead head back to convex.dev or use the link on the screen. And after that, go inside of the documentation, authentication, and select clerk. And in here, you can find very specific steps for adding clerk with convex. Be mindful that the first example is for React. So make sure that you select Next.js or just scroll down until you find Next.js because that's what we're building with. So we already did step one and step two. Now it's time to do step three. We need to create a JVT template. So let's go back inside of our clerk dashboard, configure JVT templates in the left sidebar under session management and click add new template. By default, it's going to be a blank template. So you can select the template input and scroll down until you find convex and click save. What's important here is that you don't modify anything. The name needs to be convex. Leave the token lifetime and allow clock skew as it is unless you are fairly certain of what you're doing. What's important here is that you copy the issuer URL. So go ahead and copy this URL right here. For me it is pleasantduck77. And now let's follow the Convex instructions further. So as you can see they say this as well copy and save the issuer URL somewhere secure and do not rename the JVT token. It needs to be named Convex. So the exact steps that we did. And now let's go ahead and store that issuer URL under next public clerk frontend API URL. So where do we store it? Well only one of our apps will need clerk authentication and that's going to be our dashboard, the web application. So go inside of web, not widget, and inside of .environment.local, what I like to do is I like to separate my environment variables by their provider. So this will be clerk, next public clerk frontend URL. I think it's something like that. Let me double check. Next public clerk frontend API URL. What's important here is the next public prefix so that we can expose this environment variable and access it in our front end. Great. And once you've done that, you have to modify your alth.config.ts instead of your backend package. So let's go ahead and just prepare that file so you know where it is. Inside of the packages folder, find the backend folder, convex, and then create a new file called alth.config.ts. Let me just confirm that is the name alth.config.ts. and you can copy the entire content inside. It's not really complicated. So what we're doing here is we are exporting default an object and then the providers array and inside a single object. In here, I seem to be having this error that process is not something available here and it could very well be because of my types node missing. So the way you can quickly check if that is true or not or at least the way I would do this if I encounter this error is I would check my other files. So let's see, my UI package, which I'm using as a source of truth here, does it have types node? It does have types node. So let's see if we can very quickly fix this issue by simply running pnpm-filter backend add save dev add types node. And let me just confirm that that's the package that is missing, types node. Let's see. I'm going to add this from the root of my application. and once I go back there we go the issue was resolved and let's look at my new package json types node is 20 perfect so what I think that pnpm does I'm not sure and feel free to correct me but I think that types node was installed for the version 20 it could very well be because this is the latest version I'm not aware of what is the latest version of this package but I think that if in one of your packages you have types node version 20 it will find that in the log file and use the same version so you don't have any mismatches. I think I'm not 100% sure but that's something that I have noticed or at least think that I've noticed that. Now let's go back here so replace your own clerk issuer URL from the convex JVT template or with process.environment clerk jvt is your domain. Now here's what you have to do. What I personally like to do is I also like to add this to my .environment.local. So I am going to add it here as well and you can copy it from your web. So it's this one right here. But if you just do it like that, it's not going to work because they strictly instruct you to add this to your convex dashboard because this isn't reading your local environment. It's reading the convex cloud environment. So what you have to do here, I'm going to, I'm not going to close this documentation. I'm just going to open a new convex.dev here and I will log in and let's make sure that we are inside of our newest project. So echo tutorial here, you can see that I am in my development instance. And now I'm going to go ahead and go inside of settings here, environment variables, and I will click add. And cool thing is you can just paste the environment file. So when I go inside of my environment local for my backend package, I can just copy this entire thing and paste it like this. I don't have to manually move it around. And that's what we need to do. So clerk.jvt is your domain. Whenever you're working with environment variables, I advise you to copy them as much as possible because typing them manually is a recipe for a mistype. And then you're going to spend hours debugging why something's not working when it's a simple issue like a mistype. So please just copy it from here and then paste it here and then paste that here and then copy that here and ensure that it is the same one here. Great so now let's go back inside of the docs here. So we just did this step and now it says to deploy our changes. Let's go ahead and do that. We can do that very easily by just running turbo dev in the root of our application because that runs all of them including npx convex dev just to remind you because inside of our package json for the backend our dev script is convex dev so because of that we have practically run this command independently but we run it across all of our tasks here and you should be getting a successful function like this if you did everything correctly. If you haven't done anything, if you are not getting a success message, it could be a small mistake. One of the most common mistakes include wrong file names, typos here, typo here. So as I said, please copy these things rather than type it out yourself. And after we've done that, we have to install clerknext.js, but we have to be careful about where we are installing this. So let's use pnpm fweb because that is our application where we want clerk. We don't want it in widget. We only want it in web here. And let's do add clerk next js like that. And this will now add clerk exclusively into this package JSON right here for the web. So now let's see the next steps. Now we have to add our clerk environment keys, which you can find by going back inside of your clerk dashboard here. Let's go inside of the overview and let me find them right here. So set your clerk API keys. Just in case you can find them here let me try and find the API keys page for you so you know how to access them There we go So under developers API keys you can find this for Next So copy these two NextPublicClerkPublishableKey and ClerkSecretKey And make sure you are inside of your web app here. Head inside of the .environment.local and just below this NextPublicClerk frontend API URL, now add NextPublicClerkPublishableKey and ClerkSecretKey without the next public prefix. That is super important. So clerk secret key is essentially your backend key, something that should not be exposed to the front end. I'm making a tutorial here and I don't have any credit cards connected to my clerk account. So it doesn't mean much to me if someone copies this key and I will remove it afterwards. But make sure that you are very careful and don't share this with anyone because they can occur costs on your account if they find out your secret key. Perfect. So we have that set up. So let's go back here and follow further. Now let's add the clerk middleware. So I'm going to copy this very simple example here. And don't worry if you haven't copied it. It's super simple. You're going to see now. So instead of web, go ahead and create middleware.ts here and paste. So we are importing clerk middleware from clerk next JS server. and we export default clerk middleware here. And well, this config is not something you're gonna want to write. So let me show you multiple places where you can find this. One place is here from the convex documentation, but you can also at this point focus on the clerk documentation. So there we go. You can see that on my overview page, before I add any user here, you can see that it instructs me to create the middleware TS in the exact same way. So I can copy it from here too. is exactly the same. So let me also show you just one more way you can do that. I think that in here you can access the documentation and then in here middleware next.js there we go same thing. So I just shown you three places where you can find this code so you don't have to write it yourself. Perfect. So let's see what next. After we have added clerk middleware, we have to modify our convex provider. So if you remember what that is, inside of our web app, instead of components, we have providers.tsx. And now we're going to have to slightly modify it. So we access, so we add clerk to the convex provider. so what I'm going to do is I'm going to import convex provider with clerk from convex forward slash react dash clerk let me just add that here there we go convex provider with clerk from convex react dash clerk and then I'm going to add use out from clerk next js and then let's go ahead and well if you want to yeah you can add this error message I mean I think we should have had this from before, but it's not the end of the world if you don't have it. It's basically this base next public convex URL. Without it, the app can't work. That's true. So yes, you can add this if you want to. But now let's focus on clerk again. So now we have to, let's see, convex react client. So this stays the same, but we are no longer using convex provider here. Instead, we're using convex provider with clerk so just replace this existing one with that and we have to add a new prop here called use out so let me just assign use out here there we go use out and this should now work just fine uh and we have already wrapped the clerk uh oh so let me just see so uh uh-huh I see okay so I'm gonna go ahead and import clerk provider here actually I will follow the docs to a dot so I'm gonna go inside of web layout let me just go my apologies app folder layout so we already have the providers so I'm going to for And now just do the exact thing they do in the docs, just to be sure. So they import clerk provider from Next.js and they wrap the convex provider around like this. I'm 99% sure we could have done this in the providers tab as well. But, you know, I just want to stay true to the documentation just in case I'm not thinking ahead of something that might happen because of maybe this or maybe this needs to initialize first. I'm not sure. So that's why I'm going to follow exactly as the documentation says. And now let's go ahead and let's show UI based on authentication state. So at this point, I think that we can safely do Turbo Dev and we can focus on our web task, which is running on localhost 3000. So let's get that open here. Right now, all it should do is just load our convex database users here. But now we're going to mark that as authenticated. So let's go ahead and go inside of app page.tsx right here. And I'm going to import authenticated and unauthenticated from convex react. Well, all of these can actually be imported from the same place like that. And now let's go ahead and wrap this in authenticated like this. And let me just wrap this entire thing in a fragment here. And let's go ahead and add this as unauthenticated. and in here I'm just going to say must be signed in and I think we can also add authenticated loading or maybe not. Let's just leave it like this. So now you can see that it says must be signed in and I think that even in their example they also add the sign in button and the user button from clerknext.js So let's add those two. Sign in button and user button from clerknext.js. So in the unauthenticated, let's add the sign in button. Sign in. I think that's how you use it. And in here, let's add user button, which you use like this. So let's try that out again. So you should have this sign in text, which when you click should redirect you to the login page, which right now you're probably noticing is a different URL. Don't worry, we're going to change that as well. So for now, let's just try creating an account. So I've used Google login. And after that, there we go. I am redirected back and I can see my user button here, which is where I can find some information about my account here. You can go inside of security to see your IP address and all the places you're logged in from. and the place where you also sign out from. Perfect. So that is what we wanted to achieve. But there is one problem and one inconsistency now. So you can handle protected and public states in many ways using Convex and Clark. One way is by using these authenticated and unauthenticated states. another way is on the back end where you use the identity so I think they have a short example of that as well there we go use authentication state in your convex functions so you should always make sure that you have identity inside of your convex functions for example let's do this now I think it's important for us to explore this so we learn go inside of your packages back end convex and go inside of your users here and for the add method let's go ahead and make sure that from context we can await context out get user identity and then we're going to check if identity is equal to null it means that we are not authenticated and we don't let the user create a new account and now what I'm going to do is well here's what we can do we can just go to forward slash we can just go to 3001 because in here we have a widget and if you try adding a user now you're going to get an error because you need to be logged in right you remember that in the widget which is running on 3001 we didn't add authentication right so this is screen is still normally showing even if we are not authenticated but you can see that our api is still protected so even if our front-end validation fails, our backend routes are protected. That's what's important. And if you go back to 3000 to apps web, you can see that we must be signed in to see this in the first place. So let me just sign in again. And now in here, I should be able to add users. There we go. I can add users here because I am signed in. Perfect. Excellent. So let's go ahead and see what else we have to do. So the next is tutorial for something else. But what I wanted to talk to you about is that we now have an inconsistency because we are using these components, authenticated and unauthenticated in convex React, but our middleware, which should technically do the exact same thing, isn't doing that. So I want to show you how you can use the middleware to do that. So let's go ahead and let's import from clerk middleware here create route matcher and in here I'm going to set const is public route and I'm going to set create route matcher like that and this is what my public route will be. My public route will be all sign in routes which we don't yet have but we're going to create all sign up routes and that's going to be it. That's the only thing that's going to be my public route. And now inside of the clerk middleware, what I'm going to do is I'm going to extend it to be an asynchronous method, which gets the auth and the request like that. And from here, I should be able to extract user ID and organization My apologies no organization yet just user ID from await alph and you can import alph from here i believe uh actually my apologies we have alph sorry and then i gonna check if not is public route and just passing the request i'm gonna go ahead and trigger await alf.protect just like that so that's the only thing I actually have to do I don't think I have to really check for let me just see I don't think I have to even do this I think this should be enough so now even if I don't have the let me just try it like this let's get inside of web app and let's create a new test folder and inside page.tsx. And in here, let's go ahead and do an export default page, test page. So what's important here is that you do a default export and that it is called page.tsx inside of a test folder. And now we can go to localhost3000 forward slash test and you should see the test page like that. So here's what I'm going to do. I'm going to go back and I'm going to make sure I am signed out and then you can see that already this is working so if I try going to localhost 3000 forward slash test I'm immediately redirected here so that's what we just achieved with the middleware we didn't have to use the components here but let's try this what if I add forward slash test here and then manually go to localhost 3000 forward slash test. You can see that I can now load it even though I am not logged in because we have allowed it to be a public route. And you can probably notice that you can use the same logic to do the reverse, right? So if you will have more public routes than private routes, then you can use is private route. And then test will be the private route and then just use that here and reverse the logic right so however your app works you can adjust it i'm going to be using the public route method because i will have more private routes than public routes so we just use the test to demonstrate how you can still control your authentication methods even without convex components like unauthenticated and authenticated so what i want to do now is i want to fix this little feature that when we get redirected, we get redirected to another page. This is not on localhost. So let's go ahead and fix that by going inside of apps, web, app, and in here, let's create a route group called alph. So route groups are defined inside of parentheses, and they are not going to become the part of the URL. So you've noticed that when I create a page file inside of a folder in Next.js that becomes the part of the URL. But when you add that inside a parenthesis, that doesn't happen, which allows you to structure your folders more cleanly. So I'm going to create a sign in route. And then in here, I'm going to create a catch all and sign in again. And then I'm going to do page dot T S X. And I'm going to explain what this is in a second. Just make sure for now that you are using double square parenthesis here and spreading the sign in. Make sure it says sign in here, sign in here. And in here, I'm just going to go ahead and return a page and I'm just going to return sign in from clerk next JS like this. And then I'm going to copy this and paste it and I'm going to call this sign up. And then in here, I'm going to go ahead and rename this to sign up as well. And in the page, I'm going to import sign up. just like that. So basically what we're doing now are further steps in clerk. So right now, if I go ahead inside of the documentation for clerk here, we already did the quick start for Next.js, right? We added the clerk middleware here, we added the clerk provider, all of these things are done, we've tested those things. And now in here, we have next steps, creating a custom sign in or sign up page and we just created this so that's how I knew what to do because I already know it by heart but this is where you can find that information and if you're interested about what this weird syntax is it is the nextgs optional catch all route so basically it's gonna allow us to add a bunch of parameters inside of there so that we can redirect the user back to where they wanted to access while they were not logged in and in here they are not using the alf route group is just something I like to do. I like to keep all of my authentication routes together. I don't want them to just be on the same level as test and page. So that's why I'm using the route group. I think it's a cool feature that Next.js allows us to do. And you import these components, sign in and sign up. That's exactly what we did. And we already created the public route for sign in instead of our middleware. Let's just double check that middleware. We already did sign up and we also prepared for sign in and I think we created both routes here. And now what we have to do is we have to add these to our environment variables here. So let's go ahead inside of the web app here dot environment dot local here and under clerk add next public clerk sign in URL to be forward slash sign in. Next public clerk sign in fallback redirect to be a forward slash and the same thing for sign up like that and now let's go ahead and just finish the next step which is custom sign up page so we already did this we already made the sign up public and now we just have to add our next public clerk sign up url just like this and now that you've done this, I'm fairly certain that we are ready to test our app. So let's go ahead. Let me just see where am I here? So let's go back to localhost 3000 here. And you can see that now we are seeing our sign in route instead of seeing someone else's sign in route. And what you can do to center this is very simple actually because we are using this route group we can leverage it by adding a layout file here and you can go ahead and create a layout extract the children from here give it a type of react my apologies children children are a type of react react node and in here you can return a div and render the children inside and create a class name here. Flex, let's actually do minimum height of screen, minimum width of screen, height, my apologies, flex, flex column, item center, and justify center. So I'm going to explain what this layout file is. But you can see it's a reserved file name that also needs to have a default export. And what it does is when it's placed under a route group like out, it will use the same wrapper around all routes that it is next to, right? So now you can see how this is centered. And I think I just forgot to do, maybe I need to do height full. Flex call item center, justify center. There we go. Once I do that, it is fully centered. And regardless if I am on sign up or sign in, it's going to be the same thing. because if I click on sign up, there we go. Same thing. So that's how you leverage the layout file and the auth route group to create common styles for two different routes because we want both of them to have the exact same centering method. So that's what we achieved with the layout.tsx files. Perfect. Let's see if there's something else that we have to do now. So I think this is a good place to wrap up the chapter. We still have this weird state where we are protecting both with our middleware, but then also with our authenticated and unauthenticated components. And the truth is, we're going to need both of them, but just not in this way. We're going to create a new component in the next chapter that will be used to properly protect this in an even different way. But I think this is a good place to start because we are starting to dive in into some more Next.js specific stuff. So I don't want to confuse you too much. But basically, this out folder is a special folder in Next.js that allows you to structure your routes. The reason it's used to structure the routes is because itself, it's not a part of the URL. You can see that whenever you create a new folder instead of this app folder and put a page file inside of it, it means that this folder will become the URL. But when you add parentheses around, it will not become a part of the URL and it will only start becoming the part when it reaches the first normal named folder. and this specific clause is a catch-all that clerk recommends when building the local sign-in and sign-up views so I personally never really use it but I think for you it is enough to know that clerk requires this so it can pass all the random parameters and so that it can redirect the user to where they come from once they log in that's what this is it's just some special folder conventions inside of Next.js. So what we learned in this chapter was how to connect clerk with Convex using a JVT template. We've learned how to add the environment variables to our packages and to our web components. We have modified the Convex provider here and we also added some environment variables to the Convex cloud dashboard and then we learned how to protect our app in three different ways. First, we use the normal authenticated and unauthenticated components. After that, I told you that you can protect your backend routes using the identity, which is exactly what we did for whoever wants to add new users. We made sure that they have a context.auth available. And if they don we throw an error And we saw an example of that when we went to localhost 3001 which is where our other component other app is hosted And you can see I keep getting errors because I not logged in in this different application in our monorepo. So that is working perfectly because this is the actual important ALF, right? Frontend can always be hijacked in some way, right? It's just JavaScript. But this is on the backend. This needs to be protected. it ironically this is also javascript but i think you get the point that i'm saying right and then we learned how to use the middleware which is another layer of security and in my honest opinion you should never rely on the middleware to protect your stuff so as i said the most important thing here is the back end that's what's important that's what you need to protect your routes for so you shouldn't ever do you know forward slash api imagine this says protected routes. So you shouldn't put your API folder inside of the middleware and just think, oh great, API is protected. No, never do that. Always explicitly protect your API routes like this. Always, no exceptions. Excellent. So I think that we are ready to end this chapter and review our pull request. So let's see everything that we did here. So I'm just going to go ahead and close some of the things here. So we have a clean slate. 03 clerk authentication. So I'm going to go ahead instead of my source control, always be careful not to accidentally click this one because this will discard all the changes. Always click on the plus icon here, stage all changes. Now it says staged changes. 03 clerk authentication. And let's click commit. And before I click sync changes, I'm going to go down here and I'm going to click create new branch 03 clerk authentication. And then instead of sync changes, I'm going to click publish branch. And once that is done, you can go inside of your GitHub and click compare and pull request. Let's go ahead and create this pull request. And now we're going to go ahead and review it. So let's see what we did in this pull request. Summary by CodeRabbit. We added authentication integration using clerk, including sign-in and sign-up pages and the middleware to protect routes. We also introduced a test page accessible within the app. So that is the test page that we created right here. The main page now displays content based on user authentication status, showing user-related actions only when signed in. So that is our convex React authenticated and unauthenticated state wrappers. We enhanced provider setup to support clerk authentication across the app. And user creation now requires authentication. That is our API protection here. Perfect. Amazing, amazing job. So in here, we have a more in-depth walkthrough. So this is what we did with our ALT route group and the layout files. So we created a new centered layout for authentication routes and new sign-in and sign-out page components rendering clerk sign-in and sign-up respectively. We added app layouts and providers and we added authentication middleware, authentication aware main page, a test page, clerk integration and out convict within convex as well as some dependency updates such as slint js downgrade and node types so we can access process.environment. In here we have an interesting sequence diagram explaining exactly how our app works right now and this kind of stuff will come in very handy especially when we start to build more complex things. Great amazing amazing job. I'm going to go ahead and merge this pull request right here. And then what you're going to do is go back inside of your main branch and press on these two buttons and click OK. And that will synchronize all changes. And then when you go inside of your source control and go inside of the graph, you can see that we detached to create clerk authentication and then merged it back to our main branch. So just for a sanity check, make sure you have sign-in pages, for example. Perfect. Amazing job. I believe that marks the end of this chapter. So we resolved the S-Lint issue. We set up clerk with Convex. We created an account, JVT template, middleware, and pages. And we completed the GitHub process. Amazing, amazing job. And see you in the next chapter. in this chapter we're going to implement organizations feature in our project and the reason I want to do this now because it is very closely related to authentication which is still fresh from our last chapter so the first thing I actually want to do is I want to implement a component called alph guard so the reason I want to do that is because right now our state of the app is a little bit weird. If you go inside of your web app page.tsx, you can see that we are using this authenticated and unauthenticated components to handle front-end authentication, but then we do the same thing in the middleware, right? So it's not exactly consistent. So this is what I want to do next. Inside of your apps web, go ahead and create a new modules folder. and inside of here go ahead and create an alph module so this is the file structure that we are going to use and inside of here create the ui folder create components and now also create layouts and create views like that and now the first thing we can do is we can create the alph-layout.tsx and this one is quite easy you can copy it by going inside of app auth layout. So just copy the entire thing and paste it inside of auth layout, except it's going to be a little bit different. So it's going to be an export const instead of export default, and it's going to be called auth layout, because this is a specific component for the auth module. And the rest can stay the same. And now what we're going to do is we're going to go back inside of app auth layout.tsx. And we're simply going to go ahead and import auth layout from modules auth UI layouts. This is the way I love to write my apps instead of Next.js. So I like to use the app folder as the router and the loader but I like to develop my components in the modules folder so I have them categorized together right here. So we just created this very simple replacement of the layout. Now is the same thing with the views. So I'm going to go ahead and I'm going to enable signinview.tsx and I'm going to go ahead inside of app, out, sign in page and I'm going to copy the entire thing here. So now let's go ahead inside of the sign in view and let's paste it here and let's just do an export const sign in view here and remove the default export. And since it's super simple you can copy and paste the sign up view here as well and you're going to see why it's actually useful to do this other than just structuring your app in a second when we develop the out guard component so let me just rename this to sign up view and change this to be sign up there we go so we now have these three new components so now what i want to do is i want to go inside of apps web app out and i want to go instead of sign in and in here what I'm going to do is very simply return sign in view from modules out UI views. And then inside of my sign up, same thing. Sign up view like this. And we can remove the unused clerk import now from both of them here. Perfect. Nothing should really change now. This should all continue to work just fine. But now we're going to go ahead and create a third component inside of our web modules, auth, instead of components. And we're going to call this auth guard.tsx. And it's going to be marked as use client. And we're going to import authenticated, from convex react, unauthenticated, and outloading. So these three from convex react. And then I'm going to import outlayout, from layouts, outlayout, and I'm going to import sign in view from views sign in view. And let's go ahead and do export const out guard here. Let's go ahead and assign the children. Oops. We react, react node, and return the arrow function. And destructure the children from here. And now in here, go ahead and return a fragment and add outloading as the first one. And now simply add outlayout here. And in here, we can just add a text loading like this. Go ahead and copy this. And now instead of using outloading, go ahead and add authenticated. And this will simply render the children. So if we are authenticated, I'll allow whatever content is going on. and for our last one you can copy the one from above and change outloading to unauthenticated and again instead of the out layout simply add the sign in view so what have we just created we have created an out guard component that we can use to wrap any single thing inside of our app and while the out is loading it will display the loading state in a full screen centered state. So this loading test will be in the center of the screen. If we are authenticated, we will allow to render whatever we've wrapped this component with. And if we are unauthenticated, we're going to simulate a sign in view. Why not sign up? Well, you can use sign up, but here's the thing. Our middleware will handle this either way. I'm just trying to make a consistent script here. And the reason I'm even using out of the guard and not just\njust completely remove it is because you do need to have this outloading because that basically tells you, okay, Convex has loaded clerk. Because if you just rely on the middleware itself, it will still work, but your app will get errors now and then because sometimes unauthenticated routes will leak before it loads. So that's why I highly suggest developing this auth guard component. And once you've developed it, now I want to go inside of my layout inside of the web app layout. And what I'm going to do now is I'm going to wrap my children inside of the auth guard. And I want to show you what will happen now. So let's go ahead and do turbo dev here. So make sure we are running our app. And I'm going to go on localhost 3000 here, because that's where I'm rendering my app here. So right now everything works correctly here. But there is actually a little bit of a problem here because I think that even if you click on sign up, you can see that it breaks. Something's obviously wrong. So I'm logged out right now. If you are not, make sure you're logged out. You can see that when I click on sign up, something is going on, something's not correct. So what's actually going on here? When we added out the guard in the root of our layout, we have prioritized this code over everything else. So you probably know what's going wrong here. When we reach our out here, and when we reach our sign up route, what happens is that it is trying to render a sign up view, but we don't let it because we have our rule, which says don't render the children, render the sign in view. So that's what's going on here. So this is what we're going to do instead to fix this issue. We're going to go ahead instead of apps, web, app, and just as we've created alph, we're now going to create a new one called dashboard, like this. And you can remove the test one. We no longer need it. And then go ahead and drag and drop this page.tss inside of the dashboard. And if you are being asked to update imports for page.tsx, you can press yes. Basically, what was just updated when I pressed yes was this weird types file. Feel free to save that. It doesn't matter. You can see that now I have this next folder opened. You can just close it. It is this .next cache folder. It's not an important folder and it gets rebuilt every time you start your app. So don't worry, even if you have errors here, it absolutely doesn't matter. So now that I have moved my root page.dsx into the dashboard folder, I can now create a individual layout file just for this route group the same way I did for the auth route group. So I'm going to go ahead and I'm going to do layout like this. And what I'm going to do here is I'm very simply going to add my auth guard here and then I'm going to render the children inside. So let me just assign the props here. Children, react, react node and the structure, the children. Children. Just like that. So now our auth routes will independently render themselves but if we encounter, if we attempt to go inside of the dashboard, we're going to protect it both with the middleware and with the outguard. I know this is a little bit confusing, but it is what you need to do to be consistent and consistently use both the convex protection and the clerk middleware protection. And now what's important is that you go inside of your root layout here and remove the out guard from here completely and remove the import here and you can see that now I can switch between sign up and sign in without any problems whatsoever and also now if for whatever reason our middleware fails so let me just comment out this entire thing and let's try and go to our localhost 3000. Okay, I can't just comment it out. So what I'm going to do instead is just comment this out. There we go. You can see the same thing. We're still protected even though I just turned off the middleware. And that's because we have protected our entire dashboard which holds our page.tsx here. Oh, and we can also remove all of these authenticated, unauthenticated, We no longer need those at all. So you can remove all of them and just leave the normal page here. And you can also see a little error happening here. So the error is quite simple to fix. Actually, what you have to do, and I'm going to tell you why it happens. It happens because it is expecting this component, sign in view, to be rendered inside of a catch all route. But we are currently not redirecting to these routes. because we disabled the middleware. What we're doing right now is we're using the alt guard to replace the children with the sign-in view. So simply go inside of your sign-in view and change the routing to hash. And you can do the same thing for sign-up view. And once you do that, you're not going to have any errors whatsoever. But yes, right now we can see that our localhost 3000 is not exactly on the correct route. right? The correct thing is displayed but I really like when it redirects me to sign in and sign up so what I'm going to do is I'm going to enable back my middleware but what we've just achieved is consistency so both our outguard component and our middleware now use the exact same logic. There are no inconsistencies between the two. That is what I wanted to achieve. So if you now go ahead and log in, you should just see your normal page like this. And you should be able to visit any other page. There shouldn't be any problem at all. In fact, it should be identical behavior to the ones we had so far. So what I want to do now is I want to enable organizations. So the way you can enable organizations is by going to clerk. You can use the link on the screen and go inside of organizations and click configure organizations and click enable organizations just like that and what I want you to change right now is change the default membership limit from five to zero like this uh my apologies one and click save why are we doing this so the reason we are adding limited membership here is because we are going to make this organization feature from clerk self-sustainable. Organizations are going to be a premium feature in our application, which means by default, each user that joins our app will have their own organization, right? But if they want to invite users, we're going to make sure that they have to upgrade first. Because when you have organizations that have two or more monthly active users inside, that counts as a premium organization on Clerk and it is billable. So this way, your users are going to sustain their costs, which is very important in a SaaS business. So that's why we are limiting the membership to one so that only one user is allowed in organizations. Perfect. And same thing, I'm not really sure. You can allow users to create as many organizations as they want because clerk does not bill you for number of organizations. They bill you for number of active monthly organizations, which as I said, is defined as a minimum of two monthly active users in an organization. So that's why we are making sure that by default our free users will not incur any additional costs. That's why this is important. So make sure that you've enabled that and make sure you have set the limited membership. And what you can do now is inside of your web app, auth, my apologies, dashboard, page.tsx, below the user button you can add organization switcher from clerk, next.js. And let me remove the sign in button and what this will now render here okay sometimes it doesn't register immediately you you will most likely get an error because it doesn't register immediately that you are and that you have enabled organizations but now as you can see we have our personal account as an organization and I can click create to create a new organization but this is a little bit of a problem because I don't really want anyone to have a personal account as their organization. I want every single user of mine to have an actual organization before they are allowed to use my application. So thankfully Clark has a guide on how to hide personal accounts and how to force organizations. So the first thing we're going to do is we're going to add hide personal prop here. So go ahead and add hide personal to the organization switcher. And you can see that already it will simply say no organization selected because that is the true state of our app. We are currently not in any organization, as you can see right here. No organization selected. So now what I want to do is a similar alpha guard, but this time it's going to be organization select guard. So let's go ahead and develop that. I'm going to go ahead and close everything here. And I'm going to go inside of my apps inside of web modules inside of alphui components and I'm going to develop organization guard dot tsx like this. I'm going to mark it as use client and now I'm going to add a couple of imports. I'm going to import use organization from clerk next js and I'm going to import alph layout which we created recently. And then I'm gonna go ahead and export, whoops, export const organization, whoops, guard. I gonna go ahead and assign the children here React React node I going to destructure the children And I'm going to return a div rendering the children. So let's go ahead and let's immediately render this new organization guard inside of our app dashboard layout. So right after alph guard, add the organization guard. So the first thing we're going to ensure is that we are authenticated. And after that, we're going to ensure that we have an organization. So this is what we're going to do. I'm going to grab the organization ID from use organization. And I'm going to do if there is no organization ID, return div, create an organization. otherwise we don't even need a div we can just use a fragment like this and let me just check what are they doing correctly here oh it is organization not organization id so if there is no organization present we're going to show to the user hey you need to create an organization and we're going to do that inside of the out layout actually and there we go you can see that now our user can't continue looking at this app even before it creates an organization. Now let's go ahead and create a organization select view. So instead of out views here, go ahead and create org select view.tsx, import organization list from clerk next.js and export const organization list. Actually, let's do org select view. And in here, you're going to render the component we just created. And you're going to give it after create organization URL to forward slash. After select organization URL forward slash, you're going to add hide personal. And you're going to add skip invitation screen. All of these are important. and once you've created org select view here, you can go ahead and import it here, org select view, like this, and I like to be consistent, so modules, views, out UI, there we go, and you can see that now, every time a user tries to go to our localhost 3000, which is not a sign-in page, right, because out pages will obviously not be interrupted by this, they will allow you to sign in. But if you sign in and we notice you still don't have an organization, after you sign in, we're going to make it mandatory for you to create an organization first. And then again, we have a problem because this is now enforced through Organization Guard, but it is not enforced through the middleware. So let's go ahead and do the following. We now need to create our organization select page. So let's go ahead and do that. So I'm going to go inside of apps, inside of web app out, and I will create org selection. And inside a new folder, catch all org selection like that. And inside a page dot T S X, and I'm going to import my org select view. and I will simply do a default page export here and I will simply return org select view. As simple as that. So now my users will be able to, we're going to have a safeguard if for whatever reason the middleware fails and the user manages to access this dashboard route group, what's going to protect us is the organization guard which will not allow it to render the children and instead force this org select view component. But if the middleware works as intended, it will redirect the user to forward slash org selection because that is the same way it works for the signup and sign in routes. So now what we have to do is we have to revisit our middleware once again and we have to modify it so it works with routes. So what I'm going to do is I'm going to create a constant is org-free route, meaning that for this route, an organization is not needed. So I don't want this to interrupt the user's flow if they land on the following routes. So create route matcher. And we can copy this too, because we're absolutely going to have to be able to access these without being interrupted. and the other one will be the org selection. So let me just see how did I name it. Make sure you always check org-selection like this. Org-selection and everything inside. So these are the routes for which I don't want the organization guard, my apologies, for which I don't want the middleware to redirect because it will cause an infinite loop of redirects this way. So the user needs to be able to land on this route without any redirect happening for the organization case. So now what we can do is we can bring back our extraction of user ID and organization ID from out. And after we resolve the is public or is it not public route, let's check if we have user ID and if we don't have organization ID and if this current route is not organization-free route, so make sure you put exclamation points here and here, then what we're going to do is we're going to create new search params, new URL search params, and add the redirect URL. to be request.url and create the org selection to be new URL. In the first argument, open backdix and write org selection. Again, make sure that this route is exactly the one you've named here, org selection, like this, and simply assign the search params to string, like that, and request.url as the second argument here. And return next response, which you can import from next server. Return next response dot redirect org selection. Just like that. So what you've created now is the exact same logic that we developed in the organization guard, but on the middleware level. So again, double protection here, but also consistent protection. So you can see that now when you refresh, it looks the same, but take a look at your URL. You are now on the proper organization select URL, right? So if it's not working for you, you've probably misspelled your organization selection somewhere. Make sure that you're being consistent, right? So what we can do here, inside of out, org selection, maybe we can rename our org select view to be org selection view. and change the name of the component org selection view. So this way you don't have to keep thinking, oh, did I name it correctly? So org selection view here. I've just modified this instead of the auth page. And now I also have to modify it, I believe, in the components of organization guard. There we go. Org selection view. So now the exact same thing will happen, right? If you refresh, everything should work just fine. But again, if you go inside of your middleware, and let's imagine the middleware fails to work. Somehow someone bypasses it. What happens if I try to go to localhost 3000? I need an organization. And if this example is not clear to you, what you can do here is go inside of the app, inside of dashboard, and create a test route. Page.tsx. SFC page. Div. Only authorized. only out and org can see me. So if I try to go to forward slash test without middleware enabled, you can see that I'm still being protected because of our organization guard. But the URL is visibly not redirected here. So when you enable the middleware, the exact same thing happens when you refresh but you also get redirected to the proper route right here so that is what I wanted to achieve here so now let's go ahead and let's create a little company name here like that and there we go you can see that now you always have one active organization and if you actually go ahead here and try to invite someone and click send invitations you will get an error. You have reached your limit of one organization memberships. That is exactly what we want. We don't want free users to invite new users because that will create additional costs on our account. So this way, only those users which pay a monthly subscription to us will self-sustain their own costs, which is what's important in a SaaS business. Amazing. So I think this officially wraps up everything we wanted to do here. Let's go ahead. We've implemented the auth guard and we properly use the authorized and unauthorized. We moved all of our things to the dashboard shared route group and the new layout which protects everything. We enabled organizations and we created the org select page as well as organization guard and we limited the members count as well as modifying the middleware to ensure organization is active. So if you're wondering how did I know what to write here well I just followed this guide I just forgot to show you further my deepest apologies here so yes basically we hide personal on both organization switcher and organization list here and in here they are showing you many ways you can you know check for organization if you want to keep it in the url basically i just kind of extracted a combination of the two of them and did it they even have some further instructions on how to synchronize with the url but we don really need that in my case In my case all I needed was this the redirect if there is no organization present Amazing. So one more thing that I think it's important for you to know here is to enable organization on the backend. Because right now in your users, for example, there is no way for you to know if the user is part of an organization, but that's going to be crucial because we are going to be multi-tenant and we're going to scope some actions and protection only for those users in a specific organization. So in order to do that, it's actually quite easy. You just have to revisit your JVT templates here, click on the Convex template, and in here you have the session token. And this is everything that's available for Convex. Name, email, picture, nickname, updated ad family name all of these fields are available here in the identity but you can see that there is no mention of an organization here so add a comma add a new field and in here what I like to do is I like to write org id here and simply go ahead and find under organization org id like this and make sure you have a little comma here that's honestly the only thing I need for this project. So now what you're going to be able to do is the following. Const org id will be identity.org id as string. Why do we need to type it as string? Because without it, it's going to be a type of any, because they can't really know what you're going to add to this claims field. They're not type safely connected. So for this thing, we have to write it like that. So if there is no organization ID here, let's throw new error here, missing organization, like that. And this way, we will be able to store the organization ID in the database and then compare every single time someone tries to access a record, do they belong to that organization ID. So if you've done everything correctly here, you should be able to click add in the web view, you can see how it works. But again, if you revisit your localhost 3001, and let me now just do this, I'm going to skip this part, and I'm going to add a question mark here, simply because I just moved to localhost 3001, if you haven't seen. So go to localhost 3001, make sure it says app widget, and click add. Now you will have a different error, missing organization. So I just wanted to show you that that works as well. So you now know everything. Your app is now fully protected. Nothing can break your app and you are forcing organizations on localhost 3000. So the widget will not require authentication or organization. That's going to be the customer facing chat box. So obviously we won't burden them with creating an organization or out, right? Why would we do that? We want them to have a very quick way of sending us a new message. Perfect. So right now, if you go ahead and, for example, go inside of your organization and delete it, let me just delete it, what's going to happen is you will get redirected to create organization, right? You need to have some kind of organization present. And if you sign out, you can see you need to be logged in. So our app is now super duper protected. that nothing can break it and we are protected both by the middleware and by our dashboard route group. So yes, just remember that when you develop that yourself, if you do it after this tutorial, you're going to have to put things inside of this dashboard to ensure that clerk and convex are working together on protecting your app, right? So that's why I've created this dashboard route group separated from this out route group because these fields don't have to be protected. So we are utilizing both the middleware and secure API routes and secure front-end protection. Amazing, amazing job. Let's go ahead and merge all of this. So this is called 04 organizations. So I'm going to go ahead and commit all of these 04 organizations, commit. I'm going to go ahead and change my branch to 04 organizations and I'm going to publish my branch. Once I've done that, I'm going to go inside of my echo tutorial here and I'm going to open a new pull request and now let's go ahead and review all of our changes for this chapter. And here we have the code rabbit summary. So let's go ahead and read from the walkthrough this time. This set of changes introduces a new authentication and organization guarding system in a web application. It replaces external authentication UI components with internal wrappers, adds guard components, restructures layouts, and introduces a dedicated organization selection flow, as well as updates backend logic to enforce organization context. Several new components and pages were added, while legacy authentication logic is removed. Perfect. So in here we have a file by file change summary, but what I'm interested is the sequence diagram here. So let's take a look. If the user requests a protected route, the first guard is the middleware. And if the middleware says you are not allowed to do that, we're going to go ahead and redirect the user back. But if they are authenticated, we're going to redirect the user to the organization select. Now, even after that, if the middleware says you are good, or somehow someone manages to fool the middleware, what we do is if the user then tries to access the dashboard, there's another guard waiting for him, the auth guard component. And then we don't redirect anywhere, but we prevent them from seeing anything other than the sign-in view. And the exact same logic works for the organization select view, Both the middleware and our organization guard are doing the same level of protection here. So I really, really like how consistent both of our guards and middlewares are and everything is completely compatible with Convex and Clerk. This also doesn't include the diagram, but we also have protection in our backend route. So we couldn't be more protected here. It would take a very, very good hacker to break into this right now. so in here CodeRabbit is suggesting some completely valid suggestions that I should refactor my add user function but the reason I'm not going to accept this is because this is just a demo page to showcase convex right obviously we are going to add proper loading methods and error handling and everything once we develop proper actions now in here it suggests something interesting and that is that I should be adding use client to my sign in view and to my sign up view and I guess into my organizations. No, I will research that on my own to see if it's true or not. I think that clerk has updated since and that you can use sign up as server components, but maybe this routing hash changes it. I'm not 100% sure. In here again it recommends using the actual identity name here. Again this doesn't matter. This is just a random API route that we created but CodeRabbit is strict and tells us this is not a good API route because it's not. It's just a test route. But I'm very very satisfied with this. We just did an amazing job here. As always I'm not going to delete my branch and instead after I merge I'm going to go back and click down here select my main branch and click on this push and pull icon and click okay and that will synchronize my recent merge with my local main branch and when I click on graph here you can see that we just detached organizations into their own branch and then merged it back here amazing amazing job I believe that marks the end of this chapter and see you in the next one. In this chapter I want to talk about error tracking within our project. This is an especially important scenario to think of within an application like ours. This is a monorepo application which by default means it has a lot of moving parts and that means a lot of things that can go wrong. And while you as a developer can capture many errors on your own while you're developing, your users will most likely use your app in a completely different way. And you need to be able to track the errors of your users, not just your developer errors. So what I'm going to do here is create an error scenario, and then I'm going to show you the current way we can track errors thanks to convex error tracking. And then we're going to enhance that by adding production grade error tracking with Sentry. So let's go ahead and create an error scenario first. So what I would like you to do, if you haven't, is just run Turbo Dev. And I want you to go to localhost 3000 where your current authenticated application is. and just to make this easier go inside of apps web app dashboard page you can go ahead and remove this div with json stringify because it just seems to be taking space at the moment so great we now have just the add button so now if you go ahead inside of your api users ad and after successful identity and after successful organization, go ahead and throw a new error here called tracking test. So what happens now? First, make sure that inside of your workspace backend, the functions are successfully compiled. After that, go ahead and click add. What you're going to see are two issues appearing right here, error tracking test. And while this is perfectly fine when you are the developer and you encounter this issue. What if this happened to one of your users and you had absolutely no idea it happened to them? Well, thanks to Convex, there is one thing we can already track. So if you go inside of your Convex dashboard and go inside of your project here, you can see the high failure rate. And you can also individually click on functions and click on add and in here you can find all the logs that went wrong But the problem with this is it doesn really tell you much about the user who triggered this error So it is tracking the backend error but it cannot really tell you what went wrong. How did this happen? It cannot give you the breadcrumbs that led to this event. Because of this, we're going to integrate Sentry, production grade error tracking. So you can use the link on the screen or the link in the description to let them know you came from this video. And now go ahead and simply create an account. After you've created your account, since it's a completely new account for me, I have no projects here. So this is how it looks like. And in order to add Sentry to your project, you can access the documentation very easily by clicking on help and then clicking in the documentation. And in here, you immediately have Next.js integration. And in here, let's go ahead and let's run century wizard latest dash I Next.js. And the way we're going to run this is like the following. I want you to go inside of apps and then I want you to go inside of web specifically. So make sure that you are inside of web. You should be able to see the app folder, lib, middleware, all of those things. And now instead of running npx we will run pnpn dlx century wizard at latest and we are going to pre-select next js option here and now let's go ahead and select the following so it's warning us that we have some untracked files in our repository the reason it's warning us about this is because it might overwrite some files but we changed this just to demonstrate the error so this is perfectly fine So do you want to continue anyway? You can press yes. And now it's asking us if we are self-hosting Sentry or if we are using the SAS version. So we are using the SAS version. So make sure to select that. Do you already have a Sentry account? Yes, we just created one. So now let's go ahead and create a new project, which I'm going to call Echo Tutorial, like that. And go ahead and click continue. And now you can return into your terminal. So in here you have to select your package manager using the arrow keys so I'm going to select pnpm and press enter. And now Sentry Next.js is being installed with pnpm. Now it's asking us some additional configuration questions so this will usually depend on your complex application and what you want but for this tutorial you can select yes for this. This will basically allow Sentry to work even with ad blockers. So yes for the enable tracing yes and session replay a very cool feature select yes for that as well. Logs yes and let's also select yes for the example page so we can test the Sentry setup properly. And in here we have one warning. Sentry SDK is only compatible with Turbo Pack on Next.js version 15.3.0, but we are using an older Next.js version. So the way to fix this is to remove dash dash turbo or to upgrade the Next.js version. We're going to use the first option simply because this is the version that ShatZN Monorepo template suggested at the time of making this tutorial. So just to keep things simpler, we're just going to remove dash dash turbo. Now, just to remind you, Turbo Pack and Turbo Repo are not the same things. This means it's not compatible with the bundler. It doesn't mean it's not compatible with the mono repo. Those are two separate things, just to make that clear. And here's a little note that the SDK will continue to work for non-Turbo Pack production built, so don't have to worry. And let's go ahead and press I understand. and also one important thing it failed to add this to gitignore because we have a global gitignore so don't worry we're going to think of this as well and now let's go ahead and select yes for the ci cd tool here and then you will get your sentry out token go ahead and copy it and then you can go inside of your web application here so web and find dot environment dot local and after clerk, I will add sentry. And I'm just going to add it here. And now let's press yes, continue. There we go. So now we have to do a couple of things here. The first thing I'm going to do is I'm going to add this to my git ignore. Because it warned me that it wasn't able to do that. So I'm going to go inside of this git ignore here. And after local environment files, I will add sentry environment files and paste it here and now if you go inside of your apps web and you will see that this is grayed out because it is now in git ignore and you can also see a bunch of new files being written here all of that was added by sentry now what we have to do is we have to temporarily remove turbo from our dev command so that is also quite easy to do so inside of our app where we just added sentry. Let's go inside of package.json here and let's simply remove dash dash turbo back. That's all we have to do. Make sure you've done that inside of the web app. And now I believe that one thing I really want to do is just going to the root of my application and just run pnpm install just like this. And now let's go ahead and let's do pnpm, my apologies, let's do turbo dev. and let's focus on the web to make sure that it is working. Let's make sure nothing is failing here. You can see we don't have any warnings because we removed the TurboPack flag. And now let's go back to localhost 3000 here. Let's see if it will load fine. Looks like it's loading just fine. And now let's just visit the sentry error page that was created. So apps, web, app, century example page. So localhost 3000, century dash example dash page. There we go. So it looks like my events are blocked here. So I'm just going to go ahead and disable my uBlock origin here. There we go. So that fixed the error. Make sure you don't have any extensions, at least for this development mode. So let me just quickly show you the URL that I'm on here. This is the URL that I'm on, just so you are aware. And go ahead and click throw sample error. And you will get the message error sent to sentry. And now if you go inside of your project here, maybe do a refresh here, you should be able to now see your errors. There we go. So 13 and 12 seconds ago, we triggered both API error and the front end error. And this is now a completely different experience of error tracking. So it is absolutely amazing that Convex has built in error tracking, but Sentry is built for this kind of things. You can see that it will exactly tell you what browser version this happened on, the operating system, even the commit if you want to connect your repository to it. It will tell you the exact route this happened on. It will tell you every single thing. If you scroll a little bit down, it will tell you that it happened in app, century example page, page.tsx in onclick at this exact line. If you want to, you can also see a session replay to kind of guess what happens. So this is insanely impressive to me because they are not recording your screen. They are recreating what could have possibly happened. So that's what I think is absolutely amazing. You can see that a button was clicked and then something appeared at the bottom. That is exactly what happened if you remember this button and then this appeared i think this is absolutely insane so you can see what i mean when i say production level error tracking and in here you can see the breadcrumbs so you can see exactly what happened starting from when the person clicked on something as you can see we clicked on something we clicked on the button and then we did this API request and that threw back an error. So that's how you can track exactly what happened. Not only that, but you can also see all the information about the user that this happened to. So by scrolling even further, you can see even more examples of how this happened. So basically, just by this super simple button click, you get all of this amazing information to understand where the error come from, because that's what you need to know if your users are creating errors. There is no other way for you to know. You can also see the exact React version. So you can basically do absolutely everything to become this user, because it's super hard when you cannot reproduce something, but your users keep getting errors. This is how you fix those issues. and this was barely scratching the surface of Sentry, right? So what I just did is showing you the most basic error tracking it offers. They offer so many things from, this can actually turn into its own little JIRA or Trello board where you can resolve issues, where you can get notifications if it happens again. I'm gonna put a little screenshot. When I develop my applications, I get Gmail notifications from Sentry whenever something goes wrong. And this basically tells me if I'm, I don't know, in a supermarket, it tells me, hey, your users are having serious errors. You should go check this out. And that saved me many times because I did not encounter those errors. But for some reason, my users did. And by tracking the logs, that's how I fixed the issue. By looking at the session replay, I understood, okay, I know exactly where this is. Now I can reproduce it. So that is the power of Sentry. But now what we're going to do even further is we're going to try to make Sentry log our convex error. Because right now our users.ts here is throwing an error. But by default, I'm pretty sure Sentry will make.\nnot catch that because convex is an entirely different environment. So what we're going to do is we're going to connect convex and sentry together. So now let's go ahead and let's test this convex error one more time. So I'm going to go to localhost 3000 again here and I'm going to click add. So again we get this convex error and let's see if that will be tracked in here automatically. So let me just go inside of my issues. And you can see that we do have this error here. So Sentry does an amazing job of figuring this out as well. But it only knows this because it was called by the client. So it actually has limited knowledge of why this happened. But there is a way to connect convex error tracking with Sentry to give it full back-end and front-end knowledge about this. So in order to do that, you have to go back inside of your Convex dashboard here, click into settings, and then click into integrations. And the first option here is Sentry. So let's go ahead and click a plus button here. And now we have to pass the Sentry data source name. So what I'm going to do now is find that by closing my packages and by going inside of my apps web. and I think that you can just find sentry edge config or server config. I think they use the exact same DSN right here. There is probably some other way you can obtain the DSN but this works as well. So just go ahead and add it here. You don't have to pass anything in here and click save and you can see that it is active. So let's go ahead and try this again. I'm going to go ahead now inside of here again and I'm going to throw this error again and I believe that now if I go back inside of my issues here maybe it just repeated this error let's try throwing it a few more times I'm trying to make it appear again maybe we'll have to develop another error to see it here and let's just confirm that all of this is working correctly here let's refresh a few more times there we go and now you can see the difference so look at the first example in the first one it didn't exactly know why it happened but in here it knows exactly that it was users ad at least I think that's the order I might have confused it but basically one of these is now connected maybe it's actually this one I'm not sure because in here you can see the exact file where it happened. So maybe I told you incorrectly, I was fooled by the name here. Let's simply check out the both of them and then see what is the one with more information. I have a feeling it is this one right here, because it tells me the exact name of the file where this happened. So basically, it was that easy for us to connect the convex. And now we have backend information as well. So the function name was users add. And you can also see the exact user that this happened to, thanks to our clerk integration. So your users are now no longer anonymous. You can now also definitely know to whom this happened. Absolutely amazing. And you can see the SDK is convex and the version. So this is how you track errors in production grade environments. This is how it works. And again, this is just scratching the surface. There are so many different things that you can look at here. Sentry isn't just for errors. You can also track dead clicks, slow SSR. So whatever is going wrong with your app, you can track here. All the routes, all that's good, all that's bad, all of those things are available to track here. So as I said, I barely scratched the surface of this, I highly advise you to go ahead and explore this yourself. So now that we have this implemented, I do want to try and confirm a very important thing and that is, do our apps still work? So the one thing that I want you to do, I want you to go inside the terminal, focus on the web dev and I want you to scroll a little bit through here and I want you to pay good attention if you see something called open telemetry. The reason I'm telling you to pry your eyes in trying to find that is because there actually is a little troubleshooting guide here which might or might not happen. It did happen to me the first time I developed this so I know it's possible to happen and it is this. Import in the middle external package errors. So pry your eyes into this. import in the middle and try and find something in here or maybe just search import in the middle. So you can see that right now it is not happening for me anywhere but this is how I'm going to 100% confirm. I'm going to run Turbo Build. So build is the ultimate okay it works and I can deploy it. right so looks like this is working on the widget it's still building on web so let's go ahead and wait till the web finishes to determine whether we need to change anything or not and here we go so three successful builds means that everything is okay and I still can't find any open telemetry or import in the middle issues in my builds here but I am going to try and find a screenshot of that happening and post it here so that you can recognize it if that's something that you are getting. And if you are getting it, they have a guide on how to resolve it. So you have to go inside of their documentation, Next.js, click on troubleshooting here and find PNPM resolving import in the middle external package errors. So when using PNPM, you might encounter errors related to packages that can't be external, particularly packages like import in the middle and require in the middle. So it is because PNPM's strict dependency management and hoisting behavior. Now there are a couple of ways you can fix it. One way that I fixed it immediately in my previous project was by adding these two inside of my npm rc file. So this is what you would do. You would go inside of npm rc and you would paste those two here. And after that what I did was I simply run pnpm install in the root of my application and you will be asked if you want to restart node modules from scratch so in here I selected true so let's see what happens now I'm purposely doing this just in case some of you are having this issue because I was able to get that problem so let's go ahead and try again does Turbo Dev work now math is obviously working dev is working web is working backend is working perfect let's try turbo build just to confirm three successful looks like everything works just fine amazing amazing job so obviously i'm going to keep an eye on this if it happens and keep you alerted in case you are having some errors always make sure to watch a little bit more of my tutorial because sometimes i fix errors from previous chapters in the next chapter right so don't worry i will keep an eye on everything here and test it as much as i can to make sure you and i are seeing the same thing in our consoles here amazing amazing job so we just developed uh error tracking by connecting convex to sentry now let's go ahead and commit these changes here so i'm going to go ahead and add all of these changes i'm going to call this 05, I believe, error tracking, like so. I'm going to click commit. I'm going to click on main down here, create a new branch, 05 error tracking, like that, and I'm going to publish this branch. And then I'm going to go ahead inside of my echo tutorial repository here, and I'm going to open a new pull request. Let's go ahead and review in case we have some serious issues, but I think it's mostly going to be okay. And here we have the summary by CodeRabbit. So we introduced the Sentry error monitoring and performance tracking across the application, including both server and client environments. And of course, we added this dedicated page for testing Sentry, and we added a backend API endpoint. We didn't do this manually. The Sentry wizard did this for us just in case you're confused we removed the display of the user list because it was it was interfering with our work at the moment in here as always we have a file by file change but we don't really have to review most of this because a century wizard added it like this so i don't really know if we should move this or not so yes it is adding comments for this century example API which we're going to remove in the next chapter so really no need to fix something that was just used as an example here but obviously all correct comments from code rabbit here and in here it's suggesting changing the traces sample rate not so sure about that I'm gonna have to explore about it to see if this is a true statement or not but again the century wizard set it up like this so I'm going to leave it like this. Let's go ahead and merge this pull request and after you have merged it let's go ahead and go inside of error tracking here. Let's click back on main and let's click this button right here to synchronize our changes just like that. There we go and then click on graph here and confirm that you have detached in 05 error tracking and then merged it back here. Amazing, amazing job. I believe that marks the end of this chapter as well. Amazing job and see you in the next chapter. In this chapter, we're going to learn how to build an AI voice assistant using Vapi. More specifically, we're going to build a bank customer support agent that will be able to verify our account using the last four digits of our phone number and then give us back our bank balance. We're going to test this agent first using the WAPI dashboard and then using the client SDK for our Next application And after we finish that I going to briefly explain how Vapi API work and our multi needs I going to try to explain how we going to white label Vapi API allowing our customers to bring their own API keys, making our app that much more flexible, because every single customer will be able to configure Vapi the exact way you're going to see we will be doing now. And that is an amazing feature of this B2B software as a service. So let's start by creating a WAPI account. You can use the link on the screen to let them know you came from this video. After that, let's go ahead and create an account and head to our dashboard. Once you land in your dashboard, in the upper right corner, you will find the documentation button. In here on the left side, in the sidebar, click on the VAPI guides. I highly recommend that after you finish this chapter, you try doing one of these on your own to get more familiar with VAPI because this will explain to you all the things that your customers will be able to do thanks to the white label method that we are going to be doing. For now, we're going to build one of these together, specifically the inbound support. We're going to be building a banking support agent with function tools and knowledge bases. So let's go ahead and first download the knowledge base using the spreadsheets. If you cannot find them for whatever reason, using the link on the screen, you can access my public repository, Echo Assets. And in here, find the WAPI folder, and you can find the same spreadsheets here, as well as all the prompts and descriptions needed for this guide. So now let's go ahead and upload these files to our WAPI dashboard. So head to the files in the sidebar. You can see that I already added them. Make sure you have them as well. After that, let's go ahead and create an assistant and call it Tom. So head into your assistants, create an assistant, call it Tom and select blank template. Once you have created your assistant, you're going to have to modify the first message and the system prompt. You can find both of them inside of my public assets folder, first message, system prompt, or simply follow the documentation along. I'm going to modify the first message here. And I'm going to modify the system prompt. Just like that. After that, if you want to, you can also configure the LLM settings to choose a different model, but I'm fine with this. So I'm going to click publish. Once you've done that, let's go ahead and let's add tools to our assistant. So head into tools and click create tool. Select query. Once you've created this new tool, let's go ahead and rename it to get balance. Let's go ahead and paste the function description here. And let's go ahead and click add knowledge base. Let's go ahead and call it accounts. Let's give it a description of use this to retrieve account information. Let's go ahead and select the files, accounts, and go ahead and click save. After that, let's create get recent transactions tool. Again, a query tool. Let's go ahead and give it a description. After that, let's add two knowledge bases. First one will be access to the accounts just as in the previous one with the same description and same files. Then add another knowledge base, transactions. Let's go ahead and give it the description. And let's select transactions. And let's click save. Let's go ahead and create our last tool, which will be called Lookup Account. Let's rename it to Lookup Account. Let's give it a description. And let's go ahead and add a knowledge-based accounts. And just as in the previous examples, let's add a description. And let's select accounts. and let's click save. After you have added those three tools, let's go ahead and add them to our assistants. So head into assistants, select Tom, scroll down until you find tools and go ahead and select lookup account, get recent transactions and get balance and hit publish. After you've done this, you are ready to test your assistant. You can do that using the talk to the assistant method. I would highly suggest that for the first run, you try the following. In the first question, ask it to verify your account. After it asks you to provide the last four digits of your phone number, respond with 1, 2, 3, 4. Why that number? Well, if you take a look into our accounts.csv, you can see that that is the only available account. So that's how the lookup function, the lookup tool will be successful only if you pass the proper account here. And then when it asks you what do you want help with, I highly suggest asking it to get balance. I've had it fail a few times with recent transactions which highly depends on what model you select and what CSV file you have added. So for best results, I highly suggest asking it to get your balance. I'm now going to click talk to assistant and I'm going to pause the video after each checkpoint. So after I have clicked on start call I first had to allow access to my microphone and then I asked Tom to verify my account. After that I told it one two three four and after that my account was successfully looked up and now I have to ask a question again but that is only true for this test call, not in the actual call. You can see that after I have asked to give my balance, again, it shows me that it ran a tool. So now I have to ask again, and it will print out the balance. This will not happen in a real example. And there we go. So every time that it activates a tool, you're going to have to ask the question again. again this only happens in this testing mode not in the real mode where your customers will talk with this ai you saw it in the demo how well it works so after i asked it what is my balance it basically told me the exact balance that i have inside here so i highly suggest that when doing testing you test using this script hey tom can you verify my account one two three four and then can you give me my balance? Perfect. Amazing. So what did we just achieve? We achieved a very, very simple VAPI assistant. But as you saw in their guide here, in their guides, they have much more complex workflows here. You can do appointment scheduling, you can do medical triage and scheduling and all of these things as you can see you can do no code drag and drop things with nodes and all of those things you can see they have much more complex examples and i highly recommend that you play around and try and create one of them on your own so you see the true power of vapi and this is what each of our customer will be able to do right so we are not going to limit them by just wrapping, just being a wrapper around Vapi. Instead, we're going to be a white label provider so that whatever customer knows how to use Vapi, like we just learned to do, we're just going to give our app API keys and that will automatically pull all assistant IDs like Tom, who we just created, and all phone numbers. So phone numbers is something that we haven't done yet. So let's just quickly do that. Head into your phone numbers here. You can see I already have one because I just tested this a second ago. You will probably have none. So just click create a phone number. And the trick is right now they only allow US phone numbers. You can still create them outside of US, but you can probably only call them if you are in the US. Otherwise, you will have high costs. I don't know what is the area code for the US, so I will just use whatever one they recommend. Let's try 928. There we go. And then simply call this Tom Vapi Bank Hotline, something like that, and select your assistant to be Tom. So this is what your customers will be able to do. They will be able to create phone numbers, workflows, tools, files, assistants, all of those things on their own using Vapi. And we are going to tell them, great, just give me your API keys and I'm going to fetch all of those and display them in your dashboard. And you will tell me which one, which assistant you want to use and which phone number you want to use. And I'm going to display them in the customer facing widget on your website. So that is how this is going to work. I think that's a super cool feature and something we've never done on this channel before, bringing this app even closer to enterprise-grade software. So let's go ahead now and test out this voice agent the same way we just did using the Vapi dashboard, but let's do it inside of our Next.js application. So we are not going to do this inside of our web app. We're going to do this inside of our widget app because this is where customers will be able to talk to AI assistants and AI chat. So let go ahead and do pnpm filter widget add at vapi forward slash web So this will now install that package specifically in the widget application. So just make sure you have added that package JSON here. Perfect. And now let's go ahead inside of our apps widget. And in here, let's go ahead and create a new folder modules. let's create a widget module and let's go ahead and create hooks inside use wapi.ts let's go ahead and import wapi from our new package and let's import use effect and use state from react let's go ahead and create a type called transcript message with a role of either user or assistant and the text of type string let's export const use wapi let's go ahead and let's first define const wapi and set wapi instead of a used state. Give it a type of wapi instance or null. Now we're going to create three booleans. All of them are going to be the same. Is connected and set is connected and same thing for is connecting and is speaking. So just be mindful there is a difference between these two. All of them will be false in the beginning. And the last state here will be the transcript, which will be a type of transcript message, but in the form of an array. Now let's create the use effect. Go ahead and give it an empty dependency array. And now let's create the VAPI instance using new VAPI. Inside of here, we now have to add our public API key. So head inside of your WAPI API keys and click add key. I'm going to call this widget echo tutorial and I won't select any of these other ones. I will just click create public token. Widget echo tutorial. I'm going to click copy. Make sure you did this for the public API key and simply paste it inside. So why am I adding this like that and why am I not adding this in the environment file? Well I'm going to add a comment as to why. Only for testing the VAPI API. Otherwise, customers will provide their own API keys. That is why. Because as I just explained, we are not going to use our own VAPI keys. Each customer will have to add their API keys, allowing them to create agents of their own, to create workflows of their own and phone numbers of their own, making our app much more flexible. And for us, a great lesson in white labeling other APIs. So let's now set WAPI to be WAPI instance. And now let's go ahead and let's do something on the WAPI instance call start. So when the call start happens, let's set is connected to true, set is connecting to false and set transcript to an empty array. Now let's do the same thing on call end. Set is connected will be false, set is connecting will be false as well as set is speaking. Now let's add two very simple ones. on speech start set is speaking to true and on speech end set is speaking to false. Now let's also add the error state and I also recommend adding a little console.error here. Let me just obtain the error and just pass it here simply so you can see if something happens and you cannot debug it. And now let's go ahead and do vapi instance on message. Now in here, you have to be a little bit careful because message is a type of any. So what I'm going to do is carefully type if message.type. The thing is, you can add this and it won't throw you an error, right? So make sure you don't misspell this. If message type is transcript, you can also misspell here, so be careful, transcript. And if message.transcript type is equal to final, same rules here, only then call set transcript. We're going to get the existing values in the transcript and we're going to return an empty array. Then we're going to reuse the existing values in the transcript and we're going to add a new one. we are going to assign a role for this transcript. Who is speaking? By checking message.role and checking if it is a type of user. If it is, assign user. Otherwise, set it as an assistant. And the text will be message.transcript. Make sure you are not misspelling transcript in any place because this is a type of any, so you can misspell things very easily here. Before we end this useEffect, make sure to add a return method which will call vapi instance??stop. Now let's go ahead and let's do const start call method. Set is connecting to be true and call if it exists vapi start. Inside of start we have to define which AI assistant we want to call. So head into your assistants and copy the ID of Tom and paste it in here. I'm going to copy this comment from above one more time. The reason we are pasting it like this is because this is only for testing the Vapi API. Otherwise, customers will provide their own assistant IDs. so that's what white labeling will do for us and now let's add one more function end call and finally from this hook let's return all of those things is speaking is connecting is connected transcript start call and end call now let's go ahead and use this hook I'm going to go ahead and go inside of apps, widget, app, page.dsx and in here I'm going to actually remove everything besides the button here and I'm just going to leave the button and nothing else like that and now I'm going to go ahead and I'm going to call my new hook, useWAPI from modules, widget, hooks, useWAPI So this is directly inside of the widget folder. Modulus, widget, hooks, use WAPI. Now let's go ahead and let's destructure everything that's being returned from WAPI. IsSpeaking, isConnected, all the way to endCall. Like that. And in here, let's call startCall. And the button will also say startCall. Sure, this can stay like this. below it add the end call method and call give it a variant of destructive let's also give this a maximum width of MD MX auto and full width and then let's go ahead and add a paragraph here is connected and inside of here open backticks and render is connected and then the same thing is connecting and is speaking and then let's go ahead and let's json stringify our transcript like so i believe we now use everything from here make sure you have use client at the top right here and now let's go ahead and do turbo dev in the root of our application. Since we are working within the widget dev, we have to go to localhost 3001. And we should now have start call, end call, and indicators here. Make sure to open your console so that you can see if any errors get logged here. When you click start call, you will most likely have to allow access to your microphone, so be mindful of that. So I'm going to attempt to have the same conversation that I just had in the WAPI dashboard. I will ask it to verify my account, and then I will ask it to check my account balance. And here we have our conversation. So we started with, hello, you've reached Vapi Bank customer support. My name is Tom. How may I assist you today? Hey, Tom, can you verify my account? That was said by the user. Sure, please provide the last four digits. I said one, two, three, four. Did you verify my account? Yes, I verified your account, John Doe. How can I help you today? I said I would like to check my balance and after that it told me your current balance is $2,534. So the exact same experience that we just had on the VAPI dashboard has been transferred to us here. Perfect. So that is what we wanted to achieve. Amazing. So now let's go ahead and see what we've done. We've created a Vapi account. We set up customer support agent. We've added a knowledge base. We've added some tools. We tested the agent from the dashboard and from the client SDK. But now let's talk about Vapi's API. So initially, my idea was, okay, now what I have to do is I have to add my secret API key for Vapi and use Vapi's server side SDK. and for each one of my customers create their phone numbers and create their assistants and all of those things. But then it hit me. Why am I actually doing that? Because all I'm doing is I am recreating the entire Vapi dashboard. That makes no sense. Like Vapi's dashboard will always be one step ahead of mine. So it doesn't make sense to do that. So I thought, well, how about a white label VAPI instead? So we are going to learn for the first time how this works. But there is another problem at hand, right? Another reason why I chose to white label. So right now each of these entities phone numbers assistants and tools all of them are tied to one API key and they are not separated by tenant So from a security standpoint I would have all of my knowledge bases in my account And that is a kind of weird thing to do to your customers, you know, coupling all of their accounts into just one knowledge base. There's a couple of things that can go wrong here. So I thought, okay, how about instead of one Vapi key, one tenant, and single phone number, single assistant, single tools, how about I allow my users for my B2B SaaS application to white label Vapi's API, and I tell them, give me your API keys. And then I'm going to fetch all of your phone numbers, your assistants, your tools, your workflows. So I can now have infinite number of tenants and all of them will have separate knowledge bases, separate phone numbers, separate assistants. That is where the idea to white label comes from. And if you're wondering how do we store these API keys, that sensitive data. We're going to be exploring Amazon Web Services, more specifically the Secrets Manager. That is where we are going to store our keys. Amazing, amazing job. So as I said, I now recommend after you finish this chapter that you go back into the VAPI documentation, head into VAPI Guides, and try and do one of these on your own. I will probably play around this some more and then you can create as many of them as you want and all you will have to do later is add your API keys to our customer support app and we are automatically going to fetch all the assistants you have created and all the phone numbers you have created allowing your users to use your VAPI account directly. So go ahead and try to build one of these on your own if you want to of course. Now let's go ahead and commit these changes here. So yes we can leave them like this for now. So let's go ahead and go inside of here. I'm going to select all of these changes. Let's see this chapter 06 AI voice assistant. Like so. Let's commit. I'm going to click on the main here. I will create a new branch, 06 AI Voice Assistant, like that, and I will click Publish Branch. After that, let's go inside of my repository here. Let's go ahead and open a new pull request, and let's review our changes. Even though most of this is just for test, but I do want to see our hook reviewed just in case. And here is the CodeRabbit summary. We introduced voice call functionality with controls to start and end calls. We display a real-time call status and a transcript of the conversation. so the changes introduce a new custom react hook called use vapi to manage voice calling directions using the vapi ai assistant within the widget application that is exactly what we did in here we have a sequence diagram explaining what happens when we click start call we use the use vapi hook and we initiate the call with the assistant id the assistant then emits events such as call start, speech start, transcript, etc. And we update the state accordingly. And then we render the call state and the transcript. End call works very similarly. And in here, it gave us two comments and that is not to use hard-coded API keys and assistant IDs. Obviously, we are not going to do that. I just added it as an example. But yes, make sure that this isn't a public repository. I should have definitely told you this sooner. Or what you can do simply, if you have committed this already, just go inside of your VAPI API keys and delete them, right? So in case this is a public repository you're working with, so no one can steal your public API keys. Even though public API keys are kind of meant to be public, right? It would still be a good idea not to exactly share them with everyone. Great, so we're going to remove that in the next chapter so we no longer have that issue and for now you can just click merge pull request and confirm merge and after you've done that I'm going to change my branch back to main and I'm going to synchronize my changes. After I have synchronized my changes in the graph below we should see an example. We detached to do AI Voice assistant and then we merge that back into main. Amazing, amazing job. Perfect. So now we have a base model for our voice assistant and how our customers will create support agents and then give us their API keys so that we can call this useVAPI hook with their assistant ID and with their next public API key. That's how this is going to work. And just, you know, a small note here, in case you had any trouble finishing this and you're worrying, will you be able to continue the tutorial if this, for whatever reason, doesn't work for you? Maybe you don't have a microphone or something like that. Don't worry. This is a very cool additional part of this project, but you will be able to continue this even if you had some trouble doing this. Amazing, amazing job and see you in the next chapter. In this chapter, we're going to go ahead and create the dashboard layout. And this is how it's going to look like. Organization switcher on the top, sidebar item groups in the middle, and in the footer, we're going to have our user button. Let's start by adding some more ShatCN components because right now our packages UI source components only has a button and input. To remind you how we add components, let me load the monorepo documentation from ShatCN UI. You go inside of cdappsweb and you run the command. So CD apps web, make sure you are inside and then PNPM, DLX, ShatCN at 2.9.2 because that is the version that I used to initialize this tutorial. So that's why I'm using this version again. And I'm going to add all components to save some time so we don't individually pick because we do need a lot of them, but not really all of them. So I think this is a better solution. Let's just add all of them and then in the end we can remove those we don't use. So let's go ahead and press enter here. And I believe that we will overwrite the button and the input. I'm just not sure if it's going to ask us to do that or not. There we go. So do you want to overwrite? Sure. Yeah, we can overwrite. And same thing for the other component that we have input, I believe. and here we go so it took me quite a lot to do this two minutes so yes if yours is still loading it's normal so it added a bunch of components and it also updated a bunch of my packages here so you can see in pnpm lock we got a bunch of new stuff here and now when I added my form component it will automatically install the hook form resolvers and it's also going to install zod and react hook form i had some problems with that i just want to tell you that in advance and i had to manually fix the versions so just in case you try building the app or you see something weird with hook form resolvers don't worry i had that problem as well and if we encounter it i will fix it so let's go ahead now and let's develop the sidebar. So the first thing I want to do is I want to go back in my root application. If you want to you can run pnpm install after this just to confirm everything is okay and then let's do turbo dev and let's focus on our web application. Looks like it's building great. So let's go to localhost 3000 because this is where our main application is. And it looks like it is building. Works. Perfect. Now let's go inside of apps. Let's go inside of web. And instead of here, let's go inside of modules. And I'm going to create a dashboard module here. And inside of here, I'm going to create UI. And then I'm going to create layouts. And inside, I will create a dashboard layout.vsx. Let's go ahead and export const dashboard layout here and I'm going to extract the children from here. Let's go ahead and type the children and let me just see this is a react node. Inside of here let's go ahead and render our auth guard like that organization guard like that and then inside of here let's render main with class name flex flex one flex call and render the children inside like that and now you can go inside of your web app dashboard layout and you can simply replace this with dashboard layout. So we're going to develop the dashboard layout there like this. And you can remove the auth guard and the organization guard because we just moved it here. So right now nothing much should change within your application. But now what you have to do is you have to import the sidebar provider. And you can import the sidebar provider from workspace UI components sidebar because we just added this component and in here you have the sidebar provider there we go so after you have imported sidebar provider\nLet's go ahead and wrap our main inside of that. So sidebar provider. Now let's go ahead and do the following. Let's add const cookie store to be await cookies. And let's set the default open here to be cookie store dot get sidebar underscore state question mark dot value is equals to true. Instead of a string, this is important. Now let's go ahead and mark this as an asynchronous method. And let's import cookies from next headers. And make sure that you don't have useClient at the top or in your layout file here. Now that you have that, go ahead and pass to your sidebar provider the default open value. This way, even during server-side rendering and server components, you will not have a weird flash that happens because you will be able to collapse your sidebar and when you collapse it and refresh for a second it's going to expand and then it's going to read its local storage state and it's going to collapse back and that just causes a weird effect. So by doing this we make sure that even during server side rendering we know whether sidebar is collapsed or not. Why sidebar state? Well, the answer is inside of the sidebar component in packages UI source components. In here, you can find the sidebar cookie name sidebar underscore state. So just double check that you are using the correct one. What we have to develop now is the dashboard sidebar component. So let's go ahead inside of dashboard UI in the apps web modules dashboard and go ahead and create components and inside add dashboard sidebar.tsx. Let's go ahead and mark this as use client and let's go ahead and import everything we're going to need. So we're going to need organization switcher and user button from clerk next.js. Then we're going to need all the icons, right? Because if you remember here, we're going to need all of these icons to display. We can do that by importing them from Lucid React. We already have this package installed thanks to Shatsy and UI. Credit card, inbox, layout, dashboard, library, big, mic, and palette icon. Then we're going to need to use both the image and link from next but individually next image and next link. And then use path name from next navigation. And now we need to import all the components from our sidebar component. Sidebar, content, footer, group, group content, group label, header, menu, menu button, menu item, and sidebar rail from workspace UI components sidebar. And lastly, let's go ahead and import cn from workspace ui lib utils. And now let's go ahead and let's create our first array of items. Const customer support icons. My apologies, items. Let's render them in an array. each item will have a title a url icon let's set this to the inbox icon and badge let's go ahead and set this actually no need for a badge that's fine and let's go ahead and give this a title of conversations the url forward slash conversations like that and then go ahead and copy it and change it to be knowledge base forward slash files library big icon. Now let's render those items. So export const dashboard sidebar like this is going to return a sidebar with a class name of group. Make sure to capitalize the N in class name. And in collapsible mode, it will only display the icon of each item. So when we collapse the sidebar, only the icon is going to be visible. Now let's go ahead and let's add the sidebar header. Now let's go ahead and inside of that header, add the sidebar menu. sidebar menu item and sidebar menu button. And go ahead and give it an as child property and size large. And inside, render the organization switcher and add hide personal and skip invitation screen components. So now let's go back inside of the layout here. My apologies. Inside of the dashboard layout component. And just next to the main element, render the dashboard sidebar. And I like to use the modules sidebar UI dashboard. Like that. and now you should have a big sidebar here and you should have the organization switcher available and right now there is no way to collapse this sidebar but we are going to add that soon and we're also not really displaying any items here so let's go ahead and continue developing this until we can see some items so that we can start building this so let's go ahead and go outside of the sidebar header and add sidebar content. Instead of sidebar content, let's mark this as customer support so we know what group this is. Now render the sidebar group, sidebar group label, and write customer support. And there we go. You now have a label customer support here. And now let's go ahead and let's add sidebar group content here. And let's go ahead and do sidebar menu and inside of here iterate over customer support items which we defined above get the individual item here and render sidebar menu item give it a key of item.title render sidebar menu button give it an as child property give it a tooltip property of item.title title let's just not misspell it and also give it is active prop to be let me just see I think we have to develop is active yes so let's go ahead and just quickly develop is active by first getting the path name from use path name which we imported and then const is active will be a function which accepts the URL and it will simply check if the URL is root page return path name is equal to that otherwise return if path name starts with URL. So let's go ahead and use the is active here item dot URL like that. Now inside of here you're going to use the link element give it an href of item dot URL render item dot icon here give it a class name of size 4 and render this span item.title and now looks like mine is collapsed and I think that the reason mine is collapsed is because I developed on localhost 3000 so it could be that I have a cookie that collapsed them for me I'm not 100% sure let me go ahead and check what is my cookie name sidebar state looks like I don't have it okay so I'm not sure why mine is collapsed I'm gonna go ahead and this is what I'm gonna do I'm gonna go to the end of the sidebar here instead of dashboard sidebar right here and let's add sidebar rail it's a self-closing tag. This should enable you to click in the edge of your sidebar and you should be able to open it and close it. And now try refreshing and it should preserve the state. So if you collapse it and refresh, it should stay collapsed. So the way this works is thanks to this, thanks to cookies. In here we store that so we know is it opened or is it closed. And also make sure you are doing this in Desktop mode, if you are too zoomed in, is going to disappear because on mobile it hides itself. Great. So now let's go ahead and expand this and let's continue developing. So now we can add some other items here. Instead of the dashboard sidebar, let's go to the top here. After customer support items, you can add configuration items. So a new array, exactly the same as the one above, but with some new icons and some new elements. Widget customization leading to forward slash customization. Integrations leading to forward slash integrations. Voice assistant leading to forward slash plugins forward slash VAPI. And make sure to use the proper icons. and now what we have to do is we have to render the same thing here so let me go ahead and let's copy the sidebar group and let's add an empty space here and let's paste another group change this to be configuration change this to be configuration and in here change this to be configuration items I think right now yeah this says voice assistant Now, we are later going to change this to be a Vapi icon. For now, we can leave it as is. I think this is okay for now, yeah. But later when we, we're going to filter this out, and we're going to change this to be as in the screenshot, as you can see. we're going to use a VAPI icon so it looks like a proper integration with some other platform right now let's do the same for accounts and account items so this will be much simpler so go at the top where your configuration items are and simply add account items and then you just have to do the same thing right so go ahead and copy the last sidebar group here like so, add a space and change this to be account. Change this to be account and account items. And now let's go here and there we go. Conversations, knowledge base, widget customization, integrations of voice assistant, plans and billing. So now what I want you to do is I want you to go outside of sidebar content and open up sidebar footer. And in here, render the sidebar menu. And let's go ahead inside and render sidebar menu item. Go ahead and render the user button component, which is a self-closing fact. And go ahead and give it a show name prop. and now at the bottom you will have your user button. But we have one problem with this and that is when it's collapsed the clerk components don't look good. So all the other ones seem to integrate quite well as you can see but these ones seem to be a little bit problematic. So what we're going to do now is we're going to fix them starting with the organization switcher. So let's go ahead and focus on this component right now. So we can use the appearance prop for the organization switcher. So let me go ahead and do that. After these two props, go ahead and add appearance and open an object. Go ahead and select the elements object. And first, let's define the root box. Give it a full width and put exclamation point at the end. This is equivalent to writing important, right? So if I go ahead and add an exclamation point and hover, oh, it looks like nothing shows when I hover. Not sure why this is. Usually when you have Tailwind CSS IntelliSense, it should be able to help you with class names. But maybe this is because of monorepo structure. I don't know. But basically, just put an exclamation point and that will mark it as important, which means it will override if there is another width set. And same for height 8. And you can see that now when you refresh, well, you can't really see anything, but the root box now extends up to here, except it doesn't have a color. So now we're going to change the user button trigger, which is the thing that we can currently see. And that's what we're going to change to be width full as well. Let's see. Okay, still nothing is showing. So with full padding 2 and let's do hover BG sidebar accent like that. Let me just check. Is anything here changing? I just want to make sure I'm not doing something incorrectly here. Okay, so let me just check. Organization switcher. the root box is definitely all the way to the end but I think there is something with the sidebar that's making it like this so let me just check the width is definitely overridden by width full so I'm just going to continue developing and we're going to see okay so user button trigger now has with full padding to hover bg sidebar accent hover text sidebar accent foreground group data collapsible is equal to icon make sure you are writing all of this without any space so this is all one thing when that is active give it a size of eight and group data collapsible icon again, give it a padding of two. And now when you collapse, okay, I'm not sure why this is not doing anything here. So let me just check. My deepest apologies, I am reading you completely incorrect things. This is not user button. For some reason, I thought we were developing the user button my deepest apologies let's remove the user button trigger because that's not what we need let's go ahead and add avatar box just below the root box my apologies again rounded small so make sure you add these two classes i completely missed what component we were editing and i was telling you incorrect instructions so the one we need to target is organization switcher trigger component. Let's try it now. So with full justify start. Let's see. There we go. You can see how the entire thing is now selected. My deepest apologies. I kept seeing that something is wrong, but I wasn't sure what it was. Okay. And now let's do on group data collapsible icon. go ahead and change the size to be 8 and then you can copy this class name use it again and change the padding to be 2 and now when you collapse it should look like this there is still this carrot here don't worry we will solve it but you can see how now it adapts to the rest of our app i think this looks much better already now let's continue developing this so let's go ahead and give it the organization preview option and in here we're going to do a similar thing so you can just copy this group data collapsible icon because it's going to be the same everywhere give it justify center and gap two so right now we only change the gap when it's expanded so it matches the gap of all the other items here. Then let's do organization preview text container. Again, go ahead and copy group data here, collapsible icon, and simply make it hidden. And after that, give it a text extra small, font medium, text sidebar foreground. So on collapsible icon only hidden. Everything else is independent. So let's go ahead and see how it looks now. When you collapse the text button completely, the test completely disappears. And now let's go ahead and solve the carrot. So the key for that is organization switcher trigger icon. let's go ahead and copy the group data again and make it hidden and then let's use ML auto and text sidebar foreground and I think that now we finally get a beautiful effect perfect it looks just like all the other icons and you can open it normally and it fills the whole width when active. Perfect. Now let's do the same thing for the user button which is what I did initially but I was telling you invalid classes. So let's go down to the user button. Let's add appearance, elements and let's start with adding a root box. Full width and height 8. Again, I don't think nothing will be visible now because we need user button trigger, what we initially were doing. So full width and padding 2. There we go. You can see how now full width is being taken and this is centered. Let's do on hover BG sidebar accent. So now when you hover, you can have the same background as all of our other element. So it matches. On hover, text, sidebar, accent, foreground. Group, data, collapsible, icon, size 8. And you can copy this. And besides, size 8, also padding 2. So I think that already when you collapse... Okay, not perfect. let's continue developing let me just confirm that I didn't misspell the group data thingy you can copy it from any of the ones above but looks like it is the exact one yes okay so now after user button trigger let's do user button box full width flex row reverse justify end. And you can see that now I have reversed the user button icon to be on this side and the text to be on this side. So it fits all the other elements here using flex low reverse and justify end. Now let's give the same gap as all the other elements. And now let's use group data collapsible icon here to be justify center and text sidebar foreground So we are getting much better perfect Now let go ahead and target user button author identifier PL will be zero Let go ahead and use the group data again hidden and avatar box let's give it a size of four and just like that we have created it exactly the same as our other ones and when collapsed it collapses nicely perfect so that is exactly what we wanted we now have a sidebar that will appear for each of these pages right here and we will be able to change our organizations now. Let me just change this to something else. As you can see, we can now change within our organizations from the sidebar here. And you can also see how it exactly matches the organization here. And from here, you will be able to manage your account or log out. Perfect. So let me see what else do we have to do. We added all the components. We created the dashboard layout. We created a sidebar, groups, user button, and organization switcher. We didn't do the empty pages, so we could do that to end the chapter. And yes, I'm aware that the colors are a little bit different. Don't worry about that. We're going to have a special chapter called theming in which we are going to learn how to theme our projects properly. So let's go ahead and just create these empty pages for now. So I'm going to go inside of web, inside of app, inside of dashboard. And let's go ahead and do the following. I'm going to create conversations and inside page.tsx page conversations. And now when you click on conversations, you should be able to see conversations and this should be highlighted. You can see how it's bold and it looks selected. And now let's go ahead and let me just remove the test page. We no longer need it. Copy conversations and change this to be files. Go inside of files and change this to be files. And basically now go ahead and do that for each of our dashboard sidebar. items here at the top. So we added conversations and we added files. Now let's do customizations and integrations. So just like this, customization page.tsx integration page.tsx. So nothing special. We've already done this before. And now let's also do plugins of API. So this one is a little bit different. That's why I want to do it with you. Inside of dashboard, create a new folder called plugins. And then inside a new folder called WAPI. And then inside page.tsx. That's how you create nested components. Whoops. Let's just do this WAPI plugin. And I think there's only one left and that is billing. So let's go ahead and copy files. Rename it to billing. Like so. And I believe that now every single one of our pages should be visitable. So billing should load billing. Voice assistant should load WAPI. Integrations, integrations. Widget customization. Knowledge base should be files. And conversations should be conversations. Amazing, amazing job. So in the next chapter, we're going to add some theming. So it looks like I screenshotted here. Amazing. So we added empty pages. Now let's go ahead and commit these changes and review our pull request. So I'm going to add all of these changes. And this will be 07 dashboard layout. I'm going to commit. And then I'm going to open a new branch. 07 dashboard layout. and I'm going to publish that branch. If you ever accidentally push your changes without opening a new branch, don't worry. You don't have to follow the branches like me. I'm just doing it because it's much easier to keep track of your project and what's going on and so that you can see what we actually did in this chapter, right? So now I'm going to open a new pull request and this will be a pretty big one, I suppose, because CodeRabbit will now review 57 files. But I'm going to try to get the point across as short as possible because we know that what this chapter was about was sidebar. And here we have the CodeRabbit summary. We introduced a comprehensive set of UI components, including all the way from sidebar to tooltip, basically all ShatZN UI components. But here's what we actually developed ourselves. We added a responsive and collapsible dashboard sidebar with navigation as well as user and organization management. And we also added new dashboard pages for every single item on that dashboard. And in here, it says that we provided a mobile deduction hook. So that was all thanks to ShadCN UI CLI tool. We didn't do this manually. So for this reason, it really makes no sense to go through all of this individually because all of these components are mostly added by the CLI tool, except, well, this, I misspelled knowledge base. So in the next chapter, I will make sure that I correct my typo here. Great catch by CodeRabbit. And in here, yes, this is a good tip. We should probably export the sidebar cookie name from the sidebar component. So you can see how it's searched through my entire code base. And it found out that you can find sidebar state as a constant sidebar cookie name inside of my new sidebar component by ShadCN. So that is actually quite impressive because that's the first thought I had when I wrote this in a little string. I thought I should probably export the constant. And in here, it tells me to do that. So that's one thing we can do for sure. But everything else here is, I don't want to change it, even though it's probably a good suggestion simply because this is the CLI tool and I don't want to mess with these components if they work. So I'm going to merge the pull request and let's go ahead and wrap up this chapter. Let's go back to main and let's synchronize changes. And once you synchronize your changes, go ahead and open graph and in here you will have your new pull request. Amazing, amazing job. Let's go ahead and mark this as completed as well and see you in the next chapter. In this chapter, we're going to continue our development of the sidebar by adding a global theme. So I'm going to show you how I create the theme for my project and I'm going to share my globals.css file with you so that you can have the exact same look in your project. So the way I create my themes is by using ThweakCN. You can use the link you can see on the screen to let them know you came from this video. So this is a super simple application that allows you to create your custom ShadCN UI theme. And as you can see, this is actually the exact theme that I used for my previous project, which was the lovable clone. So the way I do this is just by clicking start customizing. and in here I go over a bunch of these themes until I find one that I really like. And once I find something that I like I just go ahead and slightly modify it. So in this case this is the theme that I have selected but I did some little changes here and there. You can also switch between cards, dashboard, mail, pricing just to see how it all looks together. So now what I'm going to do is I'm going to show you where you can find my exact globals.css and that is using the link on the screen to access the assets as you've already did a few times in this tutorial. And in here I have added the UI folder and in here globals.css. Now I just want you to be a little bit careful when copying this for a very simple reason. When you go inside of your packages, UI, source, styles, globals.css, just make sure that these are exactly the same as these because previously, if you try to do this in like a normal copy, if you try to copy global CSS from like a normal single repository Next.js app, it's going to break the styling. So just be careful, right? If you want to, you can actually just copy from the root all the way to the layer base, like that. And then I'm going to do the same here. I'm going to select the root, and I'm going to go all the way to the layer base, and I'm going to remove it. So this is how it looks like. And then I'm going to paste it. and you can see that I also added this layer base which basically returns the pointer cursor to each button so in here basically I mostly copied from Twixien after I added my theme and then maybe I did some slight modifications here and there I'm not exactly sure and also an important point, I have only optimized this for light mode. So I didn't really bother with dark mode for this tutorial because I think customer support apps look much better in light mode. So let's go ahead and check out our web dev and let's see if anything has changed at the moment. So we just added our new globals.css. So it has to probably compile that. And I don't know if you can see any difference but I think in our previous chapter this was darker. Now it has a white background. So now what I want to do is I want to show you how I customized my active button because you can see that in here my active button is fairly different So I going to show you how I did that Let go ahead directly inside of dashboard sidebar component And inside of here let find the first sidebar group where we go over some items And let find the sidebar menu button. And after it's active, I'm going to add a class name prop and I'm going to use the cnutil to make it dynamic. So just make sure you have imported the CN from workspace uilab utils. And now what I'm going to do here is I'm going to very simply check if is active item.url and I'm going to go ahead and write my class. So I'm going to be using a gradient to bottom. So the reason we are adding this as a direct explicit class name rather than some variable is because I didn't exactly find a way to do that for gradients. So that's the only reason we are writing an explicit class name here. So let's go ahead and do from sidebar primary and then let's go to and now let's write a hex code of the blue color that we need and that's this one. And make sure to put an exclamation point at the end. Let's add text sidebar primary foreground and hover to and let's go ahead and choose the same hex color but let's go ahead and give it a 90% opacity like that and there we go. You can see now how it looks like exactly as in my example and now what I want you to do is just copy this class name and just add it to your other sidebar menu buttons here. So configuration items and accounts, I believe that is the last one. So now all of them, as you can see, will have the exact same look. Perfect. So I think that there are still some slight differences between my sidebar and ours. So let's go ahead and do a few things. Let's go inside of sidebar.tsx inside of packages UI source components. And in here, let's go ahead and let me export sidebar cookie name. I was thinking of maybe changing the sidebar width, but actually I think it's okay. We're going to change the width if needed once we add the chats panel here and the whole chat interface so then we're going to see is this too far or not right and also try collapsing it so you can see how cool this looks I really really like the sidebar like that and I think this gradient effect is very subtle but cool enough you know you can see it's a little bit different it's not just a plain blue color it's like a very subtle effect I personally like it you can of course choose however you want. So I want you to export this constant sidebar cookie name and then go back inside of the layout of the apps web app dashboard. Is it that one? Actually go inside of the dashboard layout components, my apologies. So apps web modules UI layouts dashboard layout here and what I want you to do is very simply import the sidebar cookie name so you don't accidentally misspell it. So this way we fixed that suggestion from CodeRabbit from before. Hey there, this is CodeWithAntonio from the future and I am actually editing this video where we add our theme and where I just told you to add this sidebar cookie name constant. What I did here is I added this constant but I never actually tested if our app works because by all logic it should work normally but if you actually tried out your dashboard sidebar will always be collapsed. See even if you expand it and then refresh it will be collapsed. The reason behind is probably because due to TurboRepo, Monorepo architecture, this constant isn't loaded at the time it needs to be loaded. So this constant is actually undefined. And then default open is always false. Go ahead and try it. If you use a constant here, which is sidebar state, it will always be closed. You can't keep it open via local storage. But if you just copy the text that was here previously, and paste it here and remove the constant and then refresh. You can see that it starts to work again. I just wanted to bring this to your attention because I actually didn't notice this bug until chapter 18. So I just want to give you a tip ahead. Don't use that constant even though I literally just told you to do that. It unfortunately causes the bug. Keep it as a string instead. but just make sure that it is the same string as sidebar state here. Perfect. And also, let's go ahead and do the following. I really want you to go inside of your widget, page.tsx. So instead of apps, widget app page, and go inside of useWAPI here, and please remove your API keys from here, like that. and after you removed your API keys let's go ahead to VAPI and let's remove them together. I just don't want you to accidentally forget about this because I didn't explicitly do it I just told you to do it so after that go inside of VAPI keys here and just delete all of your public API keys you don't need them for absolutely anything besides some internal testing here. The reason I'm telling you to do this is because you may accidentally, you know, publish this repository that you're working on and then all of your keys will leak. So there we go. You just click the delete button and then it will delete after some time. So please go ahead and do that. Don't let your keys leak. And you can, if you want to also delete the assistant, but that's not too important. The assistant ID isn't exactly a secret. It's just the entity ID. So you don't have to delete Tom. Tom is fine, but please just hide your API keys. Excellent. And after you have done that, I think this marks the end of this very, very simple chapter where we just added a global theme. So from now on, it's going to be easier for us to work here. And I don't know if you noticed, but our font just became a little bit bolder. That's because by default, I've set the font to be medium. So I think that even this normal text now should have font medium. Let me just check inside of computed. Let's go and find font weight 500. I think that's medium. Not exactly sure. But yeah, we are going to be basically working with this theme from now on. Excellent. So I believe that marks the end of this chapter. end. It was a very short chapter. There is really no need to review this, but I will still follow the normal commit flow. So I'm just going to go ahead and stage all of my changes. 08 theme, commit. I'm going to go ahead and open a new branch, 08 theme, and I'm going to publish that branch. I'm going to go inside of my GitHub repository here I'm going to open the pull request and I will immediately merge it so the only reason I'm opening this pull request is for the history right so that I know it happened and so that I have a checkpoint to always go back here if something goes wrong really no need to review this because it was super simple so just make sure to go back inside of your main branch and just synchronize your changes. You can also use this button, I think. I kept using the one below, but I think they are the same buttons. And then in here, you should now, well, you can check in the use of API, and there we go, these are empty. Globals.css, well, I can't really recognize if it's different or not, but I'm pretty sure it's the correct one. Great, amazing, amazing job. We have pushed these changes, and see you in the next chapter. In this chapter, we're going to develop the widget layout. This will include creating the widget view, creating the widget header, and creating the widget footer. We're going to use this as the base for building further widget screens. Let's go ahead and let's create a widget view. So I'm going to go inside of apps, instead of widget, and I'm going to go inside of modules, and instead of my widget module, I'm going to create UI and inside views and let's create widget view.tsx. I'm going to go ahead and mark this view as US client because we're going to need it in the future. And let's go ahead and let's create an interface, props, organization ID, and let's go ahead and give it a string. And then let's export const widget view here. And let's go ahead and destructure the props, which only hold the organization ID. And inside of here, let's go ahead and let's return a div. Actually, we can call this main, like so. Let's give the main element a class name of flex, full height, full width, flex column, overflow hidden, rounded, extra large, border, and background color muted. And now let's go ahead and simply write widget view inside. That's all for now. Now let's go ahead and let's create the actual page. So inside of our app, let's go ahead and go inside of page.tsx like that. And in here for now, we can remove the useWAPI import and we can remove all the other elements inside. And this does not have to be, actually leave this as useClient as well. And let's go ahead and let's import widget view from modules, widget, UI,\nViews, Widget View, and simply return the Widget View here. Now in here, we expect the organization ID prop. So let's go ahead and add it here. So the way we can destructure the organization ID prop from here will be through search params. So since this is a reserved page.tsx file, that means that we have some special access to the props here. So that means that we can actually create an interface props, search params, which is a type of promise, and inside organization ID string, like that. And then in here, let me just refactor this. I don't like to use this method. And then let's just do export default page, like that. and then props and the structure search params. And now let's go ahead and let's destructure the organization ID by using use from React. Make sure that you have use client at the top of this page.tsx and inside of here add search params. and now you will be able to pass organization ID like so. And now just for fun, let's go inside of the widget view and let's also render the organization ID just so we can see if we pass it successfully. Now let's go ahead and let's run TurboDev and let's go to localhost 3001 where our widget is running. So you can see that now we have the widget view but nothing inside. So if I go ahead and change my URL and added a question mark organization ID 123, you will see it says 123. Now I'm going to show you the entire URL. So localhost 3001 and then question mark organization ID equals 123. So this organization ID capitalization matters. Make sure that you didn't misspell it here in the search params here and here and here. All of that matters. So this is how other users will load the widget. We are going to create a script that users will add to their website and that script will load localhost 3001 with this organization ID in an iframe on their website, rendering the widget view for that specific organization. That's how it's going to work. so once you've successfully managed to render one two three let's go ahead and edit this view so now I'm going to go inside of my UI here in the widget and I will create components and inside I will create widget footer.tsx let's go ahead and export const widget footer and we can actually use the footer element. Give it a class name of flex items center justify between border top and BG background. And now inside of here, go ahead and add a button from workspace UI components button and render a home icon from Lucid React here. Go ahead and give this home icon a class name, which will be dynamic using the cnutil. Give it size 5 by default. And then let's go ahead and do the following. Just for fun, we're going to define the current screen that is active. And let's go ahead and make that selection. And then we're going to check if screen is equal to selection. In that case, we're going to use text primary class name. Like that. Now let's go ahead and give this button some attributes. Class name, height 14, flex 1, rounded, none. On click for now it can just be an empty function. Size will be icon, variant will be ghost. Now let's go ahead and duplicate this entire thing like this. And this one will be using an inbox icon from Lucid React and check if the screen is inbox. and yeah, there is a type error here. So yeah, you can just ignore it for now. It's because we are hard coding this to be selection. So this will never be true. So let's just go ahead and wrap it up. Widget footer, perfect. And now let's go ahead and let's create a widget header component. Now the widget header will be quite similar. It's going to be a very simple component. but it will accept some classes and those will be children which are a type of React, React node and class name which will be an optional string. So that's the structure, the children and the class name. And inside of here we're going to return a header element with a class name cn which of course we have to import from our workspace ui lib utils background gradient to bottom from primary to and then we're just going to use the same blue hex color that we used in our sidebar in the previous chapter. Give it a padding for and text primary foreground. And then in the second argument pass an optional class name. And inside render the children just like this. now let's go inside of the widget view and let's go ahead and add widget footer at the bottom like that so now you have this of course depending on how zoomed in you are you might see it very very wide or you might see it like this just remember that this will be rendered inside a limited width and height iframe so this will not be rendered in a full screen like this so if it looks weird that's why and now let's go ahead above here and let's add the widget header component header like this and there we go we now have header we have the body and we have the footer and let's just simulate how this will actually look like here by adding a div around the body and giving this a class name of flex flex one. Or that is not enough, it seems. So let's see flex, flex call, hide full. Let me check the page. Okay, the page is good. Let me check the layout. Is there something that we have to do here? So we have the providers. not 100% sure why this isn't taking full width. Let me just debug a little bit. All right, so I didn't really debug it, but I do think it's missing a minimum height of screen that you have to add here, and then it will take the entire space. I'm just trying to remember why I did not need this class name in my original source code. So I'm trying to think if maybe there's a reason that when you render it in an iframe, it works differently. So I'm going to add a little to-do, confirm whether or not minh screen is needed. And while we are here, we can also add a minimum width of screen as well. And let's go ahead and add that to the comment as well. And I'm pretty sure this is not how you spell weather. weather maybe like that okay perfect so what i want to do now is i want to build a hi there how can we help you today type of widget header so instead of this widget header let's go ahead and let's create a div inside let's give this div a class name flex flex column justify between gap y2 px of two and py of six and then in here let's add first paragraph hi there and we can just use the waving emoji and below that another paragraph how can we help you today i use raycast to add emojis on my macbook you can just google hand waving emoji and then add it here or you just don't have to use the emoji let's go ahead and add a class name font semi bold and text text3xl and for the bottom one font-semi-bold text large so we can transfer font-semi-bold to this div to reduce repeating class names let's go ahead and try it out and there we go hi there how can we help you today perfect and this is basically how we're going to build our future widget views. Basically, we're going to have the out screen, the inbox screen, the chat screen, the phone number screen, and the voice agent screen, and all of them will reuse these components. And I will actually not be using the Next.js router here at all, for a very simple reason that technically this widget app does not have to be server-side rendered or things like that. So it could be a single page application without any routing whatsoever. So we're actually going to be using, I'm definitely pronouncing it incorrectly, Yotai state management to create our own little router. It's going to be very cool. And this way you will be able to migrate the widget app to Vite, I mean to Vite, to React, whatever you want, right? But I will continue building in Next.js simply because I'm familiar with it, but we're going to build it in a way that is Next.js agnostic. So you will be easily able to move it out of that if you want to Perfect So I think this is exactly what we wanted for this chapter Obviously if you zoom out a lot it will look huge But just remember this will be rendered in an iframe with fixed constraints, right? So it's just going to be limited to this wide and this high. So don't worry that it looks super weird, right? If you want to make it more realistic, you can, I don't know, displayed in mobile mode, this is how it's going to look like basically. Perfect. So let's go ahead and commit that. So this was a shorter chapter, but a sweet one. Nonetheless, let's go ahead and stage all changes. Let me see. So 09 widget layout, 09 widget layout. Let's commit. I'm going to go ahead and open a new branch, create new branch 09 widget layout like this. and I'm going to click publish branch like that. And let's go ahead and review our changes just in case that we didn't do any catastrophic mistakes here, even though this was just UI. So 09 widget layout. And while CodeRabbit is reviewing here, I actually want to show you that there is another way you can use CodeRabbit. You've probably seen this a few times. So whenever I do a commit, I can actually review within my IDE, right? And then I can go ahead and select CodeRabbit here and now CodeRabbit will review all of those changes that I've just done within my IDE, as you can see right here. And the way this works is by a completely free extension. That's right. You can go ahead and install CodeRabbit and just connect your account and you will just get free code reviews. There's no catch. That's it. It's literally free AI code reviews. And it's a super cool tool. Just for now, I'm going to stop this review simply because we have the same thing happening here. So no reason to review it twice. But yeah, feel free to install the CodeRabbit tool. I think it's amazing. The reason I like the pull request one more is simply because it gives us kind of a description. It gives us the sequence diagrams and it kind of closes the chapter in a better way. And here we have the summary. We introduced a new widget interface featuring a header, footer, and main view displaying the organization ID. We added customizable header component for flexible content and styling, as well as a footer component with navigation buttons for improved user interaction. Now in here in the code review, it does tell me that I have to remove use client. the reason I put use client is because in here we're actually going to use dynamic import to completely turn off server-side rendering for our widget view but I didn't want to do that in advance because I wanted to wait until we have to do it so you see why we are doing it so that's the only reason I put use client right now because in order to use dynamic imports I'm pretty sure you need use client. So that's the only reason I put that. And in here, it is telling me that we have to check if we have organization ID. That is exactly what we are going to do, but in a different way. So we are just building a component here to set up the base components. And we are now in the next chapter going to learn how to create screens, which will check for organization ID and things like that. In fact, we will have a special error screen that will show if organization ID is missing and if organization ID is invalid. And of course, we have a hard-coded selection screen, so we will be changing that in the future. This was a fairly simple pull request, so let's go ahead and merge it. And once you've merged it, go ahead and change back to your main branch. And you can press synchronize changes here and OK or use the little button below. Sometimes it can just look like this. You can press it as many times as you want. And once you've done that, make sure to check your graph to confirm you have detached nine and then you've merged it. So it should look like this. Excellent. I believe that marks the end of this chapter. We've created the widget view, widget header, and the widget footer, as well as the GitHub workflow. Amazing job and see you in the next chapter. In this chapter, we will develop a widget session authentication. This is basically out for anonymous customer using our chat box. And there are many ways to implement this. I chose the session-based anonymous authentication method. This approach will let us collect minimal user information, name and email. And using that, we're going to create a unique temporary 24-hour session. This will allow us to implement low user friction and let them immediately jump into the conversation while still allowing us to maintain conversation continuity. The session, which lasts for 24 hours, will be able to get refreshed if the user is still active within those 24 hours. But if the user is inactive, we're going to remove the session for privacy protection. So what are the benefits of this type of authentication? Well, first of all, it is low friction. When you're working within a chat box, you want your users to be able to ask a question as soon as possible. And I analyzed a lot of existing chat boxes and I was super interested in how they do this. And most of them do this very thing, right? Perhaps they don't exactly show an out screen, but they let you ask a question and then the chatbot asks you for your email. That's the same thing, right? We're just presenting that as an out screen like that. But the cool thing is that we don't need any passwords here. Now, just don't get it wrong. Email is not an identifier in any way. It is just some extra information. So it's not like you will be able to enter anyone's email and then load their conversations. No, that's not how it's going to work. That's simply user metadata. So if the user session expires, that's where the operator will be able to contact them to continue the conversation. So those are the benefits of this authentication. What are the cons. Well, the first con is no email verification as you've just heard, which means that anyone can enter anyone's email address. I did test some other chat boxes and pretty much all of them allow that for the same reason we allow it. It's nothing more than just user metadata, so the operator can maybe contact the user on that email address. It is not used as any unique identifier for the user, so it's technically not exactly an issue but yeah you have to understand that anyone can enter anyone's email address using this method. As per technical limitations sessions can get lost if user clears their local storage or if they switch devices so we are not protecting against that. And for the user experience issue well it will depend on how long you allow the expiration right If you make the expiration two hours, users will probably be annoyed that they have to enter their information all the time. So the longer you make your expiration session, the better will user experience be, but more privacy concerns will happen. Well, more technically, security concerns, not exactly privacy concerns. And of course, I don't exactly know where you are and you should abide by your jurisdiction laws about collecting user information because we will be collecting some additional data here like the time zone, the country the user's from and things like that. So you should research that on your own if you're planning to get this to production and make sure you abide by those laws here. Great. So now let's go ahead and see what we will be doing. We're going to create the contact session table in our schema. We're going to create contact session functions so we can create a new contact session. And we will create the out screen, which is essentially a form to create that. And in here, I'm going to come to the end of the tutorial and explain how this will all work. We're not going to implement all of this right now, but we will come to this part. So this part will be done. This local storage and validating the session and checking expiry will be done in some other segments, but I am still giving you the full picture here so you understand it better. Let's start by adding contact session table. So let's go inside of our packages. Let's go inside of backend, convex, and let's go inside of schema here. So far, we only have users. Let's go ahead and go to the top of our schema and let's add contact sessions like that. And let's define the table. Every contact session will have the name as you've just seen on the widget out screen. Then we're going to have the email. We're going to have organization ID, meaning for what organization is this contact session being created, right? Make sure you don't misspell organization ID here. And we're going to have expires at, which will be a type of number. And then we're going to have a metadata object, which will be completely optional. And inside, we're going to add an object. And this object will be able to store as much information as you want. For example, user agent, language, languages. Let's make this optional. And it's also a string. Platform the user is on. Vendor that the user is using. Screen resolution. So basically, what I'm doing now is I'm just adding as much things as possible. but you're going to have to check which things you want to use and which things you are allowed to use depending on where your users will be, of course. So in here, I'm just teaching you all the things that you are able to add from the user's browser. So I'm just adding here all the things from my source code and we going to read them now So user agent language languages which are optional here platform vendor screen resolution viewport size time zone time zone offset cookie enabled referrer and current URL. You can, of course, limit this to just language if you want to, right? It completely depends on you. and actually I think it might be a good idea to just make all of them optional right let's assume we can't get all of them so let's mark all of them as optional there we go so what I did is simply wrapped all of my current types into v.optional like this so the same thing we did with the metadata and languages was already optional so now all of them are optional I think it just makes sense, right? You don't know what you're going to be able to capture. You don't know what's available. Maybe user is using some super weird browser. So I think this actually makes more sense. And let's go ahead now and let's create an index here. So we're creating an index on the entire contact session table here. So let's go ahead and let's add dot index buy expires at and simply target the field expires at like that. So now we can very quickly query buy expires at if we need it and actually think we can add one more index here buy organization ID organization ID. We're going to see if we need this or not. I think we might actually need it If we don't, thankfully, it's super easy to remove an index in Convex. You just remove it here. That's it. Once you've done that, make sure you run TurboDev in the root of your application so that you have your backend running. And you can see that that is going to check for indexes. And you can see it added two indexes. And it validated the schema. So that's how you know you did everything correctly. If you're getting any errors, you did something incorrectly. So please double check with the code here. Perfect. Now let's go ahead and let's create a simple create function for this contact session. So we're going to do that here instead of the convex folder. And previously, I taught you that you can just create, you know, users.ts and write the functions in here. And sure, you can do that. But I think it's time for us to create a structure that we're going to follow in this tutorial. The structure that I chose is to create a folder called public. And then we're going to have a folder called private and one more folder called system. And the reason I chose that structure is because some of our convex functions will be publicly callable from the public widget component. So I want to semantically separate them in that folder so that I know whatever I write inside of this public folder should be considered accessible by anyone. That's why I'm doing that. And that's why I think you should do it too, so that you have all of your risky functions in one place. So let's go ahead and create contact sessions inside. Now in here, let's go ahead and let's import v from convex values. Let's import mutation from generated server. And let's export const create mutation. let's go ahead and open the arguments we're going to accept name we're going to accept email we're going to accept organization id and we're going to accept metadata so inside of the metadata here we need to pass an object and inside of here we basically have to add all of our items. So what you can do is you can just go back inside of the schema and you can just copy all of them like that. So after you have written your arguments, go ahead and add the handler, which is going to be an asynchronous method. Like that. And the second parameter are the actual arguments here. So now let's go ahead and let's define the session duration. How long will this session last? So I'm going to go ahead and do constant session duration and I'm going to make it 24 hours but in milliseconds. So let's add a comment, 24 hours in milliseconds. Or we can just use this so we don't have to write unnecessary comments. There we go. And now let's go ahead and use that to create our expires at. So let's get the variable now to be date dot now. And then let's define expires at to be now plus session duration in milliseconds. Let's define const contact session ID to be await context.database insert into contact sessions. So yes, convex database insert will always return ID. That's why I named this ID in advance because I knew that convex's insert always returns back the ID that was created. Let's go ahead and let's add arguments.name, email, arguments.email organization ID arguments organization ID expires at metadata arguments metadata like that and let's return contact session ID. There we go. So make sure we have this saved and let's check if our functions are ready. So you can see whenever you have any errors in your code, the convex backend will not compile. So make sure that all of them is correct. So you will see the success message here. Perfect. So I think that this is okay to be passed like this, I guess. We'll see. Now let's go ahead and let's actually create the widget out screen. So for that, we're going to go inside of apps, widget, modules, widget, UI. and let's actually create a new folder called screens like this and in here let's go ahead and let's create widget alf screen.dsx and in here we're going to import workspace header my apologies widget header not workspace header we're not going to use the footer so no need for that. Let's just export const widget out screen for now. And let's go ahead and return an empty fragment widget header. And instead of the widget header, we can actually copy what is currently inside of our widget app page, widget view. There we go. So you can copy this entire widget header and you can actually cut it from the widget view and then put it in the widget out screen. And in here, instead of how can we help you today, let's indicate that this is kind of the out screen. So let's say, let's get you started. And you can replace this with appos like that. So let's get you started like that. And let's go inside of the widget view and instead of this, I am just going to render the widget out screen. And for now, you can comment out the widget footer and you can comment out these two imports because we're not using them right now. So widget out screen. Make sure that you have your widget running and head to localhost 3001. And this should load the new let's get you started with no footer. So there should be no footer because we just commented it out and we are only looking at the widget out screen. And now that we have this ready, we have to import form components. So in the previous or the chapter before the previous chapter, I think we added all components from Shatian to our project. So now let's go ahead and let's add them. Let me just replace this. I really like to use modules, widget, UI. I really like this. Okay, let's import from workspace UI components form, form message, item field, control and form. Then let's also import the button. Let's go ahead and import the input. and let's now import use form from react hook form if you're wondering uh where do we get this from well use this should be installed in packages ui package json i think react hook form let me check. Yeah, you can see React hook form exists here. But the reason we're getting errors is because it also needs to be installed in the app that we are using it. So we're going to have to be careful here. We're getting to, and I think the same is true for Zod, which we also need. Yes. So if you check the package JSON of your packages UI, Zod also exists here. So I think we're going to have to be careful and try and install the same versions here. And let me just check, there is one more thing that we need, and that is hook form resolvers Zod. Now, when I tried to install these packages the first time in my original source code, I had a lot of bugs, a lot of mismatches. so we're going to go ahead and try and do this together now so this is how I'm going to attempt it I'm going to find the version of Zod that I need and I'm going to shut down my app in the root of my app I'm going to do pnpm filter widget right this is for widget and I going to attempt to add the exact same one so add and let me just see can I do this Okay so Zod at and I not sure if I can do this when I adding it like into the terminal like that so let me try and add it here and see if this will give me this exact version or not so first things first this should now disappear great but let me check exactly what version was added looks like that exact version was added so that's great uh look is it uh i guess this is how you do it then uh obviously if you have any recommendation for me of how i should enhance using pnpm and these versions please write it in the comments or email me whatever i would love to learn more so now let's do the same thing for react hook form let's go ahead and do same thing so react hook form at 761.1 of course you use the I mean you should have the same versions as me but just go inside of your packages uipackage.json let me show you where that is inside of your packages uipackage.json here and just find the version so make sure you're adding this to the widget let me check if that is correct now so i'm gonna go here and check react hook form i think that's the exact version perfect and we also have to do it for hook form and this hook form was giving me a lot of problems so we're gonna see now widget add hook form resolvers 5 to 0. So hook form resolvers, making sure this is the one. And we're going to see, yeah. Basically for me, this hook form resolvers was expecting a completely different version of Zod and it just caused such a weird mismatch. I think this is because Zod is now getting a version 4 and everything is just bundled a little bit. So let me try this again. Inside of my package.json for the widget I now have hook form resolvers react hook form and zod all seemingly correct versions let's see let's go ahead and try doing turbo dev if it doesn't work well we're going to debug looks like it's okay but let's wait until we actually add some components to see if it's okay or not. So back instead of the widget modules UI screens widget out screen. Perfect. No more errors here. That's great. Now let's go ahead and let's quickly define the form schema for this out screen. So you can do that by using z.object and adding the name with a minimum requirement of one and name is required and email z.string.email invalid email address. Form schema is the constant perfect and then inside of your widget out screen here what you're going to do is you're going to define constant form to be use form which we import from react hook form and inside open this type annotation and pass in z as zod.infer type of form schema constant which we just defined above. Then go ahead and execute the use form hook and pass in an object inside with a resolver, zod resolver using again form schema and default values for our two fields. Perfect. And now I'm going to create const on submit method here which is going to be an asynchronous method and the values here will be z.infer type of form schema and inside we can just console log the values for now. Perfect. Now let's go ahead and let's go below the widget header here and let's add form. Let's go ahead and spread form inside. Then let's add native form element. Let's give it class name flex flex one flex column gap y4 and padding four let's give it on submit here to be form handle submit on submit like this and inside let's do form field and let's give it control form dot control name will be name render will destruct the field like so and render form item so this composition if you're getting confused like how do i know this this is just chat cn way of writing forms nothing special right i just learned this composition because i've done it a million times you can find it in chat cn documentation just type form right inside of each form item there is form control and then you render the type of component that will be controlled in this form. In our case, that is just a simple input. Let's give this a class name, height 10, nbg background. Placeholder will be, for example, John Doe. Type will be text. I always like to explicitly add my types, even though this is the default. And let's go ahead and spread the field property. The field property basically has all the useful. You can check it if you go inside a form field controller props and somewhere you can find it. Basically, it will give your input on change, on blur, on focus, all of those useful things. Okay, so I've saved this. And now let's try and visit this page. Let's refresh. Let's see, are we getting any errors? And there we go. We are getting some errors. So we do have to fix them. All right, the good news is I've already encountered this before, so I know it's fixable, but I will have to pause the video a bit and I'm going to try and find the exact discussion that helped me fix this issue. All right, so first of all, here's what I googled. So I put module not found can resolve Zodv4 core and then I added hook form because, well, that's the error we are getting and I can see that it's coming from hook form resolvers because if you just google without hook form it will give you some similar issues but they're not exactly associated with our problem our problem is within hook form and I found this github issue here which seems to describe our exact problems so pnpm and hook form resolvers that is pretty much the exact thing that's happening to us here. Now in here there are several fixes so this person here seems to suggest to check the installed ZOD version and if it's below 3.25.0 upgrade to the latest ZOD 4 release. After upgrading delete node modules and the log file and then reinstall dependencies and if you're still seeing issues you can try setting this module resolution in tsconfig.json. Another person here says that all they had to do was pnpm remove zod and then pnpm add zod 4.0.5 two weeks ago so it seems promising but here's what I found this was also two weeks ago in here you can see that I actually marked this for myself I added some emojis because this is what obviously helped me in my project what this person did is they downgraded back to a working version of zod so in our case let's see what is our zod version so it's 3.24.2 and they're using 3.2567 so what if we went ahead and modify our zod version to this one first let's go ahead and try that uh or actually you know we can also try doing this. We can try pnpm remove zod. Let's try that. So I think this is a good exercise to try and see how we can resolve this. So let's first try pnpm remove zod. Because, you know, if we can use the newer version, why not? Okay, so I can't just do this directly in my root file. I assume I have to filter to a specific place where I'm using it. let's see what if I changed to this Zod version in all of my places so what if I did this change it here and in my packages UI I also changed it to that version and what if I then run pnpm install in the root of my app so that should now modify those two packages okay and then what they did is they removed node modules and pnpm lock file. Should we do that as well? Let's go inside of apps and let's consider widget because that's where we just added this. Let's do remove node modules. Okay, rmrf node modules. And they also mentioned removing pnpm lock file. Not too happy about that, but do I have, let me just see. do I even have that file there? I don't think I have that file inside of my widget. Yes, I do not have it. So let's just do RMRF node modules and let's go ahead outside of our app to the root, pnpm install again. Okay, let's try Turbo Dev now. Focusing on the widget, this is the one we just modified. let's try refreshing localhost 3001 and it seems to be fixed seems to be working this is obviously how i fix it in my initial project now i hope you manage to fix it as well because it looks like you will certainly come across this issue so what i'm actually going to do is i'm going to well I mean you can I just told you how to find this issue but basically I suggest that you try any of these solutions that they're giving you here right but I think that this person has this most similar example to ours here right so he's using hook form resolvers version 5 he's using a similar version like us looks like his is a little bit upgraded I mean theirs so I think this should be working because they're using PNPM and they're using Turbo Pack ours seems to be working fine what I would suggest now after just trying this also try Turbo Build so just make sure that this didn't break the build\nIf the build passes, I think that's almost like, well, ours just failed. So let's see why. Because of type. Perfect. That's actually perfect. Let's just quickly check this. So sorry for being so over this. I just want to make sure that you can follow along. I don't want you to be stuck with an error because this is something new here that we are doing. Let's go inside of components, widget footer here. and let's fix this by just let's just do this it's easier to check okay let's do turbo build I just want to see is the build still failing because of this hook form or is it all good I think we are all good if it passed the linting test there we go so it's still building the web so let's just wait for the web to see what's going to happen here because I want all three of them to pass all right here we go all three builds successful so I think that is as much proof as we can get that we fixed our issue if the development is working if turbo build is working I have to assume that we fixed the issue so we're going to probably have to revisit this problem very soon when we add the same packages to web, right? So let me just quickly check. Our Zod version everywhere is 3.25.67. Resolvers, 5.2.0. And that seems to be okay. So I think this is good. I think we can now go ahead and continue developing. Perfect. So let's go back inside of widget alt screen here. and we just added the first input here. Now let's go ahead and let's add the second input. So you can just copy this entire form field and just paste it below. And this one will control the email field. And for example, put John Doe example.com. And the type here will be email type. And you can also add form message, which is a self-closing tag. beneath each form control. And what this will do is simply render any error if it exists. Now outside of this self-closing tag, add a button with continue text. And let's go ahead and give it a disabled prop if form, form state is submitting. Whoops. Size is large and type is submit. All right. Let's refresh. Oops, yeah. Make sure you have TurboDev running and head to localhost 3001 and you should see your fields here. Perfect. Now let's go ahead and let's add, let's first test the submit method, right? So if you just press continue, you can see errors everywhere. But if you try test, If you try test here, it tells you it needs to be an address. And when you hit continue, there we go. We have the values email and name. Perfect. So now what we're going to do is we're going to go ahead and create the mutation to store our contact session. So const create contact session will be use mutation from convex react package. And go ahead and add API from workspace backend generated API. API.public.contactsessions.create. That's the one we need. Excellent. And the first thing I'm going to do here is I'm going to check if there is no organization ID because we need the organization ID. Yeah, slight problem here is that we are not passing the organization ID here and we are not going to pass it as a prop actually. we are going to pass it through our state management. So for now, just for now, I'm going to do const organization ID 123 and add a comment temporary test organization ID before we add state management. All right. So I'm going to continue writing the code as you normally would. If there is no organization ID, I'm going to break the function because we have to make sure that organization ID is present. And now we're going to create our metadata object. So this is where we basically add all of those fields. So user agent can be accessed to navigator.userAgent. Language, well, language can be accessed through navigator.language, like that. Languages are an optional array. so you have to join it with a comma like this. Now you can access the platform but you can see it's deprecated. Even though I was able to extract the platform, as I said, you don't have to track all of these things from a user. I'm just giving you some interesting information that you can do here. So vendor, screen resolution, you can take the screen width and the screen height. Viewport size, same thing. So these things are sometimes useful to debug, to help debug your user. Like are they on mobile or are they on desktop, right? Time zone, use ENTL, date format, resolved options, time zone. Then the time zone offset, new date, get time zone offset. Whether the cookies are enabled for the user. and let's go ahead and try the referrer or fallback to direct and the current URL. And I think that you might actually be able to give this a type of document, contact sessions, metadata, like this. So if you try adding something like this, it will give you an error. And yes, you can import the doc from doc workspace back and generated data model. I think this is a little bit safer because it will warn you early if something's incorrect here. So make sure that it matches your schema metadata. Excellent. And once you have that, you are ready to get the contact session ID through await create contact session. And pass in, I think we can just spread the values and then metadata. and organization ID. I think that is it. Perfect. So now after this, what we're going to do is just console log the contact session ID. That's the only thing we're going to do for now. And let's also prepare our convex dev simply so that we can actually check in the database is it created or not. So inside of my echo tutorial here, inside of my data, contact sessions. There we go. It's currently empty. So I'm going to go ahead and create Antonio. Antonio at example.com. Continue. And we have the contact session back. Perfect. And in here, we have the email. We have the metadata for the user. You can see all of that information about where I come from, all of those things. Perfect. and organization ID, which is just a mock, as well as when this will expire, right? So you can see that this will expire in 24 hours, which according to my date is correct. Perfect. So that obviously works. So in the next chapter, we're going to implement the actual, well, saving this into the local storage and the state management and maybe redirecting the screens so we can initiate a conversation finally. Perfect. So let's go ahead now and let's mark this as completed. So we added a table, the functions, and created the widget out screen. And now let's go ahead and push this to GitHub. So 10 widget session. Let me just go ahead, close this, add all changes, 10 widget session. Let's commit. Let me open a new branch, 10 widget session. and I'm going to publish the branch. Then I'm going to go inside of my Echo tutorial repository here and I'm going to compare and pull requests and let's see what CodeRabbit has to say about this change. And here we have the summary by CodeRabbit. We introduced a user authentication form that collects name, email, and browser metadata with validation and backend submission. We also added backend support for creating set contact sessions, including a new database table and mutation for storing user and metadata information. So what I find very cool about CodeRabbit here is that its sequence diagram here looks so familiar, almost like the one I presented to you in the beginning, right? So this is the one that I manually created. So I have some more information because I know how it will behave in the future, right? But it's super impressive how CodeRabbit itself managed to recreate that without ever seeing that diagram. That's exactly what we do. We submit form name and email in the widget out screen. We validate the input via Zod. We collect browser metadata and we then submit that form data with the metadata. data. And then we insert the contact session record and bring back the session ID. Perfect. So in here, it left a few comments, all completely valid, but some we know are temporary, like this organization ID. This is completely temporary. We are going to change this in a different way. We are not going to be passing props here. In here, definitely a good suggestion. It recommends handling this instead of try and catch to log some errors. We're going to do that in the next chapter we're going to add some error logging. In here it suggests validating instead of type vstring to use vemail. The problem is this is in the convex backend so I have to research if they have validators for email or not Definitely good suggestion And another very good suggestion here we should always validate if the organization ID that we are passing and creating the contact session into actually exists, right? This is a little bit problematic because organizations are handled externally. So we're going to see how we're going to do that. In my original tutorial, I don't think I've validated that, but very, very good idea to do this. When I say my original tutorial, I meant my original source code. Excellent. Very good suggestions. I will keep all of them in mind, especially the error handling and organization validation. Perfect. Now let's go ahead and let's go inside of our main branch. Let's synchronize our changes here. Perfect. and once they've been synchronized, I always like to revisit the graph to confirm. We detached in the chapter 10 and we merged it back into our app. I believe that that marks the end of this chapter. We successfully pushed to commit. We've committed the changes, pushed them and reviewed the PR. Amazing job and see you in the next chapter. In this chapter, we're going to develop the widget screen router. This is basically going to be our own custom routing system using Yotai as our state management and my apologies in advance if I'm pronouncing it incorrectly. Basically the reason we are doing this and not using Next.js routing which is completely fine by the way you can implement it with that too but I kept thinking about this widget application and I just feel like it's a very very small app. The reason I initialized it with Next.js is so I can share components from ShadCN. So I have the exact same style. That was why right. But I keep thinking of it as a very very lightweight component that could easily be moved into just React with Vite or something like that. So for that reason I don't want it to depend on Next.js routing. So that's where we're going to implement our own. So let's start by adding the yotai package. So let's go ahead inside of here pnpm f widget add yotai. Feel free to correct me in the comments how to pronounce this if I'm doing it incorrectly. Perfect. So we just added that package here inside of our widget package JSON. Now let's go inside of a widget and let's go inside of components instead of providers here. And how about we import provider from our new package. And let's go ahead and add it here. Like this. And now what we can do is we can create widget atoms. So let's go ahead instead of the widget app. And I'm going to go inside of modules here inside of widget and I will create atoms and I'm going to go ahead and call this widget atoms.ds. In here, let's go ahead and let's import atom and let's go ahead and create the first one. So basic widget state atoms, export const screen atom will be atom and the default screen will be loading, for example. And now let's actually create a type and some constants to actually define all possible screens in our app. Inside of the widget module here, I'm going to go ahead and create types.ts. And I'm going to export const widget screens as an array. And these are all the possible options. Error, loading, selection, voice, auth, inbox, chat, and contact as constant. and let's also export const contact session key and in here I recommend that you add like a little prefix like echo contact session echo would basically be your app name something so it doesn't conflict with some other app right because this will be our local storage key we're not going to use it now, but I think it's cool to add it already here since we're defining. And my apologies, these aren't the types, these are constants. Like that. Now in the same widget module, let's create the types. And the only type we're going to have will be using the widget screens. so let's go ahead and import widget screens from modules UI my apologies widget constants export type widget screen will be a type of widget screens and then just an index to access it and when you hover over you can see all the possible options now let's go back inside of our widget atoms and let's go ahead and type it correctly like this and I like to use modules widget types and there we go so now if you try something else you will get an error right we can only do specific screens like alph so change it to alph actually because that is the current screen that we have, like this. Now let's go ahead inside of our widget view. So that is inside of modules, widget, UI, we have views here, so widget view, like that. And in here, what we're going to do is we're going to import useAtomValue from Iodai. Let me just remove this imports. Don't need them. So import this like that. And then let's go ahead and set const screen here to be use atom value. And inside of here, let's go ahead and let's add screen atom from atoms widget atoms. And I'm just going to replace this with modules widget UI screens. And this too. You don't have to do this. I just really like it this way. All right. So now that we have the screen here, let's create a constant screen components. And now for the error, let's go ahead and add error. Let's just say to do like this. And now let's go ahead and do the same for all other options. So just like this. error loading alf voice inbox selection chat and contact now the only screen that we actually have here is the alf screen so we can already use that so widget alf screen and it's a self-closing tag like this and then inside of here let's remove this and instead let's do screen components screened. Just like that. And now, since inside of our widget atoms, we set the alf to be default. If you actually run through both dev and head to localhost 3001 because that is where our widget component is, you will see alf here, right? But if you change this to loading, for example, you will see to-do loading, right? So that is our little internal routing system here. So let's get inside of the widget view now. And what I want to do now is I want to create the error screen. And I want actually, let me see what is the best course of action here. Well, actually, it doesn't really make sense to go anywhere right now, because we can't really develop any other screen before we finish the loading screen. The loading screen will ensure that we have organization ID, that we have verified the organization ID, and that we are then ready to redirect the user to either the out screen or to the selection screen. So in the next chapter, we're going to develop the loading state. And we're also going to implement the ability to save our contact session inside of local storage. for each organization ID. So that's why I want to separate those chapters simply because in this chapter, I just want to focus on adding our state management as we just did and adding the atom values here, screen atom. So let's go ahead and end the chapter here. So I'm going to add all of this changes here. So we did all of this. we added our state management, created the atoms, defined the screens and created the router. So 11 widget screen router. Let's go ahead and commit. Let's go ahead and create a new branch, 11 widget screen router and let's publish this branch. and let's go ahead and review our changes just to see if there is something seriously wrong with the app I always like to do a double check with CodeRabbit especially because it notices security issues before we do and that's something important especially with the anonymous part of this app which is the chat box where anyone can submit anything and this one was fairly simple So we introduced dynamic screen rendering in the widgets, allowing different views such as ALF, error, loading, etc. to be displayed based on the current state. We added new constants and types to support multiple widget screens and session management. We also added new dependency to enhance state management capabilities. The only comment that was left here was to add organization ID, but we're actually not going to do that because we're going to be using the atoms to store organization ID as well as some other things, of course. So we can go ahead and merge this pull request No other comments Perfect And after this pull request has been merged as always make sure to go back inside of your main branch and just synchronize the changes so that you have a graph that looks like this. We detached to implement the screen router and then we merged it back here. Amazing. That is the end of this chapter and see you in the next one. in this chapter we're going to implement widget loading so in order for our customer to use the widget we're going to have to confirm a couple of steps have verified these steps will be loading the initial organization id and then verifying that that organization is real after that we're going to have to load the contact session ID and verify if that contact session ID is active. So that's what we're going to have to do in this chapter. So we'll learn how we're going to initialize this widget to allow the user to even begin making any actions. In order to do this, we're going to have to implement the error screen in case something goes wrong, like a missing organization ID or an invalid organization ID, as well as the convex functions to validate the organization and to validate the contact session. So let's go ahead and start with a very simple add error screen task. So in order to do that, we go inside of our widget, we go inside of modules, widget atoms. Inside of the widget atoms, let's go ahead and just add a very simple error. Well, Actually, we don't need a comment for this. It's pretty self-explanatory error message atom. So in here, I'm going to go ahead and define this atom as a string or null. And by default, it's going to be null, just like this. So now let's go ahead inside of UI and inside of screens. and let's go ahead and let's create widget error screen dot t s x now in here what's important is to mark it as use client let's import use atom value from your day let's go ahead and import our alert triangle icon let's import our error message atom from modules widget atoms widget atoms which we just created. And let's import our widget header from modules widget UI components widget header. Let's export const widget error screen here. And the first thing we're going to do is we're going to grab the error message. And we can grab it very easily by combining the use atom value and error message atom. And now in here, let's go ahead and return empty fragment. Let's add the widget header like this and what we can do is we can just copy the one from the out screen for example so this just copy it add it here like that and then in the div here let's go ahead and give it the class name flex flex one flex column items center justify center gap y4 and padding 4. Let's add the alert triangle icon. Let's give it a class name text muted foreground. In fact, we can give it to the entire parent div here. And below the icon add a paragraph in which you're going to render the error message or invalid configuration by default. And let's give this a text small. So now let's go ahead inside of our widget view component where we initialize our screen components and let's replace the error with widget error screen. Just like that. Make sure you have added the import. and I'm just going to go ahead and copy this. Once you've done that, go inside of your widget atoms and change this to error. Go ahead and run through Bodev and make sure you are on localhost 3001 and you should see invalid configuration here. And if you go ahead and change this to, whoopsie, you will get that text written here. Perfect. Now let's bring this back to out to make sure it's still working. So just ensure that inside of your widget view, which you can locate inside of apps, widget modules, views, widget view, you have added a new widget out screen. My apologies, widget error screen. Excellent. So that was the first step. That was an easy win. And now what I want us to do is I want us to implement these convex functions. Organizations get one and contact sessions dot validate. So let's actually go ahead and let's do the contact sessions first. So I'm going to go inside of my packages, backend convex. And now in here, we're going to go inside of public contact sessions. And we're going to export const validate mutation. Let's add arguments. Contact session ID to be v.id contact sessions. Let's add a handler with context and arguments here. and let's go ahead and check if contact session exists by doing await context database get arguments contact session id so if there is no contact session in the first place we're going to go ahead and return an object with a valid false reason contact session not found now let's check if contact session expires at is smaller than i mean less than date dot now make sure to use contact session this variable that we have and return the same thing with a different message contact session expired. Otherwise, return valid true and the contact session. Just like that. Perfect. So that is our mutation to validate the contact using the contact session ID. Now what we have to do is we have to find a way to validate the organizations. So let's go ahead and do the following. Inside of public here, create organizations.ts. And in here, let's import v from convex values. And instead of mutation, we're going to have to import an action. So what is the difference between action and mutation? Well, I think it might be a good idea to try and do mutation first. You're going to see what happens. So let's export const validate here. Action, arguments, my apologies, I just told we're going to use mutation. So mutation, arguments, organization ID, string, handler, asynchronous function with context, and arguments. and inside of here we are going to validate our organization the only way we can do that is by pinging a clerk so what we're going to do is we're going to import create clerk client from clerk backend the only issue is we don't have clerk backend installed So let's go ahead and let's do pnpmf backend add clerk backend because that's where we have to add it into our backend package. And once you've added it, you will see a package JSON modified inside of your packages backend. So 2.6.1 is my version. And now let's go ahead and let's define the client. const clerk client will be create clerk client secret key will be process.environment clerk secret key. Now the only issue here is that we don't have the clerk secret key added here. So let's go inside of .environment.local and let's do clerk secret key here and we can actually find this secret key inside of our apps web. so inside of here environment local you should have clerk secret key so go ahead and copy it if for whatever reason you cannot find it you can go to your clerk dashboard into your echo project here and you can find your API keys here in the configuration there we go clerk secret key so basically just find your clerk secret key and paste it here. And also what I would suggest you do is go inside of your convex dashboard here, go inside of your echo tutorial settings, environment variables, add and add that here as well. Just like that. Perfect. Now we can go back inside of your organizations here. Now that you have the clerk client, and you can also do this here just in case, let's go ahead and open try and catch block here. Await clerk client dot organizations dot get organization. Organization ID will be arguments organization ID. Return valid true. Like that And in here return valid false reason organization not found like that And now, let's go ahead and let's do Turbo Dev, and let's focus on the backend to see if this will work or not. so I think the functions might actually be completely ready that's perfectly fine but this actually won't work until you try and fire it so let's test that out by going inside of convex here and go inside your functions and find public organizations validate click run function and inside of organization id go ahead and pass one two three and click run mutation uh oh looks like it does work maybe i'm doing something incorrectly here or maybe it works because we are using the convex dashboard uh i'm going to show you what i was trying to demonstrate here so convex has actions which are used to call third-party services such as stripe open ai things like that right so basically whenever you are doing some kind of fetch to a third-party service, you should be doing action instead of mutation because mutation doesn't have access to fetch. So what I think happened here might be convex error. So let me go ahead and at least try and log the error here so I can see if I'm correct or not. And maybe in the logs here, can I catch that error okay never mind how about we just add it to our project let's keep it as a mutation because I am interested now maybe I'm teaching you incorrectly so I want to make sure to improve on that we can replace this with an empty underscore to indicate that this will not be used all right so now that we have added the other two things that we wanted convex functions, let's go ahead and let's add the loading screen. Let's go instead of apps, widget, modules, UI, screens, and you can copy the error screen and paste it here and rename it to widget loading screen. Change this to widget loading screen. Just like that. Now, the widget loading screen will be a little bit different. the first thing we're going to have here is a prop organization id and it's going to be either a string or null and go ahead and pass the organization id here like that and now when you have the widget loading screen go back inside of your widget view component where you added the widget error screen and add it to the loading and pass the organization ID here. And technically, it's also kind of optional here, so that's important. If you want to, you can put loading in the first item here. It doesn't matter. The order doesn't matter. It's just an object, but just like mentally to help you, loading will be the first route that's actually loading here. So for now, let's go ahead and change this to be loading. Like that. And you can use the loader icon from Lucid React and you can give it class name, animate, spin, like that. And remove the alert triangle icon. And then go inside of your widget atoms and change the default screen to be loading. Make sure you have widget running on localhost 3001. And there we go. Loading is the first screen that appears. So now what we have to do is we have to do a series of steps that will help us ensure that the widget is ready and functional for the user to use it. So let's go back instead of the widget loading screen. And in here, let's create a type init step. Let's make it either storage or organization or session or settings or VAPI and finally done. And now let's go ahead and add some states here. So I'm going to remove, okay, this is what I'm going to do. I'm just going to switch the useAtom value to be useSetAtom from Yotai, like that. And this will then be setErrorMessage, like this. And then in here, I'm going to do step, setStep, useState, which is a type of initStep. And the first step will actually be organization. like that. So let me just remove this. We actually don't need storage. It's just going to be organization. And import useState from React. Like that. Let's also add sessionValid and set sessionValid here to be useState. And by default, let's go ahead and let's make it false. Now let's quickly go back instead of our widget atoms. and just as we've added the error message atom, let's do export const loading message atom, atom null by default, string null. Like that. And then we're going to go ahead and do loading message, use atom value, loading message atom, just like that. All right, so now let's just modify this slightly. So instead of this, it's going to be either loading message or loading like that. So now it should just be loading. Perfect. And now let's go ahead and let's use useEffect to try and validate the organization. so I'm going to open useEffect here from React make sure you import it and just above this I'm going to say validate organization and I'm going to do useMutation here from convexReact and I'm going to pass in the API from workspace backend generated API dot public dot organizations validate like that and what I'm going to do now is I'm going to check if this is the step it needs to be. So if step is not organization, we are not going to do anything in this function. So we immediately break it. And then what I'm going to do is I'm going to check if there is no organization ID present. So if what this was passed isn't present for whatever reason, set error message immediately to be organization id is required and set screen which I have to add so let's just do const set screen here use set atom screen atom make sure you have imported screen atom from modules widget atoms widget atoms and set screen to be error like that so this is so let me just check and here I have to add step and I have to add organization ID yeah linting is not perfect in this project so make sure you have added step and organization ID here and I think that technically you also have to add set error message and set screen because these are not usually you don't have to add setters but since this is from a third party package I'm not sure so right now let's go ahead and let's refresh you will see the error organization id is required if you haven't passed anything but if you try and add organization id and make it one two three you will just be stuck in the loading phase right perfect and what you can actually do is you can do set loading message you also need that here so set loading message loading message atom set loading message to be loading organization ID like that and then you can see for for the first step you have loading organization ID so if you accidentally don't pass it you get an error organization ID is required that's how we are going to enforce that or maybe you can do just loading organization first and then let's go ahead and do the following set loading message verifying organization like that let me add three dots here too and then what we're going to do here is basically the following we're going to use the validate organization here so validate organization and passing the organization ID like that and let's go ahead and do then result and let's check if result as valid question mark boolean question mark dot valid okay I think I did this incorrectly let me just check actually I think you should be able to just do if result.valid I think you can just do that set organization let's go ahead and let's do the following here so we also need to add the organization id atom so export const organization id atom Adam Strachan\nor null like this. So let's go ahead and let's add this setter, const set organization ID, use set atom, organization ID atom. So the reason we are setting the organization ID again is because the only way we can currently pass it around is through props. But if we just once set it in the atom, we can access it everywhere. That's why I want to do that here. So technically, you know, we could also do that here the moment we pass that. So set organization ID, and let's go ahead and let's make it... Okay, actually, yeah, we can do it inside of here when we know it's valid. So set organization ID, organization ID, like that. Perfect. and actually this can call an early return. If there is no organization ID, we set the screen to error and no point in going further and that way this function is safe. Perfect. So if result is valid, then we set the organization ID and we can also set the step here. Well, we'll do that in a second, but let's go ahead and first do else. Let's go ahead and do set error message. And let's go ahead and use the result.reason or invalid configuration. And set screen to be error. Like that. So there we go. You can now see something here is going on. And let me just go ahead and add one more thing. So I also want to add a catch method here just in case. So if it fails for some other reason, we're also going to do unable to verify organization and set screen to be error. Like that. Perfect. Okay, so now let's see what's going on by focusing on the back end here. Okay. So it says organization not found. Let me just see. inside of validate organization. I think that's actually all correct. I was convinced that I need to use the action for this, but looks like I don't. But I will show you how you can do it with action as well. Because this is true. Organization not found lets the organization not valid. And refresh. Verifying organization. It still says organization not found. Just a second. okay let's try again there we go so i guess this needed to restart perfect so organization not valid if you pass just a random one two three number but if i go inside of my organizations here and just copy the id of one of them in my clerk dashboard so i have a valid organization id Let's check that. Ah, see? Organization not valid. Well, I think that now we are encountering the problem that I was telling you about. I think that this needs to be an action. So let me just go ahead and check just in case. Console log arguments organization ID. okay let's wait for the this to deploy and then let's try okay let's go inside of logs here you can see that the organization id is fine so i think that this should be working just fine but it's not throwing errors let's try and replace this with action as intended and then we just have to modify instead of useMutation to useAction here from convexReact and remove useMutation. Let's see if this changes now. Let's first wait for all of the functions to deploy. Let's refresh. Okay, and yes, make sure you're using the proper ID. So always double check that. I have a bunch of localhost opens, so yeah, make sure you are using just a single one. And there we go. Now it's staying at verifying organization. So it looks like it needs to be an action. Let me try one more time. I'm changing this to mutation. And I'm changing use action to use mutation. I have the proper organization ID and you can see it says organization not valid, but that's not correct. This is a valid organization, but you need to use useAction. And inside of your organizations, you need to use action instead of mutation. That is because mutation environment doesn't have access to fetch. and clerk client in the background uses fetch to their API to get the organization. The only thing I'm kind of concerned about is that we're not getting any errors about that because usually I get alerted whenever I try to use fetch within mutation. So let me just try moving this outside like this. and let's always return valid true. Let's do const organization like that maybe. So if organization is present, valid true else return valid false reason organization not valid. will this maybe trigger it or not ok so it still says that everything is fine use mutation let me refresh now it says unable to verify organization and we have an error and it says now it's behaving properly ok and I think that we finally got the error that I wanted to get this entire time. Basically, this is the type of error that you would get. The problem was we encapsulated it within try catch, right? So that was the problem. But you can see that you get errors if you try to use fetch or any SDK which wraps fetch instead of mutation. So that's why we're using action. So my apologies for doing all this just to prove a point. But I want you to understand that. I don't want you to just blindly follow what I do. That's why I want you to learn convex. Whenever you are calling a third-party SDK, you need to use action instead of mutation, and you need to use use action on the front end. Let's refresh again. There we go. Now it works, right? No errors. Perfect. So make sure you're using action in the organizations.ds file and use action on the front end. Beautiful. And now that we have done that, if we succeed, let's go ahead and let's do set step session like that. And that's going to be another use effect here. So I'm kind of creating sequential use effects here. And also, we should add all of So we have step, we have organization ID, set error message, set screen. We should also do set organization ID. We should do set step. Basically, everything that we are using here. And let's also add validate organization here. Like that. i think the only thing that's missing is loading message set loading message add that too the reason you need to add this is because during build you will most likely get errors uh it will fail because it's missing it won't be able to lint that all right so that was step one right Let's go ahead and market this. Step one, validate organization. And now we're going into step two, validate session, if it exists, like that. So what we need to do here is we need to add validate contact session, and this will be use mutation from API.public contact sessions validate. So why is this use mutation? Well, because our validate contact session inside of packages backend public contact sessions doesn't use any third-party API. This just uses the convex database. That's it. That's why we can put it inside of mutation. That's why we do that. All right. Now, back inside of the widget loading screen here, the first thing we do is the protection. If this step is not about the session, So let's do an early return. There's nothing for us to do inside of here. And now what we have to do is we have to add the contact session ID atom. So let's go inside of the widget atoms here. And let's go ahead and let's add organization scoped contact session atom. And I'm going to explain what this is in a second. So const contact session ID is going to be the following. It actually can't be just this. It needs to be export const contact session ID Adam family. And then use Adam family, which you have to import from Yotai utils. so import Adam family from Yotai utils and also import Adam with storage. So we can immediately store it in the local storage. So Adam with family will accept an organization ID which is a type of string and it's required and it going to return Adam with storage like this and inside of here what we going to do is we going to use our constants which is the contact session key so let go ahead and open our back text here so contact session key like that and then go ahead and add another underscore here organization ID. Like that. And then let's go ahead and add a third argument. Is this third argument? Let me just check. Here. There we go. Like that. Add null. So what just happened here? What is this? Well, here's the problem. You can just use Atom with storage like a normal constant contact session ID, atom with storage, some key. No, you can do that, right? But the problem is we need to store different keys for different organization IDs. We can't do an object and then org ID inside and then the session because local storage values have limits. We are better off using index DB for that. But we can still simply create an Atom family to derive Atom with storage and a specific organization ID. So now our local storage will be full of the following examples. We're going to have echo contact session and then organization ID. And then that's going to hold your user ID inside. That's how it's going to look like. That's why we created the contact session ID Atom family. and now we can go back instead of the widget loading screen, and here at the top, we can extract that atom, so I'm going to do const contact session ID that we need is use atom value, contact session ID atom family, and pass in the organization ID, and organizationD is available from here like that and this is useAtomValue like this and let me just see so yeah this is a little bit tricky because we are trying to protect ourselves against organizationD not being null so we can use like this little trick I guess because even if this happens if it happens that this is empty we are simply going to show the error screen. The problem is we need to initialize the hook above that step, right? So let's just leave it like this for now. And basically now we have the contact session ID extracted from that specific local storage value with combination of the organization ID. So each organization has its own key in the local storage. And now what we're going to do inside of the step two, which is to validate the contact session, we're first going to check if we don't have the contact session ID, which is perfectly fine. We're just going to set the is session valid to false. This is our internal set state here. Because that's completely fine. The user doesn't have to be authenticated, right? The session can expire. And inside of here, you would return early and simply set step to be done because we are finished in this case. There's nothing more for us to do. And now let's do set loading message here, validating session. And we can actually do, let's be consistent here. So this will be finding organization ID. Let's now do the same thing here. finding contact session ID because that's what we're doing in this step. You can remove this or loading messages later, but I just want us to know what we're doing at each step. Now we're validating it. So let's go ahead and do validate contact session, contact session ID, contact session ID, like that. Then result. and let's go ahead and do set session valid will be result dot valid. And I think that there is a type error here that we are not seeing. Contact session ID requires ID contact sessions, but this looks like it's a type of never. So let's just go ahead and give it a type of as ID contact sessions. and import ID from workspace backend generated data model. So the reason I'm fixing this is because we're going to get errors in build. That's why we need to take care of these things before they happen. And now let's go ahead and let's do set step here to be done because we're done at this point. and let's add a catch. Just in case this fails for some unknown reason, let's do set session valid to false and set step here to be settings. Like that. Perfect. And now let's go ahead and let's add all the dependency arrays that we need. So step contact session ID, validate contact session and set loading message. Like that. Perfect. so let's see what's going on now so you can see now it's finding contact session id and it can never find the contact session id so yours should be stuck in finding contact session id as well because after that what we do is we do an early return we couldn't find the contact session id so in your application here if you go and inside of local storage now you can see that I have a key ignore that that's because of my testing of this app right but yours is empty so it cannot find the contact session id so after it determines that it cannot find the contact session id you have to define what to do and my apologies not settings done in here in the cache method done and now let's go ahead and let's do use effect here another one if step is not done early return otherwise let's check if we have a valid session by checking if we have the contact session ID and if the session is valid. And then set screen dynamically. So if has valid session is going to be selection, otherwise it's going to be out. And let's go ahead and let's add all the necessary dependency array items. Step contact ID, session valid and set screen because that's all that we are using here and if you refresh now you should be redirected to the out screen right so every single time that you go on to localhost 3001 with valid organization id in the url without anything in your local storage you will be redirected here but if you accidentally forget your organization id it won't even let you create an account because we have no idea why you came here. Or if you accidentally enter the wrong organization, you will be unable to verify the organization. Perfect. So only if you add the correct organization ID are you able to proceed further. So now let's go ahead and do the following. Let's go inside of widget out screen right here. And now instead of the widget out screen, let's go ahead and let's change let's save the contact session id so this is what i'm going to do now i'm going to add const set contact session id to be use set adam from yotai make sure you import it contact session ID atom family and pass in the organization ID which we currently don't have so let's obtain it organization ID use atom value organization ID atom at this point we should already have the organization ID because of our loading screen right because we will show an error but we are still going to have to because of types pass in organization ID or empty string but only because of types. And now that we have that, let's go ahead and go inside of set contact session ID and let's simply set it here. Contact session ID. Let me just say, so this is, okay, it has a specific type. So let me just see how can we improve this. instead of public contact sessions create this is a type of ID contact sessions and we return that back await create contact session but in here it's described as this okay so can I do as string here or as ID contact sessions let me just check okay why is this throwing a type error so the first thing i'm actually going to do is i'm going to go back inside of my widget atoms and how about we give this atom family uh and atom with storage here a proper type so you can do that uh by going here atom with storage before the parenthesis and open pointy brackets and give it a type of ID open pointy brackets again contact sessions or null like that. So let me try and I'm going to try and make this function more readable for you because it's very hard to understand what's going on here. Is this any easier Like this if this is any easier for you I don know Basically the AtomFamily function needs to return AtomWithStorage And that AtomWithStorage can be a type of ID from workspace backend generated data model of contact sessions, or it can be empty, stored under this key, and with the default value of null. so I think that technically if you add like one two three this should be an error because it's not assignable to type contact sessions so that's how you know if you did it correctly and now go back instead of the widget out screen and you can see that now this doesn't give us any errors but if I tried one two three this gives us an error because it's not a type of id contact sessions but this type and this type seem to be compatible even though this one looks different but I guess that is the exact same thing I don't know if you're still having problems you can add as id contact sessions but for me it works this way so now let's try this let's refresh let's try test and test example.com and let's click continue and there we go you can see now that we have echo contact session underscore and then our organization id and then our value so if i try refreshing now there we go i'm redirected to to do selection perfect and let me just double check something inside of my widget loading screen here. So I'm just not sure what is the type of my contact session ID here. It's perfect. It is exactly what I want it to be. So down here in the validating session, it's the correct type. And I don't think I no longer need this now. There we go. I can even use the shorthand operator like this and maybe even make it like that. Perfect. So now I can remove the ID import. That is exactly what I wanted us to do. Perfect. So now, since you have a valid organization ID token stored, you will always be redirected to selection, but only if this contact is valid. Because remember, this contact can become expired if 24 hours pass. Because inside of the context session create, we expire it 24 hours from now. So we're later going to create additional methods which will refresh that token. But if you want to, what you can do is you can try just adding maybe 5,000. That's five seconds. So let's remove this token from here. Let's refresh. And let's do five seconds. Five seconds at example.com. Let's click continue. Let's quickly refresh. Now it works. Let's wait, let's wait, let's wait. And there we go. You can see that even though I have the session ID now, five seconds has passed. So I have to log in again. That is how our out system works. So make sure to bring this back to 24 hours. But you can see that it works. So even though we have a completely valid session ID inside of our local host, since it's expired, we are no longer treating it seriously, right? And even now, after you change this to 24 hours, that's still an expired session. We no longer need it. So you're going to have to create a new one. Oops, I reversed the two, didn't I? and now this was updated, and let's refresh. There we go, and now we are redirected to selection because we have a valid account. So that is exactly what I wanted us to do. Amazing, amazing job. So now we have a working loading screen, which is used to find the organization ID, verify the organization using clerk, find the contact session ID, which is dynamically generated using Adam family, and the correct organization ID, and then verify that session. So we achieved exactly what we planned out to do in what I believe was 10 widget session. This is exactly what we're doing now, right? So we are validating the session using that stored contact session ID in the local storage, except the contact session ID is stored along with the organization key. So we know exactly for what organization it was made. and we are also checking expiry here amazing amazing job i believe that marks the end of this chapter uh we of course have to review our changes because we did a lot of them but i am super happy with how this turned out it was a complex one but i think you learned a lot uh and especially of how we practically implemented our own out here i think that's always interesting it's obviously not super super complex but it's a good lesson in making these types of things right because this is supposed to be very very low friction so 12 widget loading let's add all changes 12 widget loading let's go ahead and let's commit let me open a new branch 12 widget loading let's publish the branch. And then let's go ahead and let's review our changes. And here we have this summary by CodeRabbit. We introduced comprehensive loading and error screens with dynamic messages for improved user feedback during widget initialization. We added validation for organization and contact session IDs, ensuring only valid sessions proceed exactly. Widget now reacts to organization changes and manages session state more robustly. Exactly what we did. And we also added back in support for organization and contact session validation as well as dependency to include clerk. So in here, of course, file by file change summary. But what I think is more interesting is the diagram here. Let's take a look at it. So the user loads the widget with specific organization ID. The widget loading screen is the first screen that appears because our atom, screen atom, is set to loading. That means that the first use effect that fires is a validate organization ID. The convex then runs the get organization, clerk. Let me just see. I'm not sure what's behind this. and basically clerk responds back with either organization found or not found and depending on that we return back valid false false or true so if the organization is invalid we set the error message and set the screen to error because you cannot proceed further you are either missing the organization id or the organization id belongs to some organization we don't have or it's completely wrong. But if the organization is valid, we go into step two. We extract the contact session ID from the local storage and we validate the contact session ID. We do the same logic as here, just using our database and return true or false. And if the session is valid, in here I think it kind of understood incorrectly. We never actually set anything to error. We don't even set the error message. Even if this call fails, we're going to set the screen to out, right? So if the session is valid, we set the screen to selection, right? So I think it got confused here, probably because we're using inside of here, validated contact session, we're using catch so maybe it thinks that this set session valid false somehow determines the error but yes that's not what we do so you can see that when we validate the contact session if result is valid we simply set the session valid to true and we set the step to done and then in here if we have a valid session we do selection otherwise we do out so this is where he got out from and they probably generated some incorrect logic but we know that's not what happens but still super impressive how it managed to explain this to us and we do have some comments and I knew it was going to comment this yes I hate how we handle organization IDs right now I hate how we have to do this or empty because technically we know organization ID is guaranteed at that point. So I'm going to look into how I can help that. Something like this, an early return could help, but you can't do it exactly like this because use set atom is a hook and you can't do conditional hook rendering. So this will not work. This would actually throw an error, but I'm not sure CodeRabbit knows that this is a hook. In here it recommends checking for this. Well, okay, yeah, sure and in here it recommends validating the organization id well we are pinging clerks api not ours so it's fine and in here uh 2.6.1 is probably like the most up-to-date version so his registry is probably not refreshed yet but 2.6.1 is perfectly valid version Great. So yeah, I really dislike this. I fully agree. I don't like this organization ID hack that we're doing. So let's just go ahead and merge this for now. We've done enough for one chapter. Amazing, amazing job. And now let's go back inside of our main and let's click the Synchronize Changes button here. And after they have synchronized, let's go inside of graph to confirm widget loading and then we merged it amazing amazing job that wraps up this chapter and see you in the next one in this chapter we going to go ahead and implement conversations schema conversation functions and the selection screen that will help us test the new functions which we created Let's start by adding the conversation schema inside of packages, backend, convex, schema, dot ts. And in here, let's go ahead and let's add conversations. And let's go ahead and add a simple define table inside. The first property that will be required is the thread ID. Now this thread ID will be used to track the convex agent generated chat that will be happening. You won't be able to fully understand what this is used for now but later when we actually add the AI chatbot which will be very soon you will see why this thread ID is required. It will basically be a reference to a conversation that the AI is having. So this way we are able to have both human chats and AI chats in one and so that we are able to fetch them as needed. Now let's go ahead and let's add organization ID which will be a type of string and then let's go ahead and let's add contact session ID, which will be a type of VID contact sessions, basically a relation to our contact sessions table. And then let's go ahead and create a status field, which is a union, which can be unresolved, escalated or resolved. And now let's go ahead and let's add the indexes, which we are going to need. The first index is by organization ID using the organization ID field. the second one is by contact session id using the contact session id field the third one is by thread id using the thread id field and the last one is a composite index of status and organization id using the status and organization id after you have added this schema just make sure you haven't accidentally misspelled organizations or contact sessions here and then let's go ahead and run TurboDev in the root of our app and focus on the back-end task to ensure that the schema is validated, indexes are added and convex functions are ready. After you've done that go inside of convex public and go ahead and create conversations.ds. Now inside of here let's go ahead and let's import mutation from generated server. And let's go ahead and import v from convex values. Now let's go ahead and let's create a mutation here. And let's prepare our handler. And now inside of the arguments, we're going to accept two things. The first one is going to be organization ID in which we want to create this conversation. And the second one will be our contact session ID, which we store in our local storage. Now in here, let's get the context and the arguments for the handler function. And the first thing we're going to do is we're going to fetch the session using context.database get and simply pass in arguments contact session ID. So thanks to this unique way of IDs per table, Condex immediately knows that this will be a type of session. So no, you don't even have to define the table. And now we have to verify if this session is correct. So if there is no session available, that's the first red flag. But even if the session exists, we still need to make sure it didn't expire. So let's make sure that it didn't surpass today's date. And now let's throw a new convex error here, which I'm going to give a code of unauthorized and a message of invalid session. And we can import convex error from convex values. If you're interested to learn more about the convex error, you can go inside of their documentation, functions, error handling, application errors. And in here, you can see how convex error can be used. So you can just pass a normal string if you want to, but you can also pass in the payload data like this with message, code, or any other properties you need. And then you can read that on the front end, and then you can display some different UI states. So now that we have this validated, let's go ahead and let's actually create the conversation ID. So const conversation ID will be await context database insert into conversations, contact session ID, which will be our fetched session underscore ID. So we use the document we just successfully fetched and status will be unresolved by default. Organization ID will be the one from the organization here. And let's go ahead and do const thread ID one, two, three. and I will add a little to-do, replace once functionality for thread creation is present. And let me just fix this, there we go. And then pass in the thread ID here and that should all be good. And return the conversation ID here. Just like that, perfect. Now just make sure that your convex functions are ready so everything is working. and let's go ahead and set up widget dev. Let's head to localhost 3001. By default, you're going to get organization ID is required, so simply head to your clerk dashboard, go inside of organizations, and find a working organization ID. So you can go ahead and add question mark organization ID and that working organization ID, and after that, if you have a correct session, you will be redirected to selection screen. If you don't have correct session, you are going to have to enter your email and username. Now let's go ahead and let's develop the selection screen. So inside of packages here, my apologies, inside of apps, widget, modules, widget, UI, screens, I'm going to copy the error screen and I'm going to rename this widget selection screen. I'm going to rename it here, widget selection screen, and I'm going to remove the error message. And I'm just going to say selection screen. And we can remove the alert triangle icon. Now let's go ahead inside of our widget view component, which is located inside of views, widget view, find the selection factory key and add widget selection screen. Let's go ahead and just make sure to import this. You don't have to do this, but I'm just going to go ahead and quickly replace this with modules. I like my imports that way. And now you will see selection screen after you successfully validate and verify your organization. Now let's go ahead and make our selection screen look like this. So we are primarily going to develop the start chat one because that is the only one which will actually have the functionality. functionality. So let's go inside of widget selection screen and let's focus on the body right here. So I'm going to go ahead, remove the inside of this div. And I'm also going to remove the text muted foreground. So the only class name that we need are flex, flex one, flex call, items, item center and justify center are actually not needed. So we need gap y4, padding 4 and let's add overflow y auto. And inside of here let's add a button from our workspace UI components button. And let's go ahead and add a div here. Class name flex item center gap x2. Render the message square icon. Give it a class name size 4. And they span start chat. Outside of that, they'll render a chevron right icon. So both of these are imported from Lucid React. Now let's go ahead and modify this button by giving it a class name, height 16, full width, and justify between. Let's go ahead and give this a variant of outline. and on click for now, just an empty arrow function. And just like that, you have the start chat button and change this to message square text icon from Lucid React. So you can remove the square icon. I think it looks just a little bit better. There we go. That is our button. Now let's go ahead and let's implement the handle new conversation click here. So what I'm going to do first is I'm going to go ahead and do const set screen and I'm going to do use set atom from Yotai and use the screen atom. Then I'm going to go ahead and grab my organization ID from use set atom, organization ID atom. and then I'm going to get my contact session ID from use Adam, oops, this is use Adam value to, so use Adam value from Yotei. This below is use Adam value as well and this one will use contact session ID Adam family and pass in the organization ID or an empty string. like that. And now let's go ahead and let's create our mutation. So create conversation will be use mutation from convex react and passing the API from workspace backend generated API. API public conversations create just like that. Then let's do const handle new chat or new conversation. Let's make this an asynchronous method. First things first, let's check. If we don't have contact session ID, we're going to set the screen back to ALT. If we don't have organization ID,\nID we're going to set the screen to error and we're going to set error message which we also have to modify here set error set error message you set Adam error message Adam missing organization ID so the user knows what happened here and let's do early returns for both of those cases even though at this point you know definitely we should have both of them and in fact this should be the order right but since we have that loading screen this technically should never happen but it will help with our type safety and now let's go ahead and let's open try and catch here in the try let's go ahead and do const conversation ID and let's make it await create conversation and pass in the contact session ID and the organization ID. As simple as that. And then let's do set screen and let's change it to chat. And now inside of catch here, what we're going to do, well I think we can just confidently do set screen and just navigate to out I mean you could look at the error let me just see the type of error will be unknown so yes the first thing you're going to have to do here if you want to check it on the client is you should check if it's instance of convex error like that but I think we can just confidently return to out because if anything goes wrong here it's most likely out related right great so let's go ahead and check it out so now I'm going to go ahead and add the handle new conversation to be the on click here just like that and let's also just make a simple is creating conversation here so const is pending conversation let's just do spending and set is pending use state false. Make sure you have imported use state from React. So when we click here, set is pending will be true. Actually, let's move it to this part like that. And in the finally set is pending will be false. and then disable this button if it's pending. Just like that. So now when you click on this, there we go. I was immediately redirected to chat. And if we go inside of our dashboard in Convex, instead of Echo Tutorial here, instead of our data, Conversations, we should see one conversation with thread 123, status unresolved, and organization ID, contact session ID, which should be a direct reference to our contact session that I'm logged in with and all of my metadata. Amazing. That is exactly what we wanted to do. So now let's go ahead and use this time to also implement one more conversation function. And so we also add the conversation ID atom into our page. so what I want to do now is I want to go inside of my widget atoms right here and now let's go ahead and let's add the conversation id atom so I'm going to go ahead yeah I kind of weirdly structured this because none of these are really different from one another we can just keep them all together and just add them as we go along so const conversation ID atom will be atom, which is a type of string or null, and null by default. And actually, it can be a direct type of conversations, right? Because that's what it's actually going to be. So now add the conversation ID atom here, go back inside of the widget selection screen. And let's just separate the setters here const set conversation ID, use set atom conversation ID atom. And then we have the set conversation ID here before we send the user to the chat. Because if we send the user to the chat, it will not be able to load any conversation ID. Perfect. So now that we have both in store, let's go ahead inside of our packages, backend, convex, and public conversations here. and let's go ahead and export const get one to be a query and the arguments are actually going to be exactly the same so let's go ahead and just add them here let's add the handler and let's destructure I mean just obtain the context and the arguments in here and just as in the first example the first thing we're going to do is validate the session So let's just go ahead and make sure the session is valid or throw an error if it isn't valid. And, you know, the more we add this, the more are we going to have the need to kind of, you know, make this abstracted. So we can just call some function to check this for us. But for now, I'm just going to repeat myself. But yeah, there is a way to make this obviously better instead of just typing all over again. so now what we have to do is very simply check if the conversation exists not by using yes we don't need organization id for this we just need conversation id which is a type of id conversations just like that so my apologies it's not the same argument the contact session id is the same argument but we are actually looking for the conversation id There we go. So if there is no conversation ID, my apologies, no conversation, just return null. We were not able to find anything. And now here's the thing. You might think, okay, and then just return conversation, right? Or just why did I even specify this, right? It's going to be null. I can just return it like that. Well, keep in mind, this is public API. We need to be careful about what we are returning here, right? So the conversation, as you can see, has information about the contact session ID. So it wouldn't be the smartest idea to send that back to practically whoever figures out they can call the get one endpoint. So instead of that, we're just going to return the minimal useful information here. conversation underscore ID, status, conversation dot status, and thread ID, conversation dot thread ID, which we are still not using for anything, but this will basically be the key to load messages, right? Perfect. So we now have the get one method. And now let's go ahead and let's develop the chat screen. So I'm again going to go inside of the widget, inside of my modules, UI, screens, and I'm going to copy the error screen. And I'm going to rename this to widget chat screen. Widget chat screen, you can call it conversation screen, conversation ID screen, whatever you prefer, really. Let's just remove this. Let's remove item center and justify center, we're definitely not going to need that and text muted foreground as well and let's just say chat screen and we can remove all of these here now let's go inside the widget view and let's find chat widget chat screen like this perfect now that we have this let's go ahead and let's try our selection again. So just refresh your widget and you will go through the same flow. So verifying organization, verifying session and click start chat. And there we go. You are now redirected to the chat screen, which now looks identical, right? So let's go ahead and slightly modify the chat screen now. So the first thing the chat screen is going to have differently is the widget header here. So let's go ahead and give it a class name flex items center and justify between. And then in here, let's go ahead and do flex items center and gap X of two. And then inside of that div, let's add a button, which we can import from workspace UI components button, give it a size of icon. and let's go ahead and render arrow left icon from lucid react and let's give it hover actually no we don't have to do anything i think it's okay just like this so now you have this back button but one thing that i don't like is how it looks and there actually isn't any variant that we can use because all variants look kind of bad here even when you use ghost and it looks kind of weird so let's go inside of button you can go by command click or inside of packages ui source components and find button here and let's go ahead inside of the variants here and let's add transparent so let me show you where this is inside of button variants cva find the variants variant and just below the link and now we're going to create our own variant BG transparent text primary foreground hover BG transparent hover text primary foreground with 80% opacity just make sure you didn't misspell any of these and then give this a variant of transparent there we go that's subtle and i like it perfect uh and then outside of this div we're just going to have a paragraph my apologies inside of this div we will have a paragraph which just says chat like that and if you're wondering okay what will be here uh well for now what we can do is just a button and we just going to add a menu icon from lucid react and give this a size of icon and a variant of transparent so just this for now nothing more and now let go ahead and let's find a way to actually query the chat right so what we can do is we can get our conversation id by using useAtomValue and conversationIdAtom. And then we can go ahead and actually fetch the conversation. So const conversation is going to be useQuery from convexReact, API from workspace backend generated API, .public, .conversations, and get1. And now inside of here, this is how we have to do this. Okay, so let's just open use query. Now here's a little bit of a problem. Even though we know that we're going to have conversation ID at this point, we have to check if we have it. So if we have conversation ID, in that case, let's go ahead and let's pass in the conversation ID. And let's do the contact session ID, which we also need. But using, let's see, just contact session ID, use atom value contact session id atom value and we need the organization id atom so let's just make sure we have those three and then let's grab the organization id or empty string like this, there we go, now we have everything we need and the alternative will be skip like this and let's just ensure that all of these are correct. So I'm just going to go ahead and check if we have conversation ID and if we have contact session ID. And let me just add this. There we go. Like that. That is how you create an optional conditional query. Basically, if it's skipped, it's not going to run. So only if we have conversation ID and contact session ID are we able to even pass these arguments, so that's why we are doing this. All right, great. And now, let's simply go ahead and let's just JSON stringify the conversation. And there we go. You now have the fetched conversation. So if you go back, oh yeah, I didn't implement the back function. Let me just implement that. So for that, we're going to need a setter. So const set screen, use set value. How do you... I keep forgetting how to use just a second. So it is use set atom, screen atom. And let's also do set conversation ID use set Adam conversation id Adam. So now that we have these two setters, let's go ahead and let's implement on back functions. So const on back set conversation id will be null and set screen will be selection. so kind of like a reset and then just go ahead and give this on click on back and when you click this there we go perfect obviously if you refresh from here you will be redirected back to this page but that is basically what i wanted us to achieve i want us to load our newly created conversation on its individual page and i want the option to created from here. Perfect. So now the more you click, the more data you're going to have here. But in the next chapter, we're going to finally make use of that thread ID so that it starts to make sense, right? Because this will be used to load our messages, but you don't even know how do we get the message schema now? Well, that's the cool thing about Convex and their agent module or agent component is that they will handle all of that for us. So it's truly a magical component and it will both allow human conversation and AI conversation. Perfect. So I think that marks everything we wanted to do here. But we are not entirely done with our API, of course. You've probably noticed that in our conversations. So for example, every time we fetch get one, what we could do is we could refresh the contact session because it's active, right? Every time we do create, we should also refetch our session because it's active. That way, if the user is active, they don't have to worry about suddenly expiring. Those are the things we have to watch out of and that's where we're going to explore our internal functions and internal queries and how we can do that. Great, so let's go ahead and merge all of these. So this is 13 conversations. I'm going to stage all changes, 13 conversations, commit. I'm going to go ahead and open a new branch. 13 conversations. And I'm going to publish the branch. And now let's go ahead and let's open a pull request. And let's see if we have any big issues with our code. And to end the chapter, here we have the summary by CodeRabbit. We introduced chat functionality within the widget, including a chat screen and selection screen for starting new conversations. We added back-end support for creating and retrieving conversations with session validation and error handling. We implemented a new database table for storing conversation details including status and indexing for efficient queries. So in here we have the sequence diagram which briefly explains what we are doing. The user clicks start chat on the widget selection screen. We call the create mutation with organization and contact session ID, and the backend convex returns the conversation ID. After that, we are using our state management to set the conversation ID atom, and we switch to the chat screen. The widget chat screen then uses the get one query, passing the conversation ID and the contact session, and returns the conversation summary to the UI. In here, as in the previous chapter, we are still getting this recommendation to find a better way to use this organization ID. And I do agree, but I will just continue like this for now because we are handling the lack of contact session ID and organization ID accordingly is okay. It's just not very convenient. The loading state will be handled, but not like this. The loading state will be handled using a special use messages hook that we're going to add later on. In here, it recommends, yes, in here, it found a very good issue. So the contact session ID actually makes no sense here, right? The only thing we're doing is, did it expire or not? But we didn't even check if that conversation belongs to that contact session ID. So yeah, CodeRabbit just saved us from a very serious security issue here. So that's something that we're definitely going to implement. I'm going to do that the first point in the next chapter for us to do. Great and in here it also recommends verifying the organization so the only thing I'm not doing that here is because remember we need to fetch a third-party API to do that clerk's third-party API so just because of that I'm not doing it and in here it recommends a better placeholder more random one but it's okay the way it is. So one very serious security issue right here and we're going to do that in the next chapter to make sure our app has no such security issues. So let's merge this for now and then let's go ahead and change to our main branch. Let's go ahead and let's synchronize the changes to make sure everything is up to date here. So you can use that button, you can use this button right here as many times as you want and just confirm that your graph shows 13 and then that we merged it. That marks the end of this chapter and see you in the next one. In this chapter we're going to go ahead and implement AI agents functionality into our project. This is our goal. We're going to create a nice interface to render both AI and user messages and we're going to create a backend system capable of handling AI responses. And yes, you will be able to use any AI provider. Gemini, OpenAI, Anthropic, DeepSeek, every single one of them are handled by AI SDK. Let's start by fixing the security issue from the previous chapter. If you remember, going inside of packages, backend, convex, public, conversations, we implemented the get1 query, But there is a problem here. We do validate the session, but we never actually check if this conversation has anything to do with our contact session. So let's actually do another check if conversation.contactSessionId is different than the contact session ID from the arguments here, or you can use the session ID to use the actual fetched record, go ahead and throw a new convex error here. And we can just go ahead and copy this inside. And let's go ahead and just say incorrect session in this case, right? And we can actually throw errors for the case of conversation missing too. So this will be not found. And we're simply going to say conversation not found. And then we can catch this in an error boundary later. Like this. There we go. So we just fixed that security issue. Now we are at least checking that that contact session has anything to do with the conversation we are fetching. Perfect. Now let's go ahead and let's add convex agent component. So if you go inside of convex documentation, Let me just go here. And in here, you can find their guides, agents. And in here you can find everything you need to know about their agents components And they are absolutely amazing And you going to see why Because remember we still haven even added any messages schema And I'm going to tell you a little secret. It's because Convex is dedicated to making amazing developer experience on building AI apps using Convex. And that's why you feel like half of your app is actually missing when in fact, this is the other half of our AI logic because they have developed it so well. Let's go ahead and let's add it to our project. So we're going to add convex dev agent to our backend app. So let's go ahead and use pnpm filter backend add and let's add convex-dev forward slash agent. There we go. once that was added we have to create a convex.config.ts so let's go inside of packages backend instead of convex and let's do convex.config.ts and in here let's go ahead and let's import define app from convex server and let's import agent from convex dev agent convex.config. Then let's go ahead and define the app and let's add app.use agent and let's go ahead and let's add export default app here. Save this file and go ahead and run TurboDev. Let's see if everything is working or if we need to fix some things here. And there we go. So it says install component agent and convex functions are ready. And I'm just interested in one little thing. When I initially developed this project I actually got an error when I added a convex agent because I was missing convex helpers. So just in case anyone here is getting errors here's how I fixed it. pnpm f backend add convex helpers. As simple as that. So just add convex helpers to your backend and then do TurboDev again and if you got any errors they should no longer exist now. But I think they have obviously updated that and fixed it so it works even without them. Great. So just make sure that inside of your packages backend, you now have convex dev agent and convex helpers because we will be using helpers either way. So please do add them regardless. Great. Now that we have added that, let's go ahead and let's create our first agent. So I'm going to go ahead and I'm going to go inside of my packages, backend convex, and I'm going to create a new folder called system. And under system, I'm going to semantically separate all the things that are neither public nor private. They are internal, should I say, or maybe shared, right? So inside of system, I'm going to create another folder called AI just to separate things that are AI related. And inside, let's create our support agent.ts. And just a quick tip, whenever you're adding files inside of convex, you have to use, you cannot use dashes, right? So because of how they parse their functions and everything, you need to make sure it's one word, right? So don't add like a dash between. Just a small tip. And now as you can see in here, they say that we have to add AI SDK OpenAI. So let's talk about AI SDK and what if you don't want to use OpenAI. You can visit the link on the screen or simply google ai sdk to head to this website so this is the ai toolkit for typescript and first things first we're going to have to install their ai package so let's do that first i'm going to go ahead hey there antonio from the future here i am editing this video and I realized that in the middle of our tutorial, AISDK version 5 has officially came out and became the default version. So what does that mean for you? If you want to, you can use version 5. But please keep in mind that that will require you to do some migrations. You will have to migrate from version 4 to version 5. So I cannot guarantee that you will be able to follow this tutorial exactly the same. For example, if I use a field maximum tokens, you're going to have to use the field maximum output tokens. If I import a type core message, you're going to have to import a type model message. And so on and so on. This isn't too big of a problem. and you can absolutely use version 5. It shouldn't break too many things. But I would highly suggest that instead of doing that right away, you follow the exact same versions that I was using to make this tutorial. It will make things a lot easier for you and you can always upgrade later once you finish the tutorial as a personal challenge, as an additional task in this project. So, how do you know if you installed version 5 packages or version 4 packages? There are a couple of important packages that you need to have in your project. I'm now going to open my package JSON at the time of me making this video. As you can see, my convex dev agent is 0.1.16. My AI SDK OpenAI is 1.3.23. and my AI is 4.3.19. So these are the versions needed if you want to use AI SDK version 4. And here's how you can find out those versions yourself. This is especially important if you are using Gemini, for example. So I have prepared for you all of these NPM packages here. You can see that three days ago, all of them were published on version 5. AI is now on version 5. AI SDK is now on version 2.0. AI SDK Google, which is Gemini, is also on version 2.0. I'm assuming most of you will either use OpenAI or Google if you're using Gemini. And as per their migration process here, you can see exactly what you have to modify. So you can see that if you want to use version 4, you have to use version lower than 2.0 of any AI SDK and then specific provider packages. And ZOD also has to be a specific package number, which is also why I didn't want to upgrade right now simply because we already had some problems with ZOD, right? So I don't want to accidentally break that again. So here's what I want you to do. When you go to AI NPM package here, you can click onto the versions here. You can see that all the latest one here are 5.0, But if you scroll a little bit down, you will be able to find the last version 4, which has almost half a million installs. So that's how you can recognize it. So once you have identified that version, that is the version of your AI package that you have to use for this tutorial. It is the exact same version that I have here. Now, what if you're using OpenAI? Same thing. Click in the versions here and scroll down until you find the last version that is lower than 2.0. In my case, that is 1.3.23. And same here, you can see 300,000 installs. And it's the exact version that I'm using, 1.3.23. What if you're using Google? Gemini. Same thing. AI SDK Google. Go inside of versions here. You can see that 2.0 is the current version. Scroll down until you find the last version that's not 2.0. In Google case, that is 1.2.22. So what does that mean for you? When you follow my instructions in this tutorial, instead of installing backend add AI, you would specify the version. So AI at, and then you would find your version here. 4.3.19 and you would install that. If you are installing OpenAI you don't just paste it like that. Instead you go ahead and find the latest version that it's not 2.0 like this and if you're using Google you install the latest version that is not 2.0 as well. So you replace this with Google and you add the appropriate version. It would be 1.2.22. What happens if you already added a package and now you have a higher version? No problem at all. So far in this tutorial, all of these packages, dependencies, I believe, are only in one package JSON, packages backend. I think that is true. If I search them, all of them, okay, looks like they are also in the widget, so that's important okay so what I would suggest you do now is you search through your code to find all of these packages and if you already have them installed and you can see that they are on version 5 and that this is on version 2 for example simply find them in your dependencies and change them to this version change convex dev agent to this version change ai to this version and make sure it's everywhere like that. And after you have done that, in the root of your app, simply run pnpm install. That is how you can ensure that you are using AI version 4 and that you have the proper provider version, which must be under 2.0. And then later, when you finish this tutorial, you can simply follow this migration guide because there are some breaking changes, as you can see, And I want you to have a nice, easy time following this tutorial. I don't want you to have to think about those breaking changes right now. But if you find yourself in a situation where you just have to use version 5, nothing is working for you, then you're going to have to follow this migration process. Luckily, it's not too difficult. If anything, it's better. But it's just different. So you going to have to carefully watch whenever I doing something and compare it with this change here to see okay I have to modify this I have to modify that But again you can just use these exact versions I'm not sure that the convex dev agent version is actually that important, but AI version is definitely important and your provider version, which needs to be below 2.0. So I hope I cleared that up. simply head to the npm package of the provider you're using if that is grok then you will search for grok let me show you the process right so ai sdk grok xai right for example if you're using this for whatever reason the same thing you go inside of versions you scroll down until you find the last version that is not 2.0. I assume most of you will be using OpenAI and Google which is Gemini. That's why I showed you the examples on those two. I hope that cleared it up and that you will be able to continue with the tutorial. And I'm going to go inside of PNPMF backend add and let's just do add AI. So that is basically this, right? Perfect. Now let's go ahead and let's go inside of documentation here. And inside of here, you can find providers and models. Now just keep in mind, you can see that I have this big banner here that I am looking at AI SDK for documentation. So to know what you should be looking at, you simply go inside of your packet JSON here in the backend and check what version of AI do you have. If you want to, you can use the exact same version as me. So 4.3.19. So this would look like this, right? You would add it like that and then you will have the same version as me. But trust me, this is handled by Vercel and Next.js. So I'm pretty sure they are taking backwards compatibility seriously here. So even if you are on AI version 5 all you have to do is make sure that you're looking at ai sdk version 5 here or by clicking here or it will simply be the default version so don't worry if you can't find it uh they the reason i'm not using sdk 5 is because it is in beta right so that's the only reason why but i'm pretty sure that for what we need to use this package is going to be exactly the same the reason i'm even telling this is because I want to be careful. I want you to be able to finish this tutorial without any troubles. So again, you can use the same version as me, but if you're using version 5, just, you know, look out a little bit for any subtle differences between the documentation. So definitely do open the documentation and follow the documentation with me. So go ahead and find providers and models or whatever is the equivalent in the version 5 documentation here. And in here, you will find everything that you can use open ai provider google generative ai provider i believe that this is uh gemini right or if you want xai grok i think they also provide free tiers or if you want to use deep seek i've heard they are super cheap if not also free not sure so basically go ahead and choose the one you like the one you prefer i personally use ai open ai in all of my projects i have credits there so I'm going to be using open AI provider but you go ahead and click on the one you want and the first thing you're going to have to do is simply install their version of the package inside of your back end so pnpm f backend add ai sdk forward slash open ai so let's go ahead and install that and there we go. AI SDK OpenAI. So this is my AI version and this is my AI SDK OpenAI version just in case you want to use the same versions as me. And once you have added AI SDK OpenAI it is very important that you scroll a little bit down here until you can find the API key. So what you need to find is the default environment variable for your provider. In my case, that is OpenAI API key. So I'm going to copy that and I'm going to go inside of packages, backend, environment, local, and I'm just going to add it here. So for example, if you were using Google Generative AI, you would go ahead and add this, right? You would scroll a bit down, API key, and you will add this environment variable, right? Now, I'm going to show you how you can obtain API keys using OpenAI, but for other providers, you're going to have to research a bit on your own. But it shouldn't be hard at all. All right, so in order to obtain the API key for OpenAI, use the link on the screen to head to the OpenAI platform and go ahead and create an account. Once you're logged in, the first thing you want to do for OpenAI is you want to click on the little settings button and head into billing and make sure that you have some balance. So $5 will be enough for this entire tutorial. If you're using Gemini or something like that, it should be completely free for you. But just make sure if you're using OpenAI like me, you need to have some balance here. And after that, head into API keys. So I'm going to go ahead and create a new secret key and I'm going to call it Echo tutorial like that. And I will simply choose the default project permissions all and click create secret key and copy it and that's it that is how you obtain open ai api key immediately after you have added your open ai key anthropic api key gemini api key grok api key whatever you are using copy it head into the convex dashboard, go inside of settings, environment variables, add and paste it here. So the exact same thing that you just added here locally, make sure to save inside of your cloud platform here. Perfect. You can now close the OpenAI platform, Google platform, Anthropic platform, whatever. And let's go back inside of building our agents. so now what we have to do is we have to import the package that we installed inside of our support agent.ts file that we started developing so for me that's open ai from ai sdk open ai in your case it might be google from ai sdk google right and now you have to also import agent from add convex dev agent. And then import components from and then go back until you hit generated API. Like that. And then let's go ahead and do export const support agent, new agent, components.agent. let's go ahead and give it chat and this will simply be open ai.chat and let's go ahead and specify gpt4o mini so if you're wondering what should i use for google well i don't know you're going to have to research the models for Google or whatever else you're using. But this is strictly typed. So all you have to do is add annotations and you will see all the options. So just choose one. There is no specific one that I recommend you use, right? I'm just using whatever I think they had in their example here. Let me just check. Yes, they're using GPT-40 mini. So that's what I put. That's the only reason. You can go ahead and Google the equivalent of Gemini or Anthropic or whatever it is you prefer. And in the instructions here, well, let's go ahead and let's simply say you are a customer support agent. That's it. Later, we're going to create a whole big prompt for that. Perfect. So now what I actually, I'm going to do instead of AI, actually, this is fine. Let's leave it inside of AI. Let's leave it under support agent like this. And now we have to go ahead and delete all of our existing conversations. So let's go inside of data, conversations, select all of them and delete them because all of them have the fake thread ID. So now inside of convex public conversations, find the create mutation here. and we're going to modify it ever so slightly. So what we're going to do is we're going to finally remove this to do and fake constant with constant thread ID from await support agent that you can now import dot create thread pass in the context and pass in the user ID. Now the user ID in this case should be the organization ID because that's how we want to associate this thread with. So arguments.organizationID, like that. And now you have an actual working thread ID. So support agent, make sure you have imported it from system.ai support agent. You can also find these instructions here in the threads. So you can see you have to do agent.createThread. exactly what we just did. You can see that you don't even have to pass anything other than context inside. And then you will also get the thread ID. But I like to associate my threads with a specific ID, which is also, as they say, metadata. All right, so just one thing I want to do before we continue. Instead of system AI, create an agent folder, and then drag and drop the support agent inside. and you're going to have to update your imports now. So inside of conversations, make sure that you now change it to system AI agents support agent and inside of convex, convex.config, whoops, not here. Let me just find, oh, that might actually be the only place where. Yeah, but inside of the support agent, just make sure you have updated the components import to go three rows behind like this. Basically, make sure you have no errors. Make sure that when you do it through both dev, in the backend, everything should be working smoothly. There shouldn't be any errors whatsoever. Let's just confirm.\nfunctions ready. Perfect. So what should we do now? We should try and create a conversation. So I'm going to go ahead and I'm on localhost 3001. I have a proper organization ID here at the top. So I have the option to start the chat. When I click start chat, you can see that now I have an actual thread ID inside. And now we can use this thread ID to create new messages and to retrieve messages for this chat. So let's see where we are right now. We added the convex component, we selected our AI provider, we added API keys, we installed AI SDK, we deleted all old conversations and we updated the conversations create function. Now it's time to add AI components from Kibo UI. Now you might be wondering what is Kibo UI? Well using the link on the screen you can access it here so you don't have to google it yourself it is basically an amazing extension on chat cn honestly it is insanely good so in here you can find all things ai chatbot related like branching conversation input message reasoning ai response sources suggestion ai tool it's honestly amazing But alongside that, they also have all these amazing other components. There is one slight problem that I encountered. I wasn't able to add them to my Monorepo project. It just wouldn't work. Luckily, they are open source. They have their source code right here. So I have prepared in my assets repository the exact code that I'm going to add to my project now. So let's go inside of my assets. You can find the link on the screen here and go ahead and go inside of UI, inside of components. And in here, you can find the AI folder. And in here, you have all of these components available. So let's go ahead and add them one by one. So I'm going to go and close everything like this. let's go inside of packages ui source components and create a new folder called ai and let's start with branch simply copy everything inside create a new file branch.tsx and paste it here like that and for branch for example everything should be completely fine because we have the button and we have cn but for some of these we're going to have to require to install some packages so now let's go ahead and do conversation which for example will need this new package. So instead of AI conversation.tsx let me just check yes it is singular conversation and when I save there's an error so we have to add use stick to bottom. So let's go ahead and do pnpm f and this is our UI package and let's go ahead and do add use stick to bottom like that. So we are adding this to the UI package of ours and when we do that in a couple of seconds when this refreshes everything should work just fine. Perfect. Now I'm going to go ahead and pause and basically do the exact same thing and I will unpause if we have to add some more packages. So after I added reasoning.tsx I encountered this package missing. So let's go ahead and add this package pnpmfui add and then this package right here. Just make sure you filter to the UI package. And after that this should be resolved. But this one at the bottom is still an error and it still will be an error. But this is the AI response that we are adding right now. So I believe some of the next components will be the AI response. Let's see. Yes, it's right here. All right. Now in the response, which I just added from here, we need to install React Markdown and we need to install RemarkGFM. Let's go ahead and do that. So PNPM, whoops, PNPM F UI add React Markdown and the RemarkGFM. And let's see if after we add these packages, all these other TypeScript errors will resolve themselves as well or not. Looks like they resolve themselves. Perfect. So now our response.dsx is intact and working. Perfect. So three more left. And here we go. So I just added all of them. So that is branch, conversation, input, message, reasoning, response, source, suggestion, and tool. I highly suggest that you do it by using my assets here simply because I think that when you use their CLI tool, they actually add more components, but I removed things that we don't need. So that's why it might be easier for you to actually do it line by line. Yeah. Anyway, absolutely amazing UI library. we will come back for a few more of these components because they are amazing but for this chapter that is it so let's go ahead now and let's create our messages function so I'm going to close everything here because we now have added a very important part but now we need to be able to actually create the messages and we need to be able to fetch the messages so let's go inside of backend, convex, inside of public, I'm going to create messages, whoops, messages.ts. But just before we go ahead and do that, let's go inside of system here. And basically, we're now gonna have to create some internal packages here. So inside of system, outside of the AI folder, just inside of system, go ahead and create contact sessions.ts. So basically follow the same naming convention for contact sessions in the system folder as you did in the public folder. So contact sessions here and contact sessions here. The difference is that in here we're going to have the following. Export const get one internal query from generated server. And arguments here are going to be contact session ID, which will be a type of V from convex values dot ID. contact sessions. And the handler here is going to have the context, the arguments. And we're basically just going to return await context database get contact session ID from the arguments. Let's just not misspell contact session ID. So what is this? Why am I even doing this? What is an internal query? Internal query is practically the same thing as a normal query, API-wise, but it can only be called within other convex functions. So in here, you add something that you either want to use in actions, because remember, actions are not the same as mutations. Actions are a separate runtime. So in order to access the convex database through an action, you need to have an internal query, right? Or if you want to protect something so it's not publicly available, right? So let's go ahead and define the internal query for compact session. And it's actually going to make more sense to you why we're doing this in a second. But we also need another one. And that is going to be conversations instead of the system folder. So let's create conversations.ts export const get by thread ID. So very specific, right? And let's also add the internal query tag here. Arguments are logically going to be thread ID. Let's go ahead and set this to be a string handler asynchronous like that. and let's grab our conversation to be await context.database. So context and arguments. Context.database. Let's query conversations with index by thread ID, which we prepared ahead of time. and let's simply query to make sure that thread ID is equal to arguments thread ID. And we're looking for a unique record that has that because there shouldn't be two conversations that have the same thread ID. That should never happen. So we can safely do unique and let's return conversation here. There we go. And now we are ready to develop our messages.ts now that we have these internal queries. So why did we need to do that? Well, because export const create in the messages won't be a mutation. It's going to be an action, right? Remember actions? Let me go ahead and refresh your memory. I think it's organizations in the convex public here. Here are actions. Actions are special type of actions functions instead of convex that are used to query third party services. And because of that, they are not able to directly access the convex database unless you use internal queries. That's why we prepared that. So this concept is a little bit unusual. So it's probably confusing you, but it's actually very simple once you write a couple of these. I highly recommend going inside of their documentation and reading about internal functions. So they can only be called by other functions and cannot be called directly from a convex client. Use cases for internal functions. Here's the first one, the exact one we need. Calling them from actions via run query. That is the first and very example that we need them for. So let's go ahead now and develop this. So here are the arguments that we're going to need for this action. prompt thread ID and contact session ID. Then let's add the handler here, context and arguments. And let's import convex values like that. Now in here the first thing I want to do is I want to get my contact session by using await not context but instead context And the query that we're going to run is internal.system. Whoops, we have to import internal from generated API. So internal.system. Let me just check the internal. Why is it not working? Just a second. internal generated API. Okay, let's go ahead and write internal.system.contact sessions.get1. We need to have TurboDev on so Convex can update and create those internal functions first. So whenever you don't have the correct types, it's mostly because of that. So there we go. You can see how now it's fixed. We do have an error, but it's not related to the API because it's missing the arguments. The argument needed is the contact session ID, which is arguments contact session ID. And that's how we get the contact session so that we can validate it. So if there is no contact session or if contact session expires at is less than date now, throw new convex error from convex values with code unauthorized and message invalid session like that and now we can go ahead and grab our conversation that we need so conversation is await context run query internal system conversations get by thread id and pass in arguments thread ID. And that's how you find the conversation. Now, if no conversation was found, we can throw a new convex error here with code not found and a message conversation not found. And now let's go ahead and just do if conversation.status here is already resolved, we shouldn't be able to send messages here. So let's throw new error here, convex error here. Code bad request message conversation resolved. Great. Now I'm going to go ahead and just add a comment here to do implement subscription check. Because if we have subscription, only then are we going to allow the AI agent to respond. And now we're finally writing the code for the AI agent to respond. So await support agent. Make sure you have imported it from system AI agents. This is why this needs to be an action because we are calling a third-party service. What is the third-party service? Well, OpenAI or Gemini or Grok. That's why this needs to be an action and that's why we are doing this here over there type of fetching, right? So let's await support agent, generate text, pass in the context as the first argument, pass in the thread ID, arguments thread ID as the second argument, and as the third argument, pass in the prompt. And let me just check, is that prompt? should be arguments.brompt. Yes, like that. Now later on, we will have tools which we can add here, but for now, this can just be empty. And I think that should be it. That is our message to basically create a new AI response. And we are doing the same type of validation, because this will be used by the widget user, right? So the widget user will attempt to create a new message. So we have to verify their contact session. But we have to use the internal system for this one because this is an action. And that's the only way we can do that. Because if you try doing context.database.get, you will get an error. There is no database here because they are strictly separating their action with their query and mutation. So this is for security reasons, right? Great. So now that we have that, let's also develop export const get many. And this can be a normal query. So yes, you can mix and match both actions and queries and mutations in the same file. That's completely fine. The only exception would be if you were to actually add use node. This is used in very, very rare exceptions when for whatever reason, the third parity package you're using needs to have that, then you can't mix queries and actions. So what we're going to have to do here is first of all import query from the generated server so make sure you have added it the same way we added action here the arguments are going to be thread id which is a type of string pagination options which are a pagination okay so pagination options validator you can import it from convex server. So basically convex handles pagination for us. We don't even have to think about it. And contact session ID, which is a type of contact sessions ID. Let's add handler, asynchronous method, context, arguments. And now let's go ahead and first things first, validate our contact session the same way we did before. so we can now normally get it. You can see the difference. In here, we have access to convex database so we can just easily do this. But when the action is in question, we have to run it through the internal system. That's why we create those internal functions. And now let's go ahead and just do the normal check if the contact session does not exist or if expires at is past invalid or expired session. Actually, let's use convex error here. it doesn't matter. You can throw normal errors. It's perfectly fine. I just like to use this method in valid session. All right. And now let's go ahead and simply return back paginated items using await support agent, which we already have imported, list messages, passing the context, passing the thread ID, arguments thread ID, and pagination options. And that's it. We have now not just fetched the messages, but we have also associated them with their appropriate thread ID, and we have added pagination. So there are so many things we usually should do ourselves here, but Convex did it for us. Not only that, but I don't think you even noticed when we create a new message, you can see that in here, we are just passing the thread ID. And that is enough for this support agent component from Convex to use the history of the chat to prepare the response, right? So you can actually ask this AI model, what did I just say two messages ago and it will know because of this thread id so all of those things are happening in the background without you even knowing so for us this seems super simple right we're just creating the message and listing it the only complex part about this entire implementation is our contact session validation everything else is pretty easy right so now i believe we are ready to test this out. So what I want to do is I want to go inside of apps, I want to go inside of widget, app, my apologies, modules, UI, screens, widget, chat, screen, right here. And now let's add all of those AI components that we added. So from the AI conversation file, that's going to be AI conversation, AI conversation content, and AI conversation scroll button from workspace UI components AI conversation. So just make sure you have them, right? Let's check once again. Inside of packages, UI, source, components, AI, branch, conversation, input, message, reasoning, response, source, suggestion, and tool. You can use the link on the screen to obtain them. After you have added AI conversation, it's time to add everything from AI input. That is AI input, submit, text area, toolbar, and tools. After that, let's go ahead and let's add everything from AI message. That is the message itself and the message content. Now for the response, it's quite easy, just response. For the suggestions, AI suggestion and suggestions. And now let's go ahead and let's try and fetch our messages here. So let's see, do we have everything we need? We have organization ID, we have contact session ID, and in here we have the conversation itself. So I think we should be able to fetch this by using use thread messages. That's right. Convex has prepared a hook for us. use thread messages from at convex dev agent react so uh let me just see i'm not sure if i have to install this or if i have to install this now because we just installed convex dev agent but remember only in our back end package so now i'm just checking uh we have to install this now in our dashboard too. My apologies in our widget. So pnpmf widget add and let me just see the exact version. So this version let me just add it. So make sure you filter into the widget. That's where we are adding it. Convex dev agent. Let's go ahead and run through dev again. and there we go no more errors now and another thing we can import from here is two ui messages basically another helper here so first let's use the use thread messages to load our messages so i going to go ahead after conversation here and i going to add const messages here to be use thread messages And let use API And then we are going to have to use the thread ID And we can obtain it through conversation, which we fetch from above. So let's check. if we have conversation question mark thread ID and if we have contact session ID. This contact session ID is available through the atom value. So if we have both of those, let's go ahead and let's open an object and let's pass in thread ID from conversation thread ID and contact session ID. Otherwise, skip. and then the third argument is going to be the initial number of items we are going to fetch and that can be 10 and that is our messages just like that perfect so now if you actually want to you can already go below this in the json stringify messages so now if you go inside of your app localhost 3001 with the proper organization ID at the top, click start chat, and you will see an empty array and is loading false and status exhausted, meaning it loaded all messages and there are none to be loaded, but it's working because we have a actual thread ID. This thread ID actually exists, so it's working. And here's something fun we can do. For a brief second, go back inside of packages, backend, and go and find your conversations.ts public here. And every time you create a new conversation, wouldn't it be cool if the AI sent the message first? Well, you can actually do that. Let's go ahead and do this. Let's do await, save message. You can import save message from ConvexDev directly. like this. And in here, go ahead and pass context and pass components.agent. In order to actually import the components, we have to do that from the generated API. So let's do this from generated API. That's where you can find the components from. so let's go back instead of the create mutation so passing the components passing the thread id arguments oh it's thread id we actually have it right here okay and then let's create a message from assistant and the content in here will be hello how can i help you today so that's going to be the first message of every chat. And I'm going to add a little to do here. Later, modify to widget settings, initial message, because we're going to allow users to customize the initial message. But right now we don't have that functionality. So refresh your entire page now and click start chat. And you can see the difference. Now we now have our first message. hello how can I help you today from assistant perfect so it works we can officially save message and we can load the message and this actually gave you a glimpse of how we're going to enable human conversation you can see that I was able to completely hijack this convex's AI component by simply importing save message directly and deciding myself what the response will be And that's how we're going to create human conversations without AI interfering. Because you can see that in order for AI to interfere, we have to call await support agent and then generate text, right? But if you don't want AI to respond anything, you can just use this. Just directly save the message, save them as user, and the content will be arguments.prompt. So now that we have all of that, let's create the UI and make this pretty. In order to create the UI, we have to go inside of the chat widget screen and we have to implement the form. So let's import Zold Resolver from hook form resolvers Zod. This is the problematic one, right? If you remember, let's go ahead and let's import Zod from Zod. and let's import I'm not sure if we have we do have button great but let me just find we are going to need to import use form from react hook form and I think that's enough for now now let's define our form schema here just above the widget chat screen form schema uses zod object to create a message which is a string a minimum value is one message is required like that and now let's go ahead and let's create our form actually I'm going to do that after the messages here oops right here so let's define our form form is going to be use form z infer type of form schema constant from above and then inside of the object of that form schema we're going to add the resolver which is a type of zod resolver and pass the form schema inside and then a very simple default values object with message being an empty string. Basically we have already done this if you search for use form z infer instead of widget out screen you can see the exact same thing but different default values so in case you're not sure where I found this from that's where perfect now that we have uh the form here let's go ahead uh let me just move this on back somewhere up here like that it doesn't need to be closer there so now i'm going to add const create message and i'm going to call use action because remember message is an action so let's pass in API.public.messages.create. So we have imported useAction from Convex React and we already have API from Workspace backend generated API. Great. And now let's go ahead and let's develop onSubmit method which is an asynchronous function here. And its values are going to be a type of z.infer typeof schema. Oops, form schema. And then in here, let's go ahead and first do, if there is no conversation, well, let's just return, like break the method. There's no point in going forward. Let's immediately reset the form so we clear it. And let's then do await, create message, pass in the thread ID from the conversation, thread ID. Prompt will be values.message, individual message one. and contact session ID will be our, so yes, we can also check if there is no contact session ID here. Like that, there we go. And now we can just use the shorthand operator here. This is the same thing. So if you write it like this, or if you write it like this, it's the same thing if the key and the value of the variable are named the same. Excellent. So now we have that. let's go ahead and let's actually develop this form. So I'm going to go ahead outside of the widget header here and I'm going to remove this div. Instead, I'm going to add AI conversation. So we have all of these components already imported. Inside AI conversation content. And then inside of here, I'm going to do two UI messages. Again, we have this imported from convex dev agent react. to UI messages, messages.results or empty array. So messages, whoa, my apologies. So let's just see. Messages come from, let me just find it, use thread messages. So that's what I'm referring to right now. We are iterating over them. But let's add a fallback here to an empty array. question mark dot map, get the individual message in here and return AI message. From property, we'll check the message role. If the role is user, we're going to say user. Otherwise, we're going to say assistant. Key is going to be message ID. now inside AI message content AI response and render message content so there isn't much for me to explain here it's just composition of this component right I just follow Kibo UI documentation and how to do this that's why I'm not explaining too much it's just composition and let's go ahead and just add a little like this. Whoops. To do, add avatar component. We do have avatar component, but we're going to have to create a custom one. Like this. Okay, in here we end conversation content. We end the conversation like that. Then I'm going to add a little to do here, add suggestions. We still don't have the resources for that yet. and I think we can already see how this looks now. There we go. Hello, how can I help you today? Much, much better already. Now it's time to develop the actual form. So I think we forgot to import all the form elements here, but there are only two that we need actually. So somewhere at the top or here, add form and form field from workspace UI components form. All right, let's scroll down now. Form spread the form from the hook inside. And now in here. Let's go ahead and add AI input. The air input props will be the following on submit It going to be form handle submit and passing on submit class name here will be rounded none border x zero and border bottom zero so how did i know that i can pass on submit here why am i even passing on submit here well again let's refer to our widget out screen this is the first place where we used the form right remember that this was the composition form and then a native form element inside with on submit. So why am I adding on submit here? Why not a form element inside? Well, if you take a peek instead of AI input, it's actually form. That's why. So why form handle submit? Why not just directly on submit? Very simply, form.handleSubmit will make sure that the fields are validated first. That's why. Now inside of this AI input, let's add form field, which is a self-closing tag. Let's give it control, form.control. So this is just a React hook form API. That's why I know how to do this. It's going to be disabled if conversation.status is resolved. So we are not going to allow the user even to type if conversation?status is resolved. the name of this form field will be message because that's the only field we actually have. The render here will destructure the field. AI input text area like that. Disabled will be if conversation question mark status is resolved again. OnChange will be field onChange just like that. on key down here will get the event and we're very simply going to check if event.key is equal to enter and if we are not pressing down the shift key we're going to submit the form as well. So form handle submit on submit and execute that. Basically this on key down prop is not required I just think it's cool that you can press enter and it will submit automatically. and now the placeholder again will depend if the conversation is resolved or not so placeholder if conversation status is resolved this conversation has been resolved otherwise placeholder will be type your message just be mindful of the question mark here and finally the value will be filled dot value great and this is actually a self-closing tag so you can add a forward slash here and remove this here, just like that. Then outside of it, go ahead and add AI input toolbar, add AI input tools, and they're going to be completely empty, but we need them just to take this space. And then AI input submit, which is our last component, I promise. Disabled if conversation.status is resolved or if form form state is valid. Make sure to put exclamation point in here. So if it is not valid and a question mark here. Status will be ready and type will be submit. Just like that. And here we have our component. And now if we've done this correctly, I should be able to ask the AI a question. How are you? And let's go ahead and click submit. Our message is submitted. And would you look at that? The AI has responded. I'm here and I'm ready to assist you. How can I help you today? What can you help me with? So you can see that this is now an AI model that we can talk to. And you can see that it's hallucinating right now, right? I just told it, you are a customer support agent. It actually has no access to tools, no access to any embeddings. It will just generally act as a customer support agent. But this is our blueprint and this is what we will be working on. So you can see how simple Convex made this for us. And we even saved some time by not having to develop these tools. We just have to add them and use them. But basically 90% of the heavy lifting here was done by Convex. We just had to add our AI SDK And the rest is history. So for example, if I now try and do what did I ask you before, you will see that this AI is history aware. So you asked me how I was doing, right? Our first message was, how are you? And then you inquired about what I can help you with. That was my second message. So you can see, we didn't do anything to give this AI memory. Convex agent component did that for us using that magical thread ID thingy. Absolutely amazing. I'm super proud of what we did. And here's a fun fact. So you are probably wondering, okay, great. I can see conversations in my database and sessions. But where are those messages? Well, look at this little button here. Click here and click on agent. This is where they are. So if you go into the messages here, you will find all of the messages in here. So that's how they are stored in Convex. You can see these are components, right? And one quick tip as well. Sometimes it's hidden. Sometimes it looks like this. So you have to click on the tables. Just a quick tip. Excellent. Amazing, amazing job. I think that's everything that we had planned. So we added AI components. We created the message functions and the internal functions. amazing, amazing job and we also made it look like this there are a couple of things missing like infinite imagination and avatars but we will add that very soon, I think it's time to end this chapter at this moment so this is 14 AI agents let's go ahead and close everything let's go ahead and stage all of our changes 14 AI agents let's commit let's open a new branch 14 AI agents and let's publish that branch then I'm going to go ahead inside of my repository here and I'm going to review this pull request just to see that we didn't miss any critical security issues like we did last time and CodeRabbit saved us with the review. So we introduced a fully featured AI chat interface with structured message rendering, input validation, and enhanced conversation controls. We added support for AI-generated suggestions, reasoning, sources, and tool outputs with new collapsible and interactive UI elements. This is describing the Kibo UI components which we added. We enabled multi-line message input, improved scroll behavior, and dynamic message fetching with agination. So all of this is still I believe mostly talking about the Kibo UI components besides message fetching that is our convex agent component. We integrated customer support AI agent using the latest OpenAI model. In my case it's OpenAI right for back-end conversations and in here file by file change of course but in here we have the sequence diagram which is always interesting to look at just to repeat our knowledge. So the user submits a message via form using widget chat screen. We call the create message function with prompt, thread ID, and session ID. The backend app receives that and it uses the support agent to generate the response. The support agent returns the AI response and then we update the messages and we fetch them via get many function. And finally, we rendered them in the chat UI using keyboard UI components. Amazing. So in here, as for the comments, it left a suggestion to wrap the await create message in try and catch. This is true. We can do that. The only reason I'm not doing it is because I haven't thought of a good solution to display error messages in the widget component because I'm not sure how a toast notification would look like because remember, this won't be a full screen app. This will be a very small part of the iframe. So that's the only reason why I'm not wrapping this inside of try catch and then firing a toast because this is the widget component. So it's a little bit odd, right? The check for open AI key, I'm fine with the way it is. in here a warning to check for duplicated open ai http traffic so good advice but i think we are good here and yes this is a uh this we should be aware of that so we just added react markdown which depending on how we use this component later on will mean that we have to sanitize user's request because without sanitization we are subject to xss injections and now it's just reviewing the kibo ui components so i'm not going to really look through these changes because they are from a third party i am okay with the way they are which means we are ready to merge this pull request amazing amazing job and let's go ahead and go back instead of our main branch Let's go ahead and synchronize our changes to make sure everything is up to date. Let's go ahead and select the graph just to confirm. We detached and we merged back. Since we are already in the 14th chapter, I would recommend that at the end of each tutorial, my apologies, at the end of each chapter, you actually try Turbo Build for a very simple reason that if something is wrong, you have time to fix it. Because if you don't check your build for the next, I don't know, 10 chapters, however many we're going to have, you're going to have a lot of problems fixing them, right? So please go ahead and do occasional turbo builds. You can see that my widget is building fine. If yours has any errors, now would be the good time to try and fix that. It is 99% a type error or a lint error, so it should be easy to fix. but yeah that's my advice basically just start building your apps because we are deep into our app and if we if you don't try turbo build and only try it when we deploy you might be hit with like a hundred type errors wondering where they came from so always turbo build at the end of each chapter so you are ahead of any errors i believe that marks the end of this chapter\nchapter we pushed to github amazing amazing job and see you in the next chapter in this chapter we're going to enhance our widget chat screen by adding infinite scroll functionality as well as an avatar component to render some images next to chat bubbles so let's go ahead and start with use infinite scroll hook since we're going to need this hook across our web and widget applications, I thought it would be a good idea to add it to the UI package because in here we actually have hooks already. So let's go ahead and add useinfinitescroll.ts. And then in here, let's go ahead and import everything we are going to need. So that's going to be use callback, use effect and use ref from React. Now let's go ahead and let's create an interface, use infinite scroll props. The first one is going to be the status. Now the status can be the following options. Can load more, loading more, exhausted or loading first page. So if you're wondering where do I get these values from? Well if you actually go inside of the widget chat screen and if you find use thread messages here you can go ahead and try and do messages dot status and when you hover over it you will see loading first page can load more loading more and exhausted so those are the options and I've just transferred them here so just double check I recommend that you do this and just hover and just double check that yours are the same here. You can even copy them from there and just add them here like that. Now they are in this order. It's the same thing. Let's add a function load more to accept a certain number of items here which is a type of number and let's go ahead and add an optional load size. So if we want to load by 5, by 10, by 20 like that And let's add an Boolean observer enabled, which will basically tell this hook, do we want automatic infinite scroll or only on load more button click. Now let's export const use infinite scroll hook here. And let's assign the props. and let's destructure the status load more load size observer enabled set to true and load size set to 10 by default now the first thing i want to do is i want to add a top element ref like this which is a use ref with a type of html div element and a default value of null Now let's define the handle load more use callback method. Like this. And inside we're going to check if the status is can load more. In that case we're going to load more how many? Well the exact load size we defined. And we need to pass status load more and load size in the dependency array. Now let's add the use effect here. Inside of the use effect, we're going to get the top element by referencing our top element ref, which we defined seconds ago. And since it's a ref, the way you get the value is by accessing dot current. So if not neither top element and observer enabled, return. So if it so happens that we can't find the top elements or if observer is not enabled, no point in continuing forward. So just make sure you put an exclamation point in here. And then let's go ahead and let's do const observer new intersection observer. go ahead and open a function like this open an array and extract entry from here go ahead and open a function if entry question mark dot is intersecting handle load more like that and then add a threshold 0.1 so this is basically sensitivity right i find it that it works pretty well with these settings. And let's add observer.observe and pass in the top element. So when we reach the threshold and intersect with the top element, we're going to trigger load more, but only if top element is visible and if, I mean, if top element was found and if the observer is enabled in the first place. And what's important is that you add a unmount function here and disconnect the observer so there aren't any leaks. And we need to add handle load more and observer enabled. So in case you were wondering why did load more need to use use callback because we're using it in use effect and that's how we memoize it and how we safely add it into the dependency array here. And now what we have to do is we have to return back the top element ref handle load more can load more, which can simply be a status, and then can load more. And then let's go ahead and just do the same for all the other statuses. Is loading more? Is checking loading more. Is loading first page? We'll check if the status is loading first page. Is exhausted? We'll check if status is exhausted. Just so we don't have to do that wherever we use the hook, we have this little helper here. There we go. All right. So I just want to remove this messages.status in the widget chat screen because I see it's unsaved and it's bothering me. So I'm just going to close it. Now let's go ahead and let's create the infinite scroll trigger component. So again, inside of packages UI components, create infinite scroll trigger.tsx. Let's go ahead and let's add button and cn both from workspace ui one from components and the other one from lib now let's create an interface infinite scroll trigger props can load more is going to be a boolean is loading more is going to be a boolean on load more will be a function load more text will be an optional string null more text as well class name same thing and reference an optional react reference html div element like so let's export const infinite scroll trigger and now in here let's attach infinite scroll trigger props and then inside of here we can destructure all of those props. So what I did was destructured all of them and I added default values for load more text and for no more text. So by default the button will say load more and if it can't find any more items it will simply say no more items. now let's go ahead and let's define the text to be load more text if is loading more text will be loading if you want to you can even create a prop for this but i think it's a bit of an overkill So, else, my apologies, else if not can load more, text will be no more text. And let's go ahead and let's return a div. Let's give it a class name, cnflexfulwithjustifycenterpy of 2. and then pass class name as the optional prop and pass ref, whoops, outside, ref to be ref. Add a button, render text inside, give it a disabled prop, if not can load more, so if it can't load more or if it is loading more. on click it will call on load more size will be small and variant will be ghost just like that perfect and now I think we're actually ready to use these two so let's start by going back inside of the widget chat screen which you can find inside of apps widget modules, widget UI screens, widget chat screen. And I just want to add my import. So let's import use infinite scroll from workspace UI hooks use infinite scroll. And let's also add infinite scroll trigger from workspace UI components infinite scroll trigger. Now let's go ahead and let's use the use infinite scroll hook right after our messages here. So it's all kept together. So I'm going to go ahead and call use infinite scroll like this. And inside, I'm going to pass status to be messages.status. And you can see that, well, let's first add all of them so we don't have any errors. So load more will be messages, load more. load size will be 10 the same number that I used for the initial number of items if you want to you can remove this magic number and then put it in a constant but you can see that I have no errors here because my status matches exactly what it accepts right so that's why it was important for you if you're getting errors here simply hover over message.status and check what's happening here and then go inside of your use infinite scroll and check that you have the same here and then you will probably have to slightly modify them here if they are different, but they shouldn't be. Now in here you can destructure the top element ref, handle load more, can load more and is loading more. Whoops, is loading more. Like that perfect Now we have to add our infinite scroll trigger which will also be the top element ref that will be used for the observer intersection So usually with an infinite scroll, you would put that at the bottom of your page, but we are doing chat interface here. So it's the opposite actually. Only when the user scrolls at the top of the chat page do we load all their messages. so we have to find the start of the ai conversation and then the start of the ai conversation content where we rendered the messages and in here add infinite scroll trigger which is a self-closing tag give it can load more of simple can load more give it is loading more is loading more onload more, onload more and give it the ref top element ref. And this should be handle load more, not onload more. Perfect. Let's go ahead now and let's do through both dev here and let's go and load our widget component. A quick reminder, you go to dashboard, clerk, go inside of your organizations here, find a functional organization ID and add question mark organization ID with capital I and pass the organization inside. Perfect. 24 hours has passed since the last time I had this session. So I have to create a new one. So I'm going to call myself antonio here antonioexample.com perfect uh let's go ahead and oh this is interesting so after i click continue nothing happens looks like we have a bug here interesting let's see inside of my application uh let me remove my old session here and try adding new one and then if i refresh what happens so it works then okay but if i remove it completely and if i try antonio example.com one two three whoops i reversed the two it adds it to the session and when it when i refresh it obviously works but looks like there is some issue in my out screen inside of widget out screen when I submit all we do is set the contact session id yeah we actually don't do anything first of all let's remove the test organization id from the out screen second let's add a setter set screen to be set atom value setAtom, use setAtom like this, screenAtom. So make sure you import screenAtom. And let's just call setScreen after that here. To selection. That was missing. Let me go ahead and just try and see if I fix this issue. And I refresh now. Let me try again. Continue. There we go. small bug fixed it very easily. Let's go inside of start chat now and what I want to do is I just want you to type out a bunch of messages right so feel free to interrupt the AI it doesn't matter right we're just trying to make this a bit of a longer chat so that we can actually test out the infinite load. One problem that we have is that we can't really test it. It just didn't occur to me until now. But if you can use your chat like you used until this point, everything should be okay. One indicator that this is working correctly is the fact that it's showing no more items at the top. The reason I'm saying we can't test this is because we don't really keep the option to select individual chats. You can see I can only start a new chat. Right, so okay, we will test this in some other chapter, but I'm pretty sure this is working perfectly fine. Nothing complicated was added here. Basically, this infinite scroll trigger is the no more items text because it loads the very first message the moment you open the chat, right? We will find some other opportunity to test this, preferably in the next chapters when I implement the actual conversation list. so for now just leave it like this even if you have a bug it will be easier for you to debug by implementing the next next chapter first all right so one more thing i want to do i want to add the dice bear avatar here in order to do that we first have to add a logo to our app so you can find using the link on the screen my public folder and inside of here let me just update the repository and you will see the logo.svg. All right, just update the repo. So public folder, logo.svg. So where did I find this logo? So every single time in my projects, I use this amazing website, Logo Ipsum. You can use the link on the screen to let them know you came from this video. They are like the lorem ipsum of logos. Amazing, amazing logos you can use in your demo applications or exercises like the one we are doing right now. So basically, just choose the one you like. I found the one I like. And let's go ahead and save it. And once you save logo.svg, let's go ahead and let's add it inside of apps, widget. And inside of here, I think you're going to have to create a public folder. So inside of widget, create public like that. and then inside of here you're going to have to add logo.svg which you just downloaded so drag and drop it copied however you want to do it great once that's here let's go ahead and let's implement the dice bear avatar component so again i'm going to do that inside of packages ui source components let's create dicebear avatar.tsx now we have to go ahead and install some packages here so let's go ahead and do turbo whoops my apologies in the root of your app pnpmf widget let's add at dicebear forward slash collection and at dicebear forward slash core so those two packages inside of your widget application and then you can turbo dev again once you have that let's go ahead and let's add use client in our diceware avatar here let's go ahead and let's import glass from uh dice bar whoops i just told you to add it to widget didn't I my apologies all right so we made a mistake let's see how to fix it we added two incorrect packages to the incorrect app so select the package we just added and the same way we added them we're going to remove them pnpmf widget remove dice bear collection so let's do them one by one so first I'm going to remove dice bear collection here and then I'm going to remove dice there or because we don't need them in the widget component so PNPM like that and that should reset your PNPM lock and basically everything it's always safer to do this than to manually try and modify your locks right so now let's go ahead and do PNPM F UI and let's add dice bear collection, and core into the UI package. And once we do that, let's go ahead and turbo dev our app again. So turbo dev to make sure everything's running here. Now we should have package.json inside of packages UI modified. let's go back inside of the dice bear avatar we should no longer have any errors here and now let's import create avatar from dice bear core let's go ahead and let's import use memo from react let's import the avatar component from workspace ui components avatar and let's import cn from workspace ui lib utils let's create an interface dice bear avatar props we're going to have a required seed which is a string size which is an optional number class name which is an optional string badge class name which is an optional string image url optional string and badge image url which is an optional string so this will basically allow us to have a cool little effect where we have the avatar and in the corner of that user's avatar we're going to display the flag where they're writing from so we know where they're located that's why we're adding the badge functionality as well let's export const dicebear avatar let's assign the props here and let's destructure all of them and let's set the size by default to be 32 now inside of here the first thing we're going to do is we're going to create avatar source we're going to call use memo here to memoize it if we have image url passed as the prop we're simply going to use that image url it's kind of like an override otherwise we're going to create the avatar so this will be useful when we have many customers and we don't have access to their images we could extract them from their emails but then again they never really verify that email so i'm not sure how much we should associate them with their email and giving them an option to upload an image in a customer support chat is just weird. So we're just going to use a simple avatar collection of this glass-like images which will use the seed which we are going to convert to lowercase here and then trim it to make sure it's valid with the size to create a unique avatar for each of our customer and we are not going to use any requests for that So this will create the SVG data URI So you won need to do any network requests for this Don worry Now let calculate the badge size So if we are basically going to round this number, but basically we're going to take 50% of the avatar size like this. and then let's go ahead and let's return a div let's give this div class name relative inline block let's give it style width size and height size so they are the same because avatars are our circular in our they have the one by one aspect ratio in our case. So let's go ahead and give the avatar component itself a class name of, let's make it dynamic, cn. We're going to add border and then class name like this. And then you can copy the style here and pass it as style. Inside, let's add avatar image. alt can be it doesn't have to be anything we can just say image and source will be avatar source and then we're going to check if we have badge image url we're going to create a div that will render that badge image url let's add class name here make it dynamic so prepare CN here absolute right 0 bottom 0 flex items center justify center overflow hidden rounded full border to border background and BG background and passing the batch class name and now let's open the style attribute here and inside we're going to pass width and height to be the batch size which we used 50% of the avatar size from above and transform translate 15% 15% this will basically put it like in the corner of the avatar so it will look nice. And then finally, just a simple normal image element. Alt can be, it can be empty. Class name will be height full with full object cover. Height will be badge size. Source will be badge image URL with will be badge size like this. Basically, in the alt, you would put what this image is representing, right? So, okay, we can just say at least badge, right? So it makes sense. This is the main image or avatar. And this is badge. So it makes sense. All right. And that's it for our Dice Bear avatar. Now let's go back inside of our widget chat screen. And let's go ahead and let's use the Dice Bear avatar. So let's scroll down here to where we iterate over messages. And here we have to do add avatar component. So let's go ahead and let's check if message.role is assistant. Let's go ahead and render a little image for them. So let's do dice bear avatar, which we have to import. So let's just go ahead and quickly import it. I'm going to just add after above this scrolls from workspace. UI components dice bear avatar let's go ahead and give it image URL of to be logo dot SVG like that and seed assistant and size will be 32 so now if you refresh here and go ahead and just open a chat there we go you will see that the logo of your app will be displayed here but how will this look for the users well you don't have to pass image url right if we don't have anything to pass we're just going to generate a random glass of them and how will the badge look like well you can pass the badge image url to be logo svg and you will see that then they're going to have a small little badge at the bottom here right and maybe this is a cool idea for you to differentiate between your AI and your operators. So operators can have glass image avatar, but when it's a bot talking, it can have like a little image at the bottom. So whatever combination you want, I'm going to choose this one. And yes, we are only displaying it for assistant because it makes no sense for you to display your own little avatar here. It's just taking up space. Great. So that's what I wanted us to do here. We also fixed an important bug inside of our widget out screen. So we fix this, this, this and this and now let's go ahead and deploy this to GitHub. So I'm just going to go ahead and add all of this. This was 15 infinite scroll. Let's commit. Let me go ahead and open a new branch 15 infinite scroll and let me publish the branch. And as always, let's go ahead and let's open this pull request and review it, even though this one will be pretty quick, simply because we didn't do anything important, just a few components to help us display the app better. And here we have the CodeRabbit summary to end the chapter. So we added infinite scrolling to the chat message list, allowing users to load more messages as they scroll. We introduced a new avatar component that displays a consistent assistant avatar next to relevant messages. We also added an UI trigger for loading additional content in infinite scroll contexts. And in here we have the diagram explaining how it happens. So when the user scrolls to the top of the message list in the widget chat screen, the top element ref intersects the viewport inside of the use infinite scroll hook. that hook calls load more callback which then retrieves new messages and returns it basically excellent so in here it looks like we left out image url prop inside of avatar source so that's something we have to fix i'm going to do that in the next chapter but for now we can go ahead and merge this pull request amazing job so this it doesn't seem too useful what we did just now but we're going to reuse these components in several places coming along so that's why i wanted to have this chapter to do that so we will come and use these components again and you will see how useful they are amazing job and see you in the next chapter and of course after you do this make sure you go inside of your main and synchronize the changes almost forgot that don't want you to continue building something in an older branch but even if you do no problem just merge all of those changes together. Great, amazing job and see you in the next chapter. In this chapter, we're going to go ahead and implement the widget inbox so that we can look at all the previous conversations we had for an active session. Let's go ahead and start by adding conversations get many function. So just make sure that you're on your main branch as always, make sure you're not developing on any other branch. And let's go inside of packages, backend, convex, public, conversations. And in here, let's go ahead and see what we have. So we have the create mutation, and we have get one. I believe those are the only two we have. Now let's go ahead and let's add export const get many. This will be used to load our inbox component. so let's go ahead and prepare the handler and then we're going to go and add all the arguments needed so the arguments needed to load conversations are the following we need the current contact session id and we need the pagination options and thankfully convex provides that for us using convex server import so just make sure that you have contact sessions here we already used it in the one below get one. So you can just copy it from there. Now in here, we can get the context and the arguments. And let's go ahead and do what we usually do. And that's just confirm if the contact session is valid or not. So I'm just going to copy this. And I'm going to paste it here. And I would like to call this just this, this, let me just so this, this and this contact session, Just because I want to be very specific, I don't like when it's called session, because this is literally the contact session table. So if the contact session table is missing, or if it expired, we are going to throw back the error. And now what we have to do is we have to fetch all conversations by this contact session. And if you take a look at our schema, our conversations table actually has an index to help us with that exact thing. So let's go ahead and fetch all conversations by using await context dot database. And let's query the conversations using with index and simply pass by contact session ID. And then go ahead and grab the query here. And let's do query equals contact session ID arguments contact session ID. As simple as that. let's order them by descending and let's paginate them using arguments pagination options and this will automatically add the cursor and everything else we need and now again be careful this is a public you can see we are writing this inside of the public folder which means nothing by default it's just the place where we chose to add functions which are available to anonymous users using our widget chat box So remember, conversations has contact session ID, right? And even though we absolutely do know it at this point, still, I would prefer if we don't return the entire object inside. And while we are here, it will also be a good idea to load the last message from each conversation because look at how this is supposed to look like, right? We are supposed to know what is the last message. Is it this? Is it resolved, right? So we are supposed to know something. And there are many ways we can capture the last message but the one we can do now is not exactly the greatest of ways but we are going to iterate over each of our conversation and find the last available message So let go ahead and do const conversations with last message. Await promise all. Inside of this promise all, let's do conversations.page.map. Let's go ahead and open async conversation here like this. And let's do let last message by default will be a type of message document from convex dev agent. So just make sure you have imported message doc here like that. Or it will be null, which is the default state. If you're worried about using async inside of dot map, don't be. So yes, you can just normally use async inside of dot map. But if you add it inside of promise all, it will work as intended. So now let's go ahead and do, let's just const messages to be await support agent dot list messages, context, thread ID, conversation, thread ID. Pagination options, number of items will be 1 and cursor will be null because we only want the last message. Nothing more than that. And if messages.page.length is larger than 0, let's assign the last message to be messages page first in the array or fall back to null. Perfect. And now let's go ahead and let's return individual items here. So return underscore ID will be conversation ID. Creation time is another conversation field that exists. So this creation time exists even if you don't add it in the schema. So convex internally keeps the creation time. Status, conversation.status, organization ID, conversation.organization ID, thread ID, conversation.thread ID, and last message. Well, just last message. And there we go. And now what we have to do is just find where this ends, this promise all right here. And let's return conversations entirely and then page conversations with last message like this. Perfect. So that is our function that loads all conversations for this contact session ID and assigns the last message to each of those. So just, you know, some of you might have already noticed, you know, this isn't the most optimized way to fetch the last message. Yes, absolutely. So for those of you who didn't notice, the problem with this is for each conversation, we are adding additional requests, right? What are the alternative ways? Well, in alternative, we could have a separate request for all messages by passing the necessary conversation IDs. The problem is our messages API at the moment works like this. And this is the only elegant solution that I could find. for the current state of our app. And I believe for even when it grows a little bit, this solution will be okay. But if you get to a point where you have hundreds of thousands of these, you would probably have to look into optimizing this a little bit. Honestly, this should never really be a problem, especially in the public conversations, because which session, which expires within 24 hours will have hundreds of thousands of conversations for a single organization. That's absurd, right? So I think in reality, this should work just fine majority of time. So just wanted to give you a quick note about that. Some of you are probably aware of its time complexity here. Great. But now let's go ahead and actually use this. So what did we say we need to do next? After this, we said we have to do the widget inbox screen component. So let's go ahead and create that. So I'm going to go inside of my apps widget. inside of my modules, widget, and let me go inside of UI, instead of screens here. And I'm going to copy the error screen simply because it's the simplest one to adapt. So widget, inbox screen, widget, inbox screen. Let's remove the error message like that. And let's remove this inside and simply say inbox. Let's remove this and let's remove this and the items centered thingy. There we go. Now let's go ahead and let's go inside of our widget view here. Let's find the inbox here and let's just do widget inbox screen. Just make sure you have added the import for it. And now let's go inside of the selection screen again. because right now we actually have no way of looking at this screen. So let me just do Turbo Dev. There is no way for us to manually go in the inbox screen. So as always, localhost 3001 with a proper organization ID is required. You can see we don't really have the inbox button now. So that's because we are missing the widget footer. So let's go inside of the widget selection screen where I am right now and let's go let's see so widget header ends here and then this div opens so right here let's add widget footer which is a self-closing component you can import it from dot dot forward slash components widget footer and there we go now as you can see you can click here and that should take you to the inbox oh actually it doesn't do anything yet so let's also enable the widget footer now so let's go inside of this widget footer and let's go ahead and do a const screen to be useAtomValue from Yotai, screenAtom, and const setScreen, useSetAtom, screenAtom, like this. So on-click here, setScreenSelection, and this will check if screen is selection, that's correct. This will check if screen is inbox, and add text primary to it. and on click it will call a set screen inbox as simple as that now let's try it out so when i click here there we go we are in our new inbox the problem is we have to go back instead of the widget inbox screen which we are currently developing here and let's immediately add the widget footer here like this and now you can switch between the selection screen and the inbox component. Perfect. And now let's focus on the inbox component. So first, let's modify the inbox screen header here. So inside of this header, we're just going to slightly modify it. So this div will be flex, and then let's just do items center and gap X of two. Then let's go ahead and remove the content inside. And let's add a button from workspace UI components button. Let's go ahead and give it arrow left icon from Lucid React as I've added right here. Let's go ahead and give this a variant of transparent size of icon and on select. Whoops, on click. Let's go ahead and let's call set screen. So const set screen use set atom screen atom so make sure you've added those two imports and let's call set screen this will go back to selection like this so now there we go we should have a button to go back from there as well and outside of here let's just add a paragraph inbox there we go I think it looks a bit nicer and gives us more space here no need for the welcome message on the inbox right welcome messages here for loading for out but for this you know we need space all right so now let's go ahead and let's load our other atoms here so I'm going to grab my organization id using use atom value organization id atom then I'm going to get my contact session ID using useAtom value again. Contact session ID Atom family and pass in the organization ID which I have to capitalize properly. Or empty string. Still don't like this solution but it's okay for now. And then let's go ahead and let's fetch our conversations. Use paginated query from convex react. Let's go ahead and pass in API public conversation. Whoops, I didn't import API. So API from workspace backend generated API. Public conversations. And this should be get many. And then we have to check. Do we have the contact session ID? If we have it, let's go ahead and let's add contact session ID to be, well, the very same. Otherwise, we skip this query. And then in the third argument here, let's add the initial number of items to be 10. There we go. Now we have our conversations. And then inside of here, we can already do the following. We should be able to JSON stringify conversations. And now, there we go. You can see all of my previous conversations, and you can also see that some messages are visible in here. Basically the last message for each of them. So now let's create the components to properly display them. So let's first see, do we have to modify something in the container? Flex, this is flex1. Flex column, let's change this to be gap y2, like that, and overflow yauto. like so. Then inside of here we're going to iterate over them. So conversations question mark results.length is larger than zero.\nand then let's do conversations.results.map, and let's get the individual conversation in here. Let me just find a way to close this. In here, we're going to render a button like this, and it looks like there are no errors here. I'm not sure. Do I need... Okay, so this can still be undefined at this point, so let's add a question mark here. and let's add a class name to this button to be height 20, full width, and justify between. Key will be conversation underscore ID. On click here, we're going to set screen to chat, and we also need to call const set conversation ID, use set Adam, conversation ID Adam. So make sure you have imported conversation ID item and the only one we don't need is the error message item here. So now we can set the conversation ID item here. And let's do it first. Conversation ID. Like this. Perfect. Then now we can select previous conversations. Let's give this button a variant of outline. And let's create a div here with a class name. Flex. Full width. flex, column, gap4, overflow, hidden, text, start. Then inside another div with a class name, flex, full width, items, center, justify between, and gap x2. Inside let's add a paragraph which says chat. And the class name, text muted foreground, and text extra small. Let's copy and paste that again. And in here, we're going to do format distance to now, which we don't have the package for. So let's quickly do pnpmf widget, add date FNS. So in our widget component, we need that. All right, TurboDev once again. So format distance to now, which you can import from date FNS. So let's just do that. from date FNS. Let me remove the unused icon here. And inside of the format distance to now, we're going to call new date, conversation, creation time. Let's refresh this briefly and let's go inside of the inbox again. There we go. Chat about one hour ago, about two hours ago, two hours ago. Perfect. so now let's go outside of this div let's open a new one with a class name flex full with item center justify okay i can't write justify there we go so justify between and gap x2 and inside let's add a paragraph conversation dot last message question mark dot text and give this a class name of truncate and text small let's see how this looks so far there we go pretty good and you can see how the truncate looks like it will basically not allow this to overflow and now we can select from previous chats, as you can see, right? So if I go ahead and click this one, there we go, you can see and now you actually saw the infinite load in action. I'm not sure if you noticed. Great. So let's go ahead and add one more thing here. Which is this, this little icon, which indicates what is the status of this current conversation. So in order to do that, we need to develop a new component called conversation status icon. So let's go inside of packages, UI, source. Let's go inside of components and let's create conversation status icon.tsx. And let's import all the icons we are going to need. Right, arrow right, arrow up, and check icon. Then let's import our cnutil. And then let's create the interface conversation status props which accepts one prop status and it can be unresolved escalated or resolved and basically you get those values from your schema so make sure that they match this if you want to you can actually import dock from let's see workspace backend oh yes but we don't have backend added to the packages ui so i think it's just simpler to do this rather than risk some weird linking here by pnpm and monorepo so just make sure it matches right like this then let's do const status config like a little factory map so if it's resolved, we're going to have the icon check icon. And we're going to have the BG color to be background. And let's do three, three FB six to F. Just some green color that I like. And then let's do the same thing for the other two statuses. So for unresolved, we're going to be using arrow right icon and background destructive. And for escalated, we're going to be using arrow up icon with VG color, VG yellow 500. And let's go ahead and let's add as constant here. Const. I'm being super inconsistent here. Here I'm using the hex code. Here I'm using a variable. And here I'm using tailwind. So feel free to standardize this if you want. I just really like this green for some reason. Okay. Now let's export const conversation status icon. and in here let's add conversation status icon props let's destructure that status and inside of here let's simply return a div with a class name cn flex items center justify center rounded full padding 1.5 config background color and I didn't define the config so const config is going to be status config status and const icon will be config dot icon like this all right and inside let's just render the icon so make sure it's icon with a capital i and let's do class name size three stroke three text white there we go now we can go back inside of our widget inbox screen and after this paragraph let's add the conversation status icon component and give it a status of conversation dot status let's go ahead and import conversation status icon so import conversation status icon from workspace UI source components conversation status icon. And you can see I have no errors in this file, which means that the conversation status matches exactly the types that I defined in this component, which means that now all of them look like this. Great, except I don't really like it. Something is weird. Let's see what did I forget to do here. So this should be a little bit bigger as you can see on my screen here. They are a little bit bigger. So maybe I have a typo. Maybe I did something correctly. Let's go inside of the conversation status icon and let's see. Did I forget to do something? So size 3 stroke 3 text white flex item center justify center rounded full padding 1.5. In the widget inbox screen truncate text small. What if I added class name here, which is not accepted, so we could add it here. I have an idea, class name, optional string. Let's grab the class name here. No idea why this worked for me, but not for you. Let's add that class name here then. How about I add shrink zero here. Okay, that does not seem to be fixing this problem. Okay, conversation status icon padding 1.5. Okay, let me just debug a little bit. All right, so not 100% sure why it's behaving this way. You can try adding like padding 2 and then it seems to work. But if I you can see padding 2 is equal to 8 pixels, which means that if I add 7 pixels, it should be just slightly smaller, but you can see it's visibly smaller. So let's just try like size 5 instead of padding. That seems to be okay. Combined with our shrink 0, I think this is okay. Let me just see. Yeah, seems to behave okay. Let's leave it like that for now. Great. And now we can actually test out the infinite scroll from the previous chapter because we didn't have the chance to do it. So find the conversation where you have a lot of messages. You can see how by default, all of them are loaded, right? So let's go ahead and go inside of our widget chat screen here and find the use thread messages, change the initial number of items to five, use infinite scroll change this to 5 too and let's just add observer enabled false and then let's go ahead and refresh this and head into that conversation again and you will see that by default it will only load five messages and you have to click to load more and then it will load five more and then five more and then five more until it reaches no more items. So that's how it works and that's how you can prove it works. You can now increase the number to 10 and remove the observer enabled false. And now that entire thing will happen but automatically. You can see it automatically reaches the top if you are at the top Great Now that we know how our use infinite scroll works and we just tested it within individual conversations, let's do the same for the inbox, right? Because this inbox already has pagination added thanks to convex's pagination ops validator. So let's go ahead and quickly do that. so I'm going to go ahead inside of my we can actually copy this this is handy from the widget chat screen go inside of widget inbox screen and after we load the conversations simply add that hook like this so use infinite scroll and let's import it make sure you have added the import and let's also import infinite scroll trigger. Instead of messages, this will be conversations, just like that. And now this, instead of rendering the infinite scroll trigger at the top, like we did in the messages, in here we have to render it at the bottom, right? So after this ends, infinite scroll trigger, like this and we can just pass all the props that we have can load more is loading more onload more which is handle load more and ref which is top element ref all of these extracted from the use infinite scroll and to test this let's change this to load only two both in the paginated query here and the use infinite scroll and add observer enabled to be false. Then let's go ahead and refresh. Let's go inside of here. And by default, only two conversations are loaded until you click load more and then two more and then two more until it reaches the end. So it works here as well. And you can see that if you change this to true, then and refresh then and click here, it will automatically do that as long as this is visible, right? So only when it reaches all the way down there, it's going to be considered no longer visible and it won't load automatically. So now we can remove this, change the 2 to 10. It was that easy for us to add infinite scroll. That's why I wanted us to develop those components. You can see how useful they are. Perfect. So we can now revisit our previous chats. We can start new chats. Let's go ahead and do checkpoint. Let's go back. Let's go here. And there we go. You can see at the top, this is the checkpoint one we created. Perfect. Amazing, amazing job. Let's see if that was the goal. So we created the widget inbox screen. We created the conversation status icon component. That's it. Perfect. So 16 widget inbox. Let's go ahead and let's merge this. I just remembered, I think there is one thing instead of our Dice Bear avatar component from the last time avatar source doesn't have the image URL dependency array so let's just add it here that was the code or code rabbits review suggestion from the previous chapter so just do that as well perfect so I'm gonna go ahead and stage all of my changes 16 widget inbox that's it and then I'm going to create a new branch, 16 widget inbox, and I'm going to click publish branch. And as always, let's go ahead and let's review this pull request, fairly simple one, but still, you can see how useful it is. CodeRabbit always manages to find something that we missed. And here we have the summary. We introduced an inbox screen for the widget, allowing users to view and scroll through their conversations with support agents. We added conversation status icons for clearer visual feedback on the conversation states. We implemented the infinite scrolling for the inbox conversation lists and conversations now display the most recent message and relative creation time. Exactly what we did. And here's the bug fix. Avatar images now update correctly when the image URL changes. So that was the bug from the previous chapter. And I can't even believe it, But we did a perfect PR. No comments from CodeRabbit. We did a very good job. And well, fair enough. It was a very simple PR. So let's go ahead and merge these changes. Once you've merged them, make sure that you go back to your main branch here and synchronize the changes. So everything is up to date. And once you've synchronized your changes, as always, just double check your graph looks like mine, like this. You can see how many things we did so far. And every single time we check out, we merge, we check out, we merge. Just like that. And just a periodic reminder, just try Turbo Build. You know, no matter if it fails or succeeds, just so you are aware. Is everything going well or are things starting to fail? Just so you have some sense of how many things you're going to have to fix before you go into deploying this app. But you can see mine is building just fine. So even if you have some issues, there are most likely super simple type errors or lint errors, which you can fix just by reading the description. Great. I believe that marks the end of this chapter. So let's go ahead and see you in the next one. Amazing, amazing job. In this chapter, we're going to create the dashboard inbox. This will be quite similar as to our previous chapter, widget inbox. But this will be used to display conversations for that specific organization on the operator side. So in the previous chapter, we made it so that the user who is attempting to contact support can see all of their previous conversations. And now we're going to create this. On the dashboard side, the operator will be able to see all the conversations made for asking for support. so let's start by building the conversations layout first and let's go ahead and do turbo dev and let's go ahead and change this time to go to localhost 3000 basically our web dev task because this is our dashboard and the reason i'm telling you to open this is simply because we got so used to working with the widget so in case you didn't forget this is actually our main dashboard, right? So we are now going to be developing the conversations part right here. So we already have the conversations page, if I'm correct, if I click here, we have empty conversations, perfect. And now what we're going to do is we're going to go inside of apps, inside of web modules, and let's go inside of dashboard here. And let's go inside of layouts. And in here, I'm going to create conversations layout. The conversations layout will have all the imports from workspace UI components resizable. So handle panel and panel group. And let's go ahead and create export const conversations layout. And in here, let's define the props. So they are only going to be children. So we can destructure them. There we go. And let's go ahead and let's return resizable panel group. Let's give this a class name of full height. Whoops, height 8 full, flex 1, and direction will be horizontal. Inside, let's add a resizable panel. let's give it a default size of 30 maximum size of 30 and a minimum size of 20 and inside let's go ahead and simply render conversations and just conversations that's it outside let's add the resizable handle which is a self-closing tag and then let's open another resizable panel here and in here we're actually going to render the children and let's give this a class name of height full and a default size of 70 just like this. Now that you have this set up let's go ahead and let's actually render this layout. So we have to go inside of our web app dashboard route group inside of conversations, go ahead and add layout. It's a reserved file name, just like page. So make sure you don't misspell it. Let's import conversations layout from modules dashboard UI layouts. And in here, let's go ahead and render it. So what we can do here is we can just copy this thing. So we don't have to repeat it again. And let's go ahead and name this just normal layout and export default layout. And inside of here, we are just going to return the conversations layout and children inside. That's it. So now, when you refresh here, you will see that you have this. and you will be able as you can see to resize this so this is where we're going to render all of our items here and it seems like every single time I close this it closes itself not sure why because I think inside of dashboard layout we did have a cookie here so weird that that happens but okay it's not too much trouble but I will research as to why this is happening but let's focus on the conversations now. So the problem is, well, there isn't a problem. Basically, this is where we're going to develop this. So how about we start with this little filter status at the top. So instead of the conversations layout, we're actually going to render inside of this paragraph a component called conversations panel. So conversations panel is going to be a self-closing tag. And we're going to put it inside of components here. So conversation, conversations panel. And the conversations panel, let's just export it like this, is going to be the following. Let's go ahead and add a div. Let's go ahead and add a class name with the following class names. Flex, FullHeight, FullWidth, FlexColumn, BG, Background, TextSidebar, Foreground. Inside of this div let add another div with a class name flex flex column gap 3 border bottom and padding of two and now we have to import all components from workspace select so select content item trigger and value from workspace ui components select and once you have those you can go ahead and build the filter. So let's go ahead and add the select component here. And let's give it some attributes. The default value is going to be all on value change for now will just be an empty arrow function. And the value for now can be all inside of select. Let's go ahead and let's add select trigger. And let's give it a class name. height 8 border none px 1.5 shadow none ring 0 hover bg accent hover text accent foreground focus visible ring 0. Instead of the select trigger add the select value which is a self-closing tag and give it a placeholder of filter. Outside of the trigger, add the select content. Inside, add a select item. And now let's go ahead and give this a value of all. And inside, let's add a div and a class name, flex items center and gap two. And inside of here, let's add a list icon from Lucid React. let's go ahead and give it a class name size 4 and let's add a span with text all so just make sure you have imported list icon and while we are here how about we add all the other icons we are going to need so arrow right icon arrow up icon check icon corner up left icon these are all we're going to need for now and now basically what we ought to do is just copy and paste this a few times but I don't want to do that before we actually render the conversations panel so let's go back inside of the conversations layout which is in the layouts folder and let's just import our newly created panel from the parenting from the sibling components all right so the problem is this has to be a client component. So let's go inside of the conversations panel and let's mark it as use client. And that should fix the issue. There we go. And now you have a little select button here. So that's how it's going to look like. Now let's go ahead and let's edit some more values because right now we only have all. So I'm going to copy this select item and the next one will be unresolved. Let's use the arrow right icon and the text unresolved. Then let's go ahead and duplicate this again and let's call this escalated. Escalated and this will be arrow up icon. Let's duplicate this again and this will be the last one which is resolved. and let's give this check icon. So now you should have all unresolved, escalated and resolved. Great. So now that we have that, let's go ahead and let's build the scroll area, which is going to display our conversations. So that's going to happen right outside of this div here. So let's add scroll area from workspace UI components scroll area. Make sure you add that import. And let's give this a class name. Maximum height calculate 100 VH minus 53 pixels. Make sure to not add any space here. So when you hover over it, it should display this. If it's not displaying anything when you hover, make sure you have the tailwind CSS extension. It will help you a lot when writing tailwind. So how do I know minus 53 pixels? Well, basically, I want the scroll to start if we fill up this container. But the problem is, you can see that this is 53 pixels high, right? So basically, I just used inspect element, I found out it's 53 pixels high, and it's fixed, it doesn't change its height. So I'm just reducing that part because it doesn't know it's there. So now inside of here, let's add div class name, flex full width, flex one, flex column and text small. And now inside of here, we have to render our conversations. but in order to render the conversations we first have to develop the api to fetch them and we already have something quite similar right inside of our packages back in convex in the public folder we have conversations so let's copy them entirely and let's go inside of convex and create a new folder called private this time so instead of private is what i'm going to well be building private api routes something that should only be accessible for operators People who are logged in, people who I know that they have in their clerk session, the organization and things like that. So a much more secure API routes and also more powerful API routes because these will be able to delete things, mark them as resolved, escalate them, those kinds of things. So I want to make sure that I store them in a different folder as compared to the public folder. And this way I know, okay, so all of these are kind of public. I should be careful with them. So in here, for example, I have to be careful with what I return. Remember, I can't just return the entire thing. I have to pick and choose what I return. Same with get one here. Well, in private, I won't have to worry about that because the user here is actually logged in. They have a password, they have an email, they have a Gmail account, things like that. So let's go inside of private conversations, which we just copied here. And let's focus on the get many. So we are no longer going to have the contact session ID. That's no longer needed. because the user is actually logged in now. But what we will have now is the status, which will be optional and the union of three literals, unresolved, escalated, and resolved. Basically, the exact same thing as our schema. Make sure they match. And it's the exact same thing we will allow the user to filter by from here. So that's what it's referring to. So how do we validate inside of this now that we don't have contact session ID? Well, just by using normal authentication with convex and clerk using the context. So you can get the identity by awaiting context out get user identity. And then if there is no or if identity is specifically null, throw new convex error here, code, unauthorized message. whatever you want, identity, not found. Just like that. And now while we are here, we can also extract the organization ID. So how do you get the organization ID? Let's just double check that we actually have this. So head to your clerk dashboard and in here, go inside of configure and go inside of JVT templates here. Select the convex template, which we created in the beginning. So Clark works with convex. And in here, yeah, you can see that we have organization ID. If you don't have organization ID, I think we did this together. I'm 99% sure we did this together. But just in case you somehow missed it, it doesn't come by default here, right? So basically, you have to add like a new field and you have to call it something. and then you go ahead and choose organization, organization.id and you click save and then that will be stored in something. And then in your code, you will be able to access identity.something, right? So just do that for organization ID if you don't have it. All right, I'm just gonna reset these changes. I don't need them. Make sure you have organization ID here. That's the important thing. Great. So now we can do this and then we can go ahead and copy this again. If there is no organization ID, basically, let's go ahead and throw unauthorized again, organization not found. Great. And now we can go ahead and load the conversations, but let's do it a little bit differently. So let's define the conversations constant here and let's give it a type of pagination result from convex server, then document from generated data model, and conversations. So let me show you those imports. Document is imported from generated data model and pagination result type from convex server. Great, so now we have our conversations here already. And I'm going to remove this simply because it's honestly easier to just build it from scratch. Let me return everything here. Let's go ahead now and let's check if we have arguments.status. So if we passed the status, in that case, conversations will be the following. Await context.database query conversations with index by status and organization ID. So that's why we have to do it this way to make it actually more optimized because we added an index called by status and organization ID. So this will be unbelievably faster than if you were to actually filter by status. so basically just a small lesson in using convex the correct way you could absolutely not use the index you can just do a filter here and then request the query query dot equals and then i'm not even sure what's the api but if you can use the index use the index it is the correct and the faster way to do it so let's go ahead here and let's pass the query and let's check if query dot equals two things. So the first thing we're going to check if it equals is if status equals arguments and it needs to be a type of unresolved Let just not misspell this unresolved or escalated or resolved And that's how you won't get the error. Basically, make sure you are not misspelling it and that it's the same thing as this, maybe even the same order as this. And actually, you can also do as doc conversations status. Yeah, that works too. and it's even more precise. And let's add another equals here. Organization ID matches the organization ID from the user's session. So we know that the user actually has permission to load this conversations. Let's order by descending and let's also paginate this using arguments, pagination options. There we go. And let's also develop else, which will basically be a simpler query. So conversations, again, await context database, query conversations, but this time we're using a simpler index, just using by organization ID. So if you want to, you can explore if you can somehow conditionally choose between indexes, but honestly, this isn't, you know, too complicated, just an if else, it's fine this way, if you ask me. Let's do query equals organization ID. And let's pass in the org ID. And then we can just copy the order and the paginate. Let's just chain them. And there we go. And now what I want to do is I want to develop const conversations with additional data. So what do we need to add here? Well, basically, similarly as to our conversations.ds inside of public, where we had to, let me just find get many, similar to this, where we had to add last message, we're going to do the same thing here, but also I want to populate the contact session information. Because remember, in my schema, each conversation has its equivalent contact session. So we're going to now load the equivalent contact session populated, like give it the username or the email, right? Whatever information we have about that contact session so that we can display it in a nicer way. Because think about it. In order to display their country of origin and their name, we need to load that conversation ID from the conversation table that we have it. So let's go ahead and do that now. Back inside of conversations, make sure you are developing this in the private folder here. And let's go ahead and do conversations.page.map. Let's go ahead and mark this as an asynchronous method and let's get the individual conversation here. Now inside of here we're going to do the following thing. First let's do last message thing which will be a type of message whoops message doc or now. So I'm pretty sure this is the exact same code that we have here. And now let's also prepare the contact session here to be await context dot database dot get conversation dot contact session id now if there is no contact session found we can just return no and we don't even have to fetch the last message or anything because this is a completely invalid conversation if we can't find the equivalent conversation contact session id we can't even display this so this is kind of like us building an inner join if you're familiar with sql this would be the inner join. We're only loading conversations which have their contact session id relation and their last message relation. We are not loading any other ones. So right now we are technically kind of loading them but we are breaking that by returning null and then we'll filter it out later. So if there's no contact session return null similar to the inner join and then let's check the following. So const messages will be await support agent dot list messages pass in the context thread id conversation dot thread id pagination options same thing as in the public one so number of items one cursor null that will load the last message so if messages page length is a larger than zero it means there is a message that we found let's assign the last message to be messages.page, first page, or fallback to null. And that is, I believe, last message solved. So now from here, we can go ahead and return conversation. And let's just populate the last message and the contact session like this. Not contact session ID. We should have the contact session entirely. So this variable, that's what we are doing. Perfect. Now that we have this, let's go ahead and filter out any null values. So after this promise all, let's do const valid conversations. Conversations with additional data dot filter. Let's go ahead and get the individual conversation and let's give it a check. So cond is non-nullable. Type of conv. Conv isn't null. like this. And then we can safely return the conversations here and page valid conversations. Just like that. Perfect. So I actually want to delete get one from here simply because it doesn't belong. And create won't even exist from the private one. At least for my needs, my dashboard doesn't need to be able to create conversations, right? So only get many is the one I need from here. So we can remove this, we can remove components. And for now, we can remove the mutation. Maybe we will need it later. Excellent. Now that we have this, let's go ahead and let's add this to our conversations panel. So I'm going to go inside of conversations panel. And now let's go ahead and let's go up here. And let's do const conversations, use paginated query from convex react and in here pass api private oops i have to import api from workspace backend generated api and now let's go ahead and you can see how now we have private dot conversations dot get many in case you don't have it always double check that your workspace backend has successful functions. If there's an error here, it will probably be very descriptive and you will just have to go back in here and fix whatever is the error and save the file until it works. If it's very stubborn, maybe just try restarting the Turbo Dev thingy. All right. So now that we have paginated query here, let's go ahead and check if we want to pass in any arguments. So for example, for status, let's go ahead and just pass. Well, if we pass undefined, that will basically be served as all of them. Because inside of our private, you can see that arguments.status is completely optional, you don't have to pass it. And if there is no argument.status, we're just going to use this, an index that loads all of them, regardless of the status. And let's go ahead and pass the initial number of items to be 10. Perfect. So now we have our conversations. And now if you want to, inside of here, let's go ahead and JSON stringify the conversations. And there we go, all of our conversations here. Great. So now let's go ahead and let's actually display them in a nice way. So inside of here, we're going to do conversations dot map, my apologies dot results dot map, get the individual conversation. and let's go ahead and define some variables first. So let's check if the last message we received is from us or from the user. So is last message from operator? That is us. That is the person looking at the dashboard. We're simply going to check the following. If conversation, last message, question mark, message, question mark role is not equal to user. and then let's go ahead and let's attempt to extract the country this user is from so the way we can do this is by using get country oops get country from time zone there are of course a billion ways you can do this but this is just one of them that I found basically if you remember in our schema in the contact sessions metadata we take in the time zone. So I found a little npm package that can transform that into the country. So let's go ahead and install that package. So pnpm, let's filter into our dashboard, because that's the only place we're going to need this, so dashboard. And let's go ahead and simply add countries and time zones. no project matched in the filters that's interesting okay my apologies web not dashboard yes my apologies we why web well because apps web that's why okay so now that we have this let's go ahead and actually create this util inside of the web app let's go inside of lib here let's go ahead and create country utils.ts let's import all as city from countries and time zones which we just installed and let's export function get country from time zone let's go ahead and accept the string here if there is no time zone well we can't return anything so let's just early return. Otherwise, let's get the timezone info by using ct get timezone, timezone. If there is no timezone info, question mark countries dot length, meaning if this array is empty, also nothing we can do, break early. Then let's do country code here. Timezone info dot countries first in the array, const country cd get country and let's pass in the country code as string and let's go ahead and return the code country code and name country whoops name or country code\nThere we go. So that's our simple get country from timezoneutil. Let's go ahead and import this from libutils. So make sure you've added it. So yes, we developed it instead of apps web and then in here in the lib folder. And now inside of here, we're going to go ahead and pass conversation.contactsession.metadata, which is completely optional, so timezone. like this. And then let's do const country flag URL. For now, let's just use logo SVG, even though we don't have it here. So let's quickly prepare that because we are going to need it. Instead of apps web, go ahead and open the public folder. Quickly go inside of widget public, copy logo.svg from here, and then add it in web right here in the public. there probably is a way to share this across all apps maybe by using the packages ui public maybe in the source let's see i don't know maybe there is a way to do it but this is not complicated it works it's fine all right so now oops now i'm just using logo svg we will develop the way to get the country later. But let's finally go ahead and let's return something here. Let's go ahead and import link from next link. So just make sure you've added link from next link. Make sure you didn't accidentally import from lucid icons because that can happen. The href is actually going to be forward slash conversations and then conversation underscore id. So whenever user clicks on one of these, that's where it's going to redirect. Now let's go ahead and let's add the key to be conversation underscore ID as well. Let's make the class name CN. So we're going to have to import CN. If you haven't, make sure you do. So CN from workspace UI lib utils. Now inside of here, let's go ahead and do relative flex cursor pointer items start gap three border bottom, padding four, py five, text small, leading tight, hover, pg accent, hover text accent foreground. And then let's go ahead and let's check if path name, which we don't yet have, so let me just quickly add it. I always forget to add a path name. So in here, let's just do const path name, use path name from next navigation. That's it. That's all we need. Let's go back to our check here. So if path name is identical to this. So if we are currently selected, in that case, let's just quickly add BG accent, and let's add text accent foreground. Basically, we're we are doing it as if we were hovering over it. So it kind of indicates, okay, this is selected. I think you can actually see that here. Let me just show you. You can see how this one is hovered. It's selected. That's what that is. All right. Now, instead of this link, let's go ahead and let's render something. So let's add a div. Let's go ahead and give it a class name, which is going to be dynamic. So I'm going to add minus translate y one and a half absolute top 1.5 left 0 height 64% width 1 rounded r full bg neutral 300 opacity 0 transition opacity and then simply copy the path name check again in that case go ahead and add opacity 100 so what is this this is super weird and also it's a self-closing tag so what is this weird div it's not visible right now but basically i'm not sure if you can see but this little indicator that something is selected that's what this is You're going to see it in a second. Now, outside of this, after this self-closing div, let's add the DiceBearAvatar component, which we developed in the previous chapter, previous chapters. So workspace UI components, DiceBearAvatar here. And let's go ahead and let's pass the seed here to be conversation, contact, contact session dot underscore ID, or you could have just used the contact session ID. Like that. And let's give it the size of 40. Let's give it class name shrink zero. Let's refresh now. Oops. Turbo dev. Let's refresh and let's see if we can actually load something. And there we go. So for each contact session that you have, you should have a different glass icon. And when you hover, you can see how nice it looks. Great. So now that we have this, let's go ahead and open a new div. class name flex1. Let's open a div class name flex full width items center and gap two. And then in here, let's go ahead and render a span conversation.contactsession.name. And let's give this a class name of truncate and font bold. There we go. And now you should see the name of your contact session, whatever that was at the time of you creating it. Great. So let's go ahead and open this again. Class name, ml auto, flex, actually, it's just shrink zero, and then text muted foreground, and then text extra small, format distance to now. We already used this somewhere, but I fear that it was in the widget, right? which means that we have to install another package here. So let's do pnpmf web add date FNS. So basically filter to the web app and add date FNS. And let's actually check the date FNS version. So it's this one. So I just want to add the same one to avoid any conflicts between my packages here. There we go. looks like all is well and now I should be able to import format distance to now from date FNS let me just do that here from date FNS great and inside I'm going to pass conversation creation time so let's check that out there we go so about six hours ago eight hours ago one day one day ago great now let's go outside this span outside of this div and let's open a new div with a class name margin top one so we separate the bit from the creation time above and let's do flex items center justify between and gap two inside of here let's add another div with a class name flex with zero grow items center and gap one. And then let's check is last message from operator. If it is, we're going to add corner up left icon. We're going to give it class name size three, shrink zero and text muted foreground. Basically, what is this? This will be a nice little indicator for us so that we know that we sent the message back. So when we are just looking at it like this, we know that we have responded to all of these conversations. Well, in our case, the AI responded. But still, same thing. Basically, the user wasn't ignored. The user received a response. And then let's add a span here. Conversation, last message, question mark text. there we go and now let's render it in a nice way shall we so let's give this a class name cn line clamp one text muted foreground and text extra small already looking much better and now let's go ahead and check if not last is last message from operator. So if the last message came to us and we didn't respond to it, let's go ahead and mark it as font bold and text black. And just for fun, you can change this to true just to see how it's going to look like. So it's going to be looking like this when we didn't respond to the user, basically as if we didn't even see that message. So it's going to be much more visible like, hey, you didn't respond to this user. If this little indicator wasn't enough. It's also going to be very bold. So you're going to see, okay, this user is waiting for your answer. Excellent. And now outside of this div, let's go ahead and reuse our conversation status icon. We can import this. Let's go ahead and just do that here. So import conversation status icon from workspace UI components, conversation status icon. We developed this in the previous chapters. let's add status here conversation dot status and let's see there we go already looking much much better and now let's go ahead and let's try the following let's try and create the flag for each of our customer here because right now we're only using the country but we aren't actually generating the badge thingy so that we can display where the user is from. So let's go inside of country utils where we develop the get country from timezone. And just below, let's do export function, get country flag URL, country code string. and let's go ahead and generate the country flag by using an amazing free service flagcdn.com. Let's go ahead and define the width 40 and then inside of here let's pass in the country code to lowercase Literally that simple to get access to all the flags in the world so truly an amazing service and now let go ahead and go back inside of the conversations panel and let actually use it So we're going to check. If we were able to obtain country code, let's use the get country flag URL, pass in the country code, otherwise undefined. So make sure you have imported the get country flag URL. and now let's go ahead and use the country flag url constant just above here on the dice bear avatar and let's pass in badge image url country flag url and if it's undefined it simply won't be rendered and there we go you can see that i'm from croatia so all of my requests here i are made with the creation flag. Amazing. And when you click on it, I'm pretty sure it's going to be 404. But still, it basically works. It redirects you somewhere. Amazing, amazing. So in here, it seems like we don't have the message probably because we were just testing the conversations in the first place. But I am super happy with how this turned out. And now let's enable the filter because that's currently not working. So there's a very easy way we can do that by using local use state. But I imagine users wanting to use this, for example, they want to set this to escalated, and they will pretty much use escalated all the time, right? So I don't want them to have to go to their app every single time and change to escalated. So how about we use a package, a state management that we already have in our widget app, Yotai, and just use it here and use the atom with storage, which will automatically save this value in the local storage so the user doesn't have to track it. So let's go ahead and quickly check what version of Yotai are we using in the widget. So it's this version. Let's go ahead and do pnpm fweb add Yotai. Whoops, that's not how you add that. There we go, like that. And that should add the exact version, I believe. There we go. Now let's go ahead and let's go inside of apps, web. Inside of web, let's go ahead inside of dashboard, module. Let's add constants.ts and I will export const. let me just call this status filter key cva or actually call it echo status filter like this and then let's go ahead again back inside of the dashboard here and let's create atoms.ds let me close everything else so we can actually see what i'm doing inside of apps web modules dashboard atoms. Let's import atom with storage from yotai utils. Let's import doc from workspace backend generated data model and let's export const status filter atom, atom with storage. And let's go ahead and give it the key of status filter key. And the default value. So you can set this to be all. And now let's just make it properly typed. So open these and you can go ahead and add document conversations status or all like that. Perfect. And now that you have this, let's go back instead of the conversations panel. and let's go ahead and add that atom here. So we are already familiar with this. So const status filter here can be use atom value from Yotai React and let's use status filter atom and const set status filter use set atom status filter atom like this. And then let's go ahead and give the value of this to be status filter. And on change, let's get the value here. And let's call set status filter and pass in the value. I'm just, yeah, it just won't be compatible here. Okay, so let's just do value as and then pass all the possible options unresolved escalated resolved and also all because we do have an item that is all and inside of here i mean inside of the status filter atom we also allow all because remember all isn't the status so in our case all means just load all of them. And now that we have this, we can actually go ahead and modify the use paginated query here to check if we have the status. So let's go ahead and check. If status filter is equal to all, in that case, it will be undefined. Otherwise, status filter. Yeah, just pass status filter. That's it. And I'm not sure if this will throw errors or not. So by default, when I hover over status filter, it could be unresolved, escalated, resolved, or all. And in here, if I check if it's all, it's undefined. Otherwise, oh, this is super cool. TypeScript is very smart. You can see, yeah, by default, it includes all. But if it's in this conditional case, it automatically removed the all type. I am actually impressed by TypeScript. Very cool. So right now, all of mine are unresolved. so if I change this to escalated, it's empty. Same thing with resolved. Only unresolved should actually load something. So you can actually go inside of your conversations here, pick one and just change it to be resolved and change some other one to be escalated. And let me repeat this resolved. And there we go. You can see how now you have more variety here. Amazing. Great. perfect so let's go ahead and now add infinite loading here simply because it's super easy to do and we already have all the components that we need so let's go back instead of the conversations panel and just below the use paginated query here let's go ahead and let's add use infinite scroll like this now we have to import use infinite scroll so let's just quickly do that here at the top somewhere. So from workspace UI hooks, use infinite scroll. And let's also add infinite scroll trigger from workspace UI components, infinite scroll trigger. Great. So now that we have this, let's go ahead and do the following. So inside of here, we're going to add status to be conversations dot status and conversations load more for load more. And load size will be 10, same as my initial number of items. And then in here, let's go ahead and let's destructure all of these things. Top element ref, handle, load more, can load more, is loading more, and is loading first page. Great. And now let's go ahead and actually add the infinite scroll trigger. So we're going to go all the way down. After we finish this iterating over the conversations, still inside of this div, Let's add the infinite scroll trigger. It's a self-closing tag. And let's simply go ahead and pass it. Can load more, is loading more, handle load more, and top element ref. And by default, you for a second just saw no more items, which will basically appear at the bottom here when you reach the end of the list. So as always, in order to test if this is working correctly, I recommend that you change the initial number of items and the load size to maybe like five and go ahead instead of the use infinite scroll and disable the observer. Refresh and there we go. So now you have to click load more to load the previous five, which is basically what's done automatically by the default settings. There we go. Amazing. So I believe that's all that we wanted to do here. What we can also do here is we can use the isLoadingFirstPage to create a nice loading element here. So let's go ahead and quickly do that. So right here, before we enter the scroll area, let's check. if is loading first page loading otherwise render the entire scroll area like this and let's indent this thing so now while it's loading first page you will see a brief loading element here and now let's go ahead and let's actually build this loading element. So I'm going to go down here and I will do export const skeleton conversations. Let's go ahead and let's return a div. And basically, this div here will be pretty much, I think, the exact same thing. Actually, not exactly. Okay. So let's give it a class name. as the following flex minimum height zero flex one flex column gap two and overflow auto then inside let's create another div and let's go ahead and give it the following classes relative flex full width minimum width of zero flex column and padding two and then let's go ahead and let's open one more div, which will actually be the container. And now we're going to render the items inside. So with full space Y2 and text small, we actually don't need the text because there won't be any text. Now in here, let's go ahead. Oops, not like this. Let go ahead and do array from length8 skip the first element get the index and let return a div here and let's close it. And now in here, let's go ahead and do class name, flex, items start, gap 3, rounded large padding four and last border bottom zero actually just this is enough let's go ahead and give it key of index and inside let's add skeleton from workspace uicomponents skeleton so just make sure you have added this import and let's go ahead and give this a class name height 10 width 10 shrink 0 rounded full that's our avatar let's create a div here class name minimum width of 0 flex 1 inside another div so I'm just recreating the how this looks like but in skeleton let's go ahead and give this flex full width items center and gap two skeleton class name height 4 width 24 open this again ML auto height of 3 with 12 shrink 0 like that and just one more class name margin top of 2 skeleton class name height 3 with full and let's use the skeleton conversations now here. So instead of this, let's just render skeleton conversations. And if you want to look at it more, you can set this to be true. And there we go. I think this looks pretty cool. And you can change this now to be is loading first page. And there we go. Now you're going to have a very nice loading mode. Excellent. Very, very cool. All right, let's go ahead and merge this in the meantime in between the chapters I'm going to debug why this is happening when we open it it should stay opened so kind of weird that that's happening but sure we are going to fix it great so let's go ahead now and merge this so let's see we added this we added this and this great and now let's go ahead and merge that so 17 dashboard index I'm going to stage all of my files 17 dashboard index let's commit let me open the new branch 17 dashboard what was it dashboard inbox yeah and I will publish the branch and now we're going to go ahead and see if we made any security issues in our code or any bugs in general. And here we have the summary. We introduced the conversations panel with infinite scrolling, filtering by status, and enhanced display including avatars, country flags, and message indicators. We added a resizable dashboard layout with a dedicated conversations panel and content area. We implemented a status filter for conversations with persistent storage. We enabled paginated and filtered fetching of conversations, including last message and contact details. Awesome. So as for the comments here, we were actually quite good. The first comment we get here, and you can see how much research CodeRabbit actually does. So in here, it actually compared my convex generated types, and it found out that the message role can be user assistant tool and system, which is true. But in here, what I do is I just check if we are not user. And CodeRabbit says, you should be careful that you should check if it's assistant. Well, what CodeRabbit doesn't know is that technically, whether we responded as the operator, or if our AI robot responded, both of that is considered the operator. So even if the system or even if the tool responds, all of that is the operator because all of that is coming from our side the dashboard side so technically this is actually true we should leave the code as it is because the only person who could be on the other side is the user and in here it added potential time zone parsing errors yeah we could do that for the country yeah I see so I should check this outside of calling this function okay yeah I'll see I'll make sure we do that in the next chapter here but for now I think we're good let's go ahead and merge this pull request right here after we have merged it let's go ahead and go inside of main and let's click synchronize changes either here or here in the lower corner you can do that and once you have synchronized your changes make sure that your oh I just realized I said index not inbox my bad perfect so I believe that marks the end of this chapter. Amazing job and see you in the next one. In this chapter we're going to implement dashboard chat giving the ability to our operators to intercept the chat as humans and write messages to the user. We're also going to do some things from the previous chapter that we noticed like the sidebar state not working and also one thing that I know we forgot which is Yotai provider. So let's go ahead and let's first fix the invalid sidebar default state, which is actually a very funny bug. So if you do Turbo Dev and go inside of your web app, so localhost 3000, you will notice that this sidebar is not exactly behaving as it should. Basically, it seems like it's always collapsed, almost as if we added a variable that it's collapsed. because right now when I expand it, when I click here in its border here, now it's expanded. Great. And what I expect to happen is that the cookie changes and that when I refresh here, it should stay expanded. But that's not the case. And I actually figured out what it is. And it's quite funny. If you go inside of web and then go inside of modules, dashboard, UI, layouts, dashboard layout. The first thing I thought was maybe this cannot be an asynchronous component because I know that layout, which is the reserved file name in the dashboard route group, this can be asynchronous. So maybe I thought, oh, maybe this needs to be asynchronous and then this can be asynchronous, but that's not the case. This doesn't have to be asynchronous. This is just fine. The problem is literally in the constant. Yes, you can see that the sidebar cookie name is sidebar state. And we thought that it was a good idea to export this so we don't misspell it accidentally. But try and replacing this with a string instead. So the exact same thing from that constant and refresh. And now it works. Try and collapse it and refresh and it will stay closed. Expand it and refresh and it will stay open. So the bug is quite funny. Looks like there is something weird happening in runtime, in SSR, because of the turbo repo, mono repo structure, because we are technically importing this constant from a package of ours. And maybe it's not quick enough by the time server-side rendering fires, because this is exclusively for server-side rendering needed, right? So I'm not too smart to tell you why this is happening, but I'm 99% sure that this would not happen in production when we actually build the app. I'm 99% sure this is development only bug. So yeah, not so super fun, but at least we know it's very easy to fix. Let's just bring this back to this and let's add a little comment. Using sidebar cookie name from sidebar component does not work due to monorepo and SSR. Let's go ahead and do that and remove the sidebar cookie name. This way we at least know why we have to add string like this. Great. Now, let's talk about the other thing that I wrote here. Add your type provider. So what is this? In the previous chapter, instead of our conversations panel component, we have this select component which uses the status filter atom and we just added Yotai for the first time instead of our web app because we used Yotai instead of widget but take a look at how widget works. Instead of components providers we have this provider from Yotai but in here in the web app, we never added that provider. So why does Yotai work? Well, looking at Yotai's documentation for Next.js, we have the explanation. By default, Yotai uses implicit global store to keep track of Atom values. This is what is referred to as providerless mode. This becomes an issue in SSR scenario because this global store is kept alive and is shared between multiple requests, which can lead to bugs and security risks. So that's why it is preferred to import the provider from Yotai. Now, as far as I know, we're not going to be using Yotai anywhere outside of the dashboard. So what we can do is we can go inside of our dashboard route group, and let's go inside of layout, and let's open the dashboard layout here. And let's simply add the provider here in Yotai React. And we can actually add it even further. Let me just do this. We can add it like here. Yeah. Inside the organization guard. I do want them to be above the provider from Yotai. So like this. Auth guard, organization guard, and then Yotai provider. And let me just check. Inside of my widget layout my apologies providers I import provider from Yotai So I going to import from Yotai here as well not from Yotai React And I don think anything should work any differently now. We are just no longer using the providerless mode, right? So now when I refresh, this should still stay escalated. Exactly. So we make it easier for our users so they don't have to constantly switch between the one they prefer. Awesome. Now let's go ahead and let's develop the chat. So the first thing I want to do is I actually want to resolve this because we have this weird conversations page that isn't actually displaying anything and it's super easy for us to add something here. So let's go inside of apps, web, modules. Let's go inside of dashboard, UI, and let's go ahead and create views. And then in here, let's go ahead and let's create the conversations view dot dsx. And let's go ahead and import image from next image. And let's export export constant conversations view. Like this. Inside of here, let's go ahead and return a div with a class name flex full height flex1 flex column gap y4 mbg muted. Inside another div with a class name flex flex1 items center justify center and gap x 2 let's render the image component with an alt of logo height of 40 width of 40 source logo svg and let's add a paragraph echo class name font semi bold text large like this. Now that we have this conversations view, make sure to save it. And then go inside of your apps web app dashboard conversations page.tsx. And in here, you can just do conversations view. As simple as that, because that's the only thing this page will display. I'll explain why in a second. So when we don't have any conversations selected, we don't really have anything to display here. So why don't we just display this kind of placeholder text? You can of course choose if you want to display a state like select a message or start a conversation, something like that. Only when we click on one of these are we going to render a different page. So let's go ahead and do that now. So inside of conversations, let's create a new folder, conversation ID. the capitalization here matters so be mindful of that and add page.tsx inside and let's go ahead and return the page div conversation id like this and now when you click here you should no longer be having any errors instead you should see this now has been replaced with conversation id why does this work and what if yours doesn't if yours doesn't work head back to modules dashboard UI layouts and let's go inside a conversations layout and head into the conversations panel so in here you have to find where you iterate over your items here so conversation results.map and take a look at the link component it should lead to forward slash conversations and then your conversation id so that's exactly what we just created let me close things inside of app dashboard we have conversations and then we just created the conversations id which can be anything this is how you create a dynamic params inside of next js so that's why it should work for you now what did i say why did i say that the capitalization matters the reason it matters is because of this if you want to extract the current id you're going to have to add the following params, which are a type of promise. And then inside conversation ID, which is a type of string. And then in here, you can destructure the params. So you can see how I spelled conversation ID with capital I. It is because of how we named it in the folder. If it was named something else in this folder, you would also have to extract it like that from here. that's what I say conversation capitalization matters unfortunately this is not strictly typed yet I think that maybe you can do it with some experimental next to yes features but right now you can type anything here and it won't throw you a warning so you have to manually confirm that you did this correctly and then you can mark this as an asynchronous page and then you can destructure the conversation ID by using await params and then render the conversation ID. And there we go. Now we can see that each of your selected conversations, which by the way, looks super cool when selected, also displays the proper conversation ID. And now I think you already know the flow of what we have to do. We have to pass this conversation ID to our newly created conversation ID view, which we're going to create soon. And then we're going to have to fetch the convex API. So how about we do the complex part first and develop inside of packages, backend convex, private conversations.ds. Let's go just above our get many here and let's export const get one. And inside of here, let's go ahead and define the arguments to accept conversation ID, which is a VID of conversations. And that's the only thing we need actually. And then let's go ahead and define the handler. And let's go ahead and verify that we are logged in and that we are within the proper organization. For that, we can literally copy the things from above, from below my apologies, like this. Let me just indent it properly. So using context.out we can get the identity. If the identity is missing we are not logged in. Then we can extract organization id and then we can throw a new convex error here. Now let's go ahead and let's get the conversation by using await context database get conversation id using arguments.conversationid. And now if there is no conversation let's go ahead and let's throw new convex error, code not found, and a message conversation not found. Now let's check if conversation organization ID is different from the one our user is currently logged in with. In that case, we can also throw back a convex error and we can pass the code unauthorized with the message invalid organization ID. Let's do it like this, like that. Great. And now let's go ahead and let's add the contact session relating for this conversation. So very similarly, contact session is a wait context database get from our now proper at this point, you know, completely valid conversation object dot contact session ID. And if there is no contact session ID, let's also throw an error here. So not found, contact, session, not found. Finally, let's spread the conversation and let's append the contact session object. Not ID, contact session. There we go. So now we are safely loading conversation only for this organization ID and we are only returning it if we found a matching contact session so that we know the user's info. Excellent. Now that we have this, let's go ahead and let's display the conversation view component. So let's go ahead and close our back and package. Let's go inside of apps, web, app, dashboard, conversations, conversation ID page. And inside of here, let's go ahead and let's return conversation view. My apologies, conversation ID view, which we don't yet have. And pass in the conversation ID, conversation ID as ID using workspace back and generated data model, conversations. Now let's develop the conversation ID view. So this will be located inside of modules, dashboard, UI, views. Conversation ID view dot TSX. Let's mark it as use client and let's export const conversation ID view like this. Let's go ahead and destruct conversation ID from here. And let's go ahead and type that conversation ID. is going to be a type of ID conversations just like this and let's go ahead and let's return a div client view because this is a client component. Now go back here and you can now safely import conversation ID view and your types should be working. Perfect and there we go client view. Excellent. So now let's go ahead and let's add our const conversation use query from convex react here. And let's pass in the API private conversations, get one and pass in the conversation ID like this. And now in here, let's go ahead and do JSON stringify conversation. And now for each of your conversations here, you should be able to see. Looks kind of weird, but basically here it is your JSON object of that. Great. And now let's actually develop it so it looks like something. So I'm going to go ahead and give this div a class name here, flex full height, flex column, bg muted. Inside of here, I'm going to create a little header. So let's give this a class name flex items center justify between border bottom bg\nbackground, padding 2.5. Inside of here, I'm going to add a button component, so make sure you have imported it. And I will simply add more horizontal icon from Lucid React. Like this. Let's go ahead and give this a size of small and a variant of ghost. And let's see how that looks like. There we go. So it should be exactly the same height as our neighboring element. That's why I specified this specific padding here because I believe inside of the conversations panel, I'm using the exact same thing here. Or at least it ends up being the same thing when combined with this div and select inside of it. Maybe the smarter idea would be to literally write the height value, but I managed to do it with padding. so I'm going to keep doing it with padding. And let's just keep this component like this for now. It's not going to do anything smart. Now what I want to do is I want to import all of our AI-related components, which we added in some previous chapter when we were developing the widget chat. So let's start by adding AI conversation, content, and scroll button from workspace UI components AI conversation. These are the ones we added manually and then we had to install the packages, if you remember. So make sure you have this from AI conversation. Now from AI input, add the input button, submit, text area, toolbar, and tools. And now let's add from AI message, AI message and AI message content. And now let's go ahead and let's add AI response from AI response. Now let's add all the form fields. so form and form field from workspace ui components form let's go ahead and let's add zod let's keep them all together here so zod and let's do a react hook form it looks like we are having all we don't really have this here because we only use them inside of the widget not sure if you remember and we had some problems with that so hopefully this time it will be easier for us to do this but I think this is okay for now let's go ahead and let's try and install this so save this file and let's take a look so search for zod and let's see so we have zod 3.25.67 so I'm going to copy this because it's the working one and I'm going to do pnpm f web add zod at 3.25.67 instead of my web app right here so this should hopefully add the exact same version of zod instead of web. Perfect. Now let's do React hook form. So this is 761.1 with the caret. So bnbm fweb add React hook form at this version. So now it should be the same thing. It should work just fine. Let's refresh. There we go. And we also have resolvers, hook form resolvers 5.2.0 so now let's do this as well pnpmf web add there we go hook form resolvers and let me add this specific version hopefully everything will work out fine if not we might have to like delete node modules or something but yeah all of our versions are now the same so at least that is a good thing and no more errors here so let's continue developing let's define the form schema here just above the conversation id view. Form schema will simply be a message with a minimum requirement of one and message is required error as simple as that. Now let's actually define our form here so const form will be use form like this inside of here add a resolver which will be the zod resolver and pass in form schema. Default values here are going to be message and just an empty string. Now let's go ahead and let's add a type here z infer type of form schema. And now let's go ahead and let's import the zod resolver because I forgot to do that. There we go. So now that we have the zod resolver set and all of this, let's go ahead and create a little submit method here. Const on submit will be an asynchronous method with values which are going to be the exact same type as this. And for now, let's just console log the values because I don't think we've created the create method just yet. Perfect. And now we can go ahead and we can start developing this. So let me just close this. Whoops. Let me close this. And just in case, I'm going to do TurboDev just to convince myself that these packages didn't cause any problems because they were quite, they caused quite a bit of a problem in the widget component. But hopefully not now because we added correct versions. Anyway, let's go ahead now outside of this div, which we can actually call header. So it's visually easier to separate them. Let's start the AI conversation component. And let's go ahead and give it a maximum height, which we can do because we know what is the height of this header? How do we know that? Well, remember in the conversations panel, I did the same thing here. So I know it's 53 pixels. So we can actually copy this exact class name here and just edit here. But actually it won't be 53 pixels because we also have to account for the, let me show you. We also have to account for this part. So I already know the height of this. So altogether, it will be 180 pixels. I just don't want you to think like I'm where I'm pulling these numbers from. Well, I know them in advance, right? So let's add AI conversation content. And in here, let's go ahead and let's, we have to load the messages now. Yes. So let's see. I think we're going to have to develop. I'm trying to think if we can reuse any of our existing messages. but I think the best way would be to create a new hook. All right, let's quickly fix this. Save this file, leave this error, close everything so it's simpler. Go inside of packages, backend, convex, and go into public and copy the messages and paste them inside of private here. So now you can leave the create because we will technically need it, but let's focus on the get many now. So in here, we need thread ID and we don't need contact session anymore. Instead, what we need is our usual verification. So let's just copy this one and the organization ID thing like this. So replace the contact session in your newly copied private messages, get many, remove this, add this instead. So we check if we are logged in and if we have the proper organization ID here. And now inside of here, let's do some additional check so const conversation that we are trying to load the messages from will be fetched by my apologies let's just query conversations and let's just do with index by thread id query query equals thread id thread id and where can we get the thread id from oh arguments I forgot that we have it here okay thread id and unique because there can be only one combination in case this one does not exist something is wrong so we should not be able to even load these messages in the first place so let's throw back an error not found and let's say conversation not found and now let's check if conversation organization id does not match the current organization ID that this user is logged into. And let's go ahead and throw in unauthorized invalid organization ID like that. And then we can safely pass the paginated here. Now, I'm not sure if there is a way to pass in that user ID from here. If you remember when we, let me try and find conversations.ts in the public folder and create so in here when we create our first thread we actually pass in the user ID here so even if we didn't do this validation here we should somehow technically be able to reuse that user ID which as you know we are having the organization ID for, right? So we could just technically use this organization ID and then somehow pass it here, user ID, like that. And then it should only load that. But I have to explore the API. I'm not exactly sure if we can do that. I'm going to leave it like this for now because our validation is more than enough. We check if the organization matches. We check if the conversation exists. We check if the organization is present. all from this auth user identity, which is not passed by the client, right? So we are more than secure when it comes to this. So let's now go back inside of our conversations ID view, conversation ID view here. And now in here, we're going to have to add our messages. So let's do that just below the conversation. const messages will be use thread messages and you can import use thread messages from convex dev agent react. Let me see, do we have this here? We don't have this. All right, so we need to import two UI messages and use thread messages from convex dev agent. As always, let's check what's the version we are on, this one. So I'm going to copy this and let's do pnpm fweb add convex dev agent at this version. And that should resolve the issue. There we go. So now perfect works Great So now we have used thread messages here And let go ahead and pass in API which we just added Now let's check, do we have the conversation from above? Let me just copy it like this. So conversation.threadID. If we do have it, let's pass in the thread ID here. otherwise skip and let's add the initial number of items to be 10 and now we have our messages, perfect so now we can iterate over them so let's go back inside of our AI conversation content here and let's go ahead and open to UI messages which we imported from above messages.results or empty array, question mark dot map, and then get the individual message. Let's return AI message component here. Let's go ahead and give it a from property here. So if message dot role is equal to user, in that case, we are the assistant. Otherwise, we are the user. I know this is a little bit confusing, but let me add a comment to explain. in reverse because we are watching from assistant perspective. So all messages that are coming from the user should be labeled as assistant because then they're going to be rendered in the opposite bubble. Let me show you here. So usually when you see a white bubble, it means that someone is responding to you. The assistant is responding to you. But since we are the assistant in this case, we have to reverse that logic. I hope this will make more sense later on when we compare both the widgets and the operator dashboard. So you'll see why we have to do this. And the key will be message ID. Now let's add AI message content. AI response. message content. Outside of the message content, check if message role is equal to user. And if it is, add the DiceBearAvatar component, which is a self-closing tag, by the way. So DiceBearAvatar from Workspace UI components DiceBearAvatar. And let's go ahead and let's pass in the seed to be conversation, contact, session ID. And let me just see. So yeah, yeah, this is fine. Seed size is 32. Yeah, okay. Because technically this can be null. Okay, let's just for now fix it like this. user. All right. And then let's go ahead and add AI conversation scroll button like this. And let's see how this looks so far. Let's head to our localhost 3000. Let's select one of the messages here. Maybe refresh in case you've restarted your server in the meantime like I did. so it has to rebuild. And there we go. Looks very nice. And you can see how all the messages coming from the AI chatbot now look like we sent them because that's what we are. We are the assistant, right? So that should look like we are sending those messages. Great. I'm very happy with how this looks. So the user's message seemed like they are the assistant, but we are actually the assistant, right? It's confusing, I know, but it makes sense when you look at it because we are the operator, we are the dashboard. And now let's go ahead and let's develop outside of the AI conversation here, not paragraph. Let's add a div. Let's give it a class name of padding2 and let's develop the form. Let's go ahead and let's spread the form constant, which we defined above. and then let's use AI input again because this is underneath a form element that's why let's go ahead and give it on submit here form handle submit on submit method so you should have the on submit method which simply logs the values and then in here let's add form field which is a self-closing tag and now we're just doing the chat cn form composition so pass in the control form.control. Disabled will be if conversation.status is equal to resolved. And we can just add a question mark here. Name will be message. Render will be field. AI input text area, which is a self-closing tag. And now in here, let's just add some props here. So let's see. It will be disabled if conversation question mark status is resolved. Or if form form state is submitting. Or let's add a to do. Or if enhancing prompt, which we don't yet have, but we will have. On change here will be field dot on change. on key down will very simply be the exact same thing that we have inside of our widget chat screen so head inside of your apps widget modules screens widget chat screen find the form field AI input text area on key down and just paste it here so you save yourself some time placeholder here conversation question mark status is equal to resolved. This conversation has been resolved. Otherwise, type your response as an operator like this and value will be filled dot value. All right. Now inside of here, but still inside of the AI input, add AI input toolbar, AI input tools, and inside of here, AI input button. Now the button will have VAND2 icon from Lucid React and enhance text. Just make sure you added the VAND2 icon and we already have AI input button imported here. And then outside of AI input tools, add AI input submit. I think it is a self-closing tag, yes, but we need to add some props here. So it will be disabled if conversation status is resolved. Let's add a question mark here. Or if not, form state is valid. So if the form state is invalid, if we haven't typed anything in yet, or if form state is submitting, and or if is enhancing prompt, which we don't yet have. Status here will be ready. Type will be submit. And now you should have a beautiful input here and the button to enhance the prompt, which we don't yet have. And in case this button is getting in the way, you can try and hide it for this session like that. Now let's head back inside of our messages, which we started developing. So backend convex private folder messages.ds. And we already have the create method here because we copied this from the public folder, right? But let's slightly modify the argument. So we are no longer going to need the contact session ID. And we are no longer going to need the thread ID. Instead, we are going to need the conversation ID, which is a type of ID conversations. Let's just fix this. There we go. Conversation ID. And let's quickly copy the authorization checks from here. And yeah, you can definitely abstract this and create a method to reuse this. Convex even offers you something called convex helpers, which allows you to create a row level of security. but that's something I will explore for a different time. For now, I just want to focus on creating the project. So I'm going to remove this check for the contact session and I'm going to replace it with identity. Make sure you check for organization ID as well. And then let's go ahead and let's check for the conversation here. And CSU, one thing we should probably change is this is a mutation. If you're wondering how come this is a mutation and the one from the public folder was an action, well, because in here, we are not actually going to be using the support agent at all. In here, we are responding as an operator. We are not generating an AI message. So in here, we can actually do this. Instead of using this internal query, we can remove this and just use the normal query. And this will be arguments.conversationID. And then if the conversation is not available, not found. And then let's just quickly check one thing here, this basically. So we don't have to type it again. After we confirm that the conversation exists, confirm that we have the matching organization ID. Are we allowed to send messages in this conversation? And then the same thing here for if the status is resolved, we shouldn't be able to send any messages. and then in here let's go ahead and let's instead of using ai to save a message let's just save a human message and you already know how to do this believe it or not remember instead of our conversations.ts in the public folder when we allow the user to create a new conversation we do the usual session check blah blah blah and then look at what we do we manually create a message so we already know how to do this let do it i just going to copy this I going to go back inside of my messages here So I working in the create method of the convex private messages.ds and paste it here. Let's import save message from convex dev agent. Let's import components from generated API. Remove the internal. We no longer need it. the thread id comes for conversation.thread id like this and now in here let's add agent name and let's go ahead and pass this as identity.family name this doesn't really matter but it could be a good idea for you to already keep track of which agent responded right because agent name is a property that exists within the message component itself. But we are not really differentiating between agents. We are kind of considering the entire organization an agent, even though it can have multiple operators. We never really showcase their image or their name. But internally, if you want to, you can keep the agent name here. You probably don't even need this field at all but I'm just giving you an option to do that if you want to. And for the role in here it will be assistant and the content will very simply be the prompt. And let's just see what's wrong with my prompt here. What is my prompt? Oh my prompt doesn't exist. My prompt needs to be accessed through arguments. There we go. Perfect. So now this is my save message method here. Let me just add a little to-do. Check if agent name is needed or not. But for now, yeah, you can leave it as identity family name and then this will literally be the logged in user's name like Antonio or something like that. But we are not displaying this anywhere. That's why I keep saying that it's not super important. Nor are we using it for literally any other thing. Now let's go ahead and use this create method. So let's go back instead of the individual conversation ID view here. And let's go ahead and do const create message here. Use mutation from a convex react API private messages create like this. Make sure you have added use mutation here. And I think that you already have the API. And now let's go ahead and let's add this. So what we now have to do here is open try and catch. Let's do await, create message, pass in the conversation ID and prompt will be values.message and then do form reset. And in the catch, let's go ahead and let's do toast but we have to import toast from Sonar. Okay, for now, let's just do console error, error. Like this. And now that we have on submit, I think that that's all we have to do actually. So go ahead and just pick a random chat and try and respond. This is a human response. There we go. And you can see how no AI response is being triggered here because we have to trigger it through code right when we are looking at back and convex private messages here all we do is we save the message here but for example when we do messages.ts in the public folder so click public here or just go inside of packages back and convex public folder messages when we do create here it's a different thing it's an action we validate the user's session here. And then what we do is we basically tell the agent what to respond with and we give it the prompt here. So we call support agent, all right, generate some text back. And if you're wondering, well, how does the user's message get saved then? Well, this convex component handles that for us. So when we call generate text, it will both save the message and it will respond the AI message. But in this case, we only want one of those things to happen. So we extract the save message from the convex agent component and we just save it still as an assistant because in this case, yes, we are the assistant in this case. We are not the user. And let's just double check this, you know, by opening our localhost 3001. And let's just pick one with a working organization you can see I have it saved in my URL here so you know the deal already make sure you have a working organization ID in your widget or visit dashboard clerk comm and then in here click on the organizations tab mine is loading slowly now but basically click on the organization stop and then you can find the organization ID so let's check things here there we go this is a human response so if I go ahead and combine these two you can see that hello I can respond and you can see how this looks like I am a human but still if I say hi there I will still get an AI response and you can see how it appears as if we sent that that's exactly what we want but what's actually supposed to happen is that when the operator responds the AI should be turned off and we're going to handle this by creating a new status called unresolved i mean we already have that status but we currently don't really have a way uh my apologies escalated status that's the one i should be talking about so when the operator responds we're going to change this from unresolved to escalated and then this means okay uh either the human has requested either the user has requested a human or the operator chimed in themselves as a human so that's what we're going to focus the next chapters on tool calling and basically wrapping this up as much as possible this operator dashboard and maybe this enhance prompt as well which is super simple you will type something click enhance and it will give you a better response and we also have the contact panel on the right here that we have to develop perfect but I think this was the goal of this chapter. Let's see. We added the Yotai provider, conversation ID view, messages functions, and conversation functions. And now let's merge this. So 18 dashboard chat. Let's go ahead and stage all of our changes here. 18 dashboard chat. Let's commit them. I'm going to open a new branch. 18 dashboard chat. And I will quickly publish the branch. And now let's go ahead and let's review our code just in case we have any serious security issues. We are actually quite on a good run in the last few pull requests. It was almost perfect. Let's see if luck follows us in this PR as well. And here we have the summary. We introduced the conversations dashboard page displaying a list of conversations. We added a detailed conversation view page allowing users to view and participate in conversation threads, send messages and see role-based message styling. We implemented form validation and input handling for sending messages within a conversation. We also added back and support for fetching a single conversation, retrieving messages with pagination and creating new messages. Exactly what we did. In here, of course, we have the sequence diagram explaining what we do, but this is exactly the same thing as our widget screen chat. We are already familiar with this entire thing. as for the comments it suggests doing exactly what we have planned to do adding the toast library here to display the proper error because we can't do that in the dashboard in here i have a typo it's unauthorized not unparalleled in here i am checking if i have a missing conversation and i'm throwing contact session not found I'm pretty sure I'm doing the if check okay but I'm throwing the incorrect message so I think that I should actually keep doing the conversation here and just fix my convex message because in the private conversations there is no reason for us to check for the contact session I think let me just quickly check messages, actually conversations.ds in the private here let's see line 49 ah I see so we check the conversation here and then instead of using contact session I'm using conversation here so I never really check if this exists or not absolutely correct thank you code rabbit we're going to fix that in the next chapter and in here yeah the same thing we mentioned you can see how it knows from the types that agent name is optional so we should either drop it or decide what to do with it. Yeah, good, good comments here. And one security issue fixed by CodeRabbit. Well, not exactly a security issue, but proper inner join behavior fixed. So merge this, head inside of your main branch and click synchronize changes, click OK. And as always, double check that your graph looks properly. Let's click here. There we go. I believe that marks the end of this chapter. Amazing job and see you in the next one. In this chapter, we're going to enable AI tool calling, enabling our AI to escalate the conversation to a human operator or to resolve it themselves. Before we do that, we're going to first enable manual status change from the operator side. So let's go ahead and go inside of our packages, backend, convex, private, conversations. And in here, in the previous chapter, in our get one query, we do an invalid if check here. So once I obtain the contact session, I need to check if the contact session doesn't exist and throw the error. There is a chance that you did this correctly. This is what CodeRabbit told me in the previous chapter to fix. So that's what I'm fixing now, basically in this get one query. And now let's go ahead and let's develop update status mutation. So make sure you have imported mutation from generated server. And then in here let go ahead and let define all the arguments that are available But before we do that let just define the handler so we get rid of the errors so it easier to look at And then let go ahead and add conversation ID which is a type of VID conversations and then status, which is a union of these three values. They need to match exactly what you have in your schema for the conversations table. So just make sure that this is exactly the same. You can even copy it from here and then paste it here. Now let's go ahead and let's get the context and the arguments in the handler. And let's go ahead and first do the identity and organization check by simply copying it from the get one query. And let's go ahead and paste it here. It should be identical. Unauthorized if identity wasn't found and unauthorized if the organization wasn't found. Now let's go ahead and as always, let's check if the conversation that we're trying to modify exists in the first place. In case conversation does not exist, let's go ahead and throw a convex error. I think we can borrow that from below as well. And while we are here, you already guessed it, let's check if the conversation organization ID matches. We can copy this as well. And let's throw an error if it doesn't match. There we go. And now let's go ahead and simply do await context.database.batch arguments conversation ID. Let's pass in the status arguments.status. That's it. That's all we need for manual update status. Now let's go ahead and let's implement a component that will do that. So let's go ahead and first implement a little popover component so that when we actually hover in here, we're going to create that button for the operator to click that. But since it's not too intuitive, because it's actually going to display the current state. So if it's if it is unresolved, the button will display unresolved. So it won't exactly be 100% clear what will happen if I click on that. So we're going to implement a little pop over or a hint component that will basically be this like this, when you hover over, it will tell you what will happen. So that's what I want to do first. Let's go ahead inside of our packages, let's go inside of UI source components and let's implement hint.tsx. Let's go ahead and let's import everything from tooltip. You already have tooltip when we added all Shatzi and components. and let's mark this as use client. Let's create an interface hint props and inside of here let's create the props. So these are going to be the children which is a required React node, text which is a required string, side which is optional and it can be top, right, bottom or left, align which is optional and it can be start, center or end. these values actually match the tooltip pretty well so if you go ahead inside of here not sure exactly where okay they are internal I think from radix itself so they're not actually exportable maybe there are through radix but let's just write them manually now let's export const hint component and let's assign hint props here and now we can destruct all the props and we can define the side and the align to be top and center by default. And now inside of here we're just going to follow the tooltip composition. So tooltip provider, tooltip inside, tooltip trigger which is going to have as child property and children rendered inside. And inside the tooltip but below the tooltip trigger, we're going to add tooltip content. And instead of a paragraph, we're going to render the text. As simple as that. Now let's go ahead and let's go inside of our apps web. And let's go inside of modules, dashboard, UI components. And inside of here, I want to go ahead and I want to develop the status button. so I will do conversation status button dot t s x and let's go ahead and do export const conversation status button let's go ahead and quickly define the props so that's going to be status which is a type of document whoops not from here document from workspace back and generated data model, select conversations, and then select the status prop. And the other one is going to be on click, which will just be an empty void. And now let's go ahead and do the following. If status is equal to resolved. Whoops, I have to destructure all of this, don't I? So status and on click. There we go. And now this should be not crossed out. let's go ahead and let's return a hint which we just developed so we can import it import hint from workspace ui components hint and let's go ahead and give it text mark as unresolved and inside add a button from workspace ui components button render the check icon from lucid react and write resolved go ahead and give this button an on click of on click size of small and variant of tertiary now we don't have tertiary so let's go ahead and quickly develop it so you can command click inside of this button or control click if you are on windows or you can manually go inside of packages ua source components and find the button in here after transparent let's add tertiary bg gradient to bottom from and now i'm just going to use a very specific shade of green that i like this one two and now i'm just using a darker shade of green here like this text will be white hover will go to my apologies hover will change the to value to be this again, but with a 90% opacity. And again, let's add hover text white. And now let's go ahead and let's copy this. And this one will be warning. this one will be from yellow 500 to let's change to a specific shade of orange be 8b 0 0 and same thing here just a 90 opacity like there and actually we don't need hover white i think I think we can remove it from both places there we go so they are identical but in here I found this okay I haven't found the closest color inside of tailwind there probably is some I just like to be specific with my colors for some weird reason feel free to change this to normal tailwind colors I just like this all right now we have tertiary and warning let's go inside of conversation status button here and we should no longer have any errors here and now let's go ahead and copy the if here. This one will be escalated and return mark as resolved. Go ahead and add arrow up icon and this will be escalated and change this to warning. And then finally we have a default return and that's basically going to be the unresolved status so hint will be mark as escalated variant will be destructive and we're going to use arrow right icon and unresolved all icons are from lucid react so what's going on here if we are unresolved and you click on this button, we're going to mark this as unresolved. If we are escalated, we're going to mark it as resolved. If we are unresolved, we're going to mark it as escalated. This won't make sense until you actually see this in action, but it's actually quite a logical toggle loop. So let's actually use it in action here. And I actually think that we can call this from here. I keep thinking that I need to pass the onclick here. Hmm. You know what? Okay, let's make this component serve for one purpose and one purpose only, and that's to display things. And let's keep the logic, this onclick button inside of conversation ID view. Let's do it like that. So let's now focus on the conversation ID view. and below on submit let's go ahead and let's add the update status function so i'm going to do update status to be use mutation api private conversations update status and let's do update conversation status and then I'm going to do const handle toggle status. Let's go ahead and mark this as an asynchronous method and in here the first thing I'm going to check if we don't have the conversation and let's early return. Otherwise let's go ahead and let's define the or escalated, but not defined. And then let's go ahead and check. If conversation status is unresolved, new status will be escalated. Else, if conversation status is escalated, new status is going to be resolved. else new status will be unresolved so what's the flow here we are going to cycle through states from unresolved user will be able to click and this will then become escalated from escalated it can be resolved and from resolved in can't go back\ninside of unresolved. So if the button says unresolved, you click on it and it will be escalated. When you click on escalated, it will be resolved. And if you want to, you can then reset the flow. And now let's open try here. So await update conversation status, conversation ID, and status new status. And let's go inside of catch error. And let's just console error. like this. There we go. So now we have that. Let's go inside of the header. And after this button, let's add status, conversation status button. And let's go ahead and pass it on click here to be handle toggle status and status conversation status. I can't decide between status and status. So I'm just using both of them. My apologies. and let's go ahead and how about we do if conversation exists then do this maybe that's better yeah it's okay let's do it like that all right and let's turn this into a boolean okay so now if you do turbo dev and head to your web so localhost 3000 in my case here in this corner you should have a button that will allow you to cycle through different states so you can see that this one is unresolved and i can mark it as escalated this one is resolved and I can mark it as unresolved. This one is escalated and I can mark it as resolved. So let's go through the cycle. There we go. Perfect. You can see how this goes through the cycles. And once it is resolved, you will see that I cannot talk back because this conversation has been resolved. We should also disable the enhance button if the conversation is resolved. And one more thing I want to do. I want to add disabled prop here, disabled optional boolean. And let's simply add to each of these buttons, disabled, disabled, like that. And then let me go ahead and add const is updating status, set is updating status, use state, false. let's import use state from react make sure you've added it I'm going to go ahead and set this to be true here and then in the finally set set is updating status will be false and let's go ahead and pass here disabled if set is updating status so we can't just spam it is updating status so now when you click here it should be disabled until the action happens you can see how cool convex is imagine that we just had to like revalidate this uh here but also we would have to revalidate it inside of the widget because remember the widget is in question here too so that's how much time convex actually saves that right you don't even think about it So, okay, I have to create a new account here. So Antonio, antonioatexample.com. Let's continue. Let's start a new chat. Hey there. Let's send a message. And then in here, hey there, you can see our bot has responded. And I'm going to mark this as resolved. And you can see it immediately updated for me, but it immediately updated for the user as well. You can see I can't answer. This conversation has been resolved. And if the user go inside of their inbox, you can see they have the checked sign as well. So that's the power of Convex. Two completely separate apps, but you can see that in this other app, it's completely real-time. That's the power of a real-time database and a proper sync engine. That's why I wanted us to use this so much because it truly is worth it. Now, let's go ahead and let's enable the tools to do this instead of us, right? because this is great. But what we want to see is the tool doing this. But let's just disable this enhance button if something is resolved because this shouldn't be enabled. So instead of the conversation ID view, let's find the AI input button here. Let's disable it. If conversation question mark status is resolved. There we go. Great. now let's go ahead and let's implement the tools to allow ai to do this on our behalf so in order to do this we first have to create an internal method to resolve or escalate the conversation so let's do that let's head back inside of packages back end convex and let's go inside of system. In here, we already have conversations. So we have a simple get by thread ID. And now let's do export const resolve internal mutation. Arguments are going to be thread ID, which can be a type of string. Handler is an asynchronous method. Let's go ahead and grab the context and arguments from here. And let's do const conversation await database, my apologies, is context.database query conversations with index by thread id so we use the most optimized way to find this let's match the thread id to arguments thread id and only let's grab the only one the unique one in case we couldn't find this conversation let's throw new convex error here let's just import it from convex values code not found and message conversation not found and now let's simply do await context database patch conversation underscore id status resolved. As simple as that. And now let's go ahead and do the exact same thing, but for escalation. So let's copy, paste, let's rename this to escalate. And let's change this to escalate, escalated. Now that we have the internal functions set up, let's go inside of our backend again. But this time, let's go inside of the AI folder. And let's create a new folder called tools. And inside of here, let's go ahead and let's add resolve conversation tool. So resolve conversation dot ds. Let's go ahead and let's import create tool from convex dev agent. Let's import z from zod. Let's import internal from generated API. Let's import support agent from agents support agent. It looks like Zod is missing here. So let me quickly check what is our version of Zod. It is this very specific version because we have that error. So now I'm just going to do pnpmf backend add zod at now let me just fix this this specific version so let me just add this to my back end here there we go now this should be working just fine great and let's export const resolve conversation create tool description here will be resolve a conversation arguments will be z.object and let's just pass in an empty string and then handler will have the context in here so we can grab some well context let's first check in case we don't have the thread id there is nothing we can do here except returning back missing thread id and now let's simply do await context and let's call run mutation because this create tool is the equivalent of action so we cannot mutate directly instead we have to refer to our recently created internal methods and let's call resolve and pass in thread id arguments thread id it's context thread id and let's now await support agent save message context thread id context thread id message open an object role assistant and content of the message will be conversation resolved conversation resolved. So what is the difference between our return and our, well, this method? So return is simply so the tool returns something, right? We could technically return the ID of the updated conversation We could fetch the entire conversation and return that right It depends How do you want to use this In my case I don want the user to see the actual tool calling I just want it to feel like magic right I just want this to happen and then the AI send the message, okay, it's resolved. That's it. Depending on what kind of app you were building, perhaps it would be more useful for you to load the entire conversation and then show that to the user, right? It depends. You can and you absolutely should be looking to convex dev documentation here inside of their agents here you have the tools let me just click on it there it goes and in here you can see exactly how all of them work and you can also see that you can actually make use of these arguments if you want to search for something in the database uh spoiler alert we will be doing that but you can see them being used here more in depth and also one super cool thing they have is the playground which is an amazing way to debug the agent because they are annoyingly hard to debug if you have no idea what's happening with the tool if your tool stops working and you have no idea what's happening i'll show you how to set a playground so you can debug it easier all right and now we have our resolve conversation tool Let's go ahead and do the same thing, but for Escalate Conversation. So I'm going to copy this tool, paste it, and I'm going to rename it to Escalate Conversation. Let's go ahead and call it Escalate Conversation. Escalate a conversation and same thing here, except we're going to call Escalate. And inside of here, I'm going to say conversation escalated to a human operator. And let's return the same thing here. Great. Now we have those two tools. And how do we add tools now? Well, let's go ahead and look at the documentation here. So you can provide tools at different times. You can provide them in the agent constructor themselves. You can do it when you call create thread, continue thread. You can call it, we can add them when you add generate text or outside of the thread, right? So all of these options are supported. Specifying tools at each layer will overwrite defaults. That's what's important for you to know. But I think in the context of our app, it doesn't really matter. So let's try and let's add them to our support agent here. So let's add tools here and I will add resolve conversation from tools. And let's add, what's the other one? Escalate conversation from tools. And let me just see if I added this correctly or not. Oh, I think this has to be an object. Yes, that's correct. And now let's go ahead and add some more instructions here. Use resolve. Whoops. Okay, let's open back this here. Use resolve conversation tool when user expresses finalization of the conversation. I don't know. Use escalate conversation tool when user expresses frustration or requests a human explicitly. And let's save this. make sure that you're tracking your backend here and that everything is working fine you can see I had some errors but they got resolved and now let's try this so refresh your widget here let's go ahead and start a new chat so let's track it in real time here I am in the operator here I am in the user and I will say I am very frustrated I want a human let's see what will happen. I seem to be getting some errors here. So let's see. Cannot read properties of undefined here. So something seems to be going wrong. All right. Let's see. We have something convex helpers. Hmm. Not sure what it is, but something it's definitely tried to call a tool, but then it failed. So I'm going to go ahead and debug. All right, so I managed to resolve it and the way I resolved it is just by changing where I add my tools. So it looks like I told you incorrectly. It's not exactly whatever when it comes to where you want to add these tools. So let's remove them from the support agent and I was actually a little bit worried about this because this seems like a circular reference. So let's actually remove both of these imports and just leave the support agent alone but leave the prompt as it is. And then head inside of messages. Which ones? Inside of packages back and convex public messages. So these are the ones that the user creates and find create action. And in here when you call support agent generate text after prompt add tools and now add the escalate conversation and resolve conversation both imported from their respective tools let's go ahead and wait for this to finish to ensure that everything is working just fine let's see there we go let's refresh again you can see that i tried some of these here so here i am again my most recent chat here Let me just, okay, I have to refresh this just a second so I can select it. There we go. Okay, so this is now selected. You can see that this is the first message. It is currently in unresolved status. Let's go ahead now and send the message. I am very frustrated. I want to talk to a human and let's send it and let's see what will happen. Conversation operated to a human operator. escalated to a human operator and you can see it was automatically escalated the new status here is now escalated exactly what we want to see and you can this is how this app is intended to be used so your operators should only uh look at this escalated filter right because the ai bots are responding to everything else you should just be looking here uh at the escalated and then uh you know you would wait for someone to send a message you don't get any notification until a user says i want to talk to a human and then it gets escalated it appears here and then we go ahead and write hey there human here you asked for me that's how this is intended to be used right and now let's check if the resolve method works so let's go ahead and now bring this back into all so we are now tracking this one and let's say sorry you can close this accident conversation resolved you can see the user can no longer write all of this happened thanks to AI without us having to do anything. I think this is super cool. And you can see how unbelievably easy it was for us to do this. It's so intuitive. All thanks to this convex agent component, right? Amazing, amazing, amazing job. That's exactly what we wanted to do. now what I want to do is I want to go and stay inside of this messages here so specifically inside of this backend convex public messages create so what happens now well we shouldn't always trigger AI we should trigger AI if the subscription is active that's the first thing But another thing is only if the status is unresolved, because escalated means the user is talking to a human. So it would be quite weird if in the middle of the conversation, suddenly AI responds back. Those are three people talking for no reason. So let's create a constant, should trigger agent. Conversation.status is equal to unresolved. Later, we're going to add or subscription, blah, blah, blah, but we don't yet have it. And then let's do if we should trigger the agent, then call this entire function. Otherwise, we're going to do something else. So let's indent this. Let's do await, support agent, and let's simply do save message, context, thread ID, and prompt. And I think I have to both do from here. Like this. And let's see, is this exactly the same thing that we are actually doing? Basically, we have to do the same thing that we do here in the public conversation. So yes, you can call save message directly from convex dev agent, or you can do support agent save message. So let's be consistent in our case. let's import save message will this actually work i think the second here needs to be components dot agent yes and you can see it's the same thing and you need to import components from generated api so let try our app now so i going to open a new one here and I will do hi how are you Let's do that first. So right now, we're talking with the AI, and let's go ahead and say, what is your name? And you can see AI keeps responding. But if I say, I want to talk to a human, you can see immediately conversation escalated to a human operator. And what's important now is that if I ask again, what is your name? The AI no longer responds. That's what we wanted. Only the message is saved. Hello, AI is no longer responding. This is now only for humans. Hey there, human responding. There we go. And no AI in between. Unless the operator marks this as resolved, so now no one can answer. and then we bring it back to unresolved. And then you send the message again, hello there. And now AI will respond again until this is escalated again. That's how our flow works. It will be quite rare that you go from escalated back to AI, but it is the cycle that we allow to happen. Excellent. Amazing, amazing job. So now let's focus on the operator dashboard. let's import the pagination here, the infinite scroll, and let's go ahead and implement this enhance tool here. Let's go ahead and let's go back inside of conversation ID view. And inside of here, let's go ahead and add the imports for infinite scroll. So use infinite scroll from workspace UI hooks, use infinite scroll and infinite scroll trigger from workspace UI components infinite scroll trigger. Let's go ahead and first define the use infinite scroll. I'm going to do that right after our use thread messages. Let's go ahead and do this. Now let's go ahead in here and let's add all the properties. So status will be messages.status, messages.loadmore, and load size will be 10. from here let's get top element ref handle load more can load more and is loading more all of those elements and now what we have to do is we have to add the infinite scroll trigger we can do that right here instead of our ai conversation content remember these are chat messages so we go in the opposite direction to load older messages we go up so infinite scroll trigger is a self-closing tag and inside of here let's simply add all of those props that we just destructured from our hook. Can load more, is loading more, handle load more and top element ref. And now when you go to the top you will see no more items. As always you can test this out quite easily by limiting the initial number of items and the load size to a small number like two and disable the observer. Refresh and let's try it out. You now have to manually load two by two messages until you get to the end. So it works. Now let's enable the observer and bring the initial number of items back to 10. Now let's create a simple tool that will help the operators enhance their prompts. So in order to do that, I want to go inside of packages, backend, convex, and let's go inside of private, messages.ts. And let's go ahead and let's import generate text from AI package. And now let's go ahead just after this, I mean, above this create function, and let's export enhance response. And let's call action from generated server. And in here, we're going to accept a few arguments. Prompt, which is a string. And thread ID, which is a string as well. And let me just check. Actually, yeah, I was thinking, okay, I'm doing the normal validation here. But actually, I don't need to. Right? because we passed the prompt manually. So actually all I need to do here is just confirm that the user is logged in. That's the only thing I need here. And eventually I'm going to check if this user's organization has premium so they don't waste my OpenAI tokens, right? So let's just go ahead and copy this part for now. That's the only thing we care about. We can also add organization because that's how we're going to check if they have subscription in the future. So let's go ahead and just confirm the user is logged in and they have organization. And then what we're going to do is just const response, await, generate text, which we just imported. Model can be OpenAI from AISDK OpenAI or whatever you ended up using. Maybe that was Google, whatever you have, right? and then just go ahead and select a model. Preferably, you would use a cheaper model for this because this is like super small thing and then pass in messages here. And let's go ahead and first define role, system, content and inside of here, let's go ahead and write enhance the operator message to be more professional clear and helpful while maintaining their intent and key information like this. And then I will just add role user content prompt. So depending on, you know, arguments.prompt, depending on how good you want this function to be, you could technically extend it by adding the context of all the previous messages that happened, right? For that, you would need to load the conversation. But I'm just trying to keep this as a simple, useful tool. So let's just return response.text here. That's it. That's our enhance response method. Now we can go back instead of conversation ID view here. And let's go ahead and let's just add that. So I'm going to do that here. Const enhance response, use action from convex react, make sure you've added it here. API dot private messages enhance response. And let's do const handle enhance response. and okay so now we just have to let's do this inside of try catch so console error error and inside of here let's do const response await enhance response and pass in prompt to be our current value let me think of what is the best way of adding that here let's do this const current value form get values message and then in here passing the current value so that's what we currently have typed will that work await enhance response this needs to be async like this great and then once we get the response let's do form set value message response. As simple as that. And then let's also add const is enhancing set is enhancing use state false by default. Make sure you have use state. I think we already have it here. Go ahead and immediately turn it on here. Set is enhancing set to true. And then open finally here set is enhancing set to false. Great. We now have handle enhance response and then we can use it down here inside of our AI input button. On click, handle enhance response. So this will be disabled if it's resolved or if is enhancing. And then we can change this too. If is enhancing, let's say enhancing or enhance. And let's also disable it if form, form state is dirty. My apologies, is valid. So basically, we can't enhance an empty string. So this needs to be valid. So let's add exclamation point. So if it's not valid, we disable this button let's try it out you can see by default it's disabled same same as the submit button and i'm gonna say uh what do you mean and let's click enhance there we go that's exactly what i want in this tool to do amazing and let's also while we are enhancing let's also disable the input so it can't be typed in so we're just going to do there we go we already added it to do here we can now remove it is enhancing and I think we have to do this somewhere up here maybe Maybe not Is enhancing Okay so here Is enhancing. Perfect. And there we go. Now let me just. Also, you can add dev indicators false in your next config if you want to hide this. In this project, it's currently not helping us too much, but it is interfering. So inside of my web app, I will go inside of my next config here. And let me just go inside of next config and add a dev indicators false. I am going to track that my web is being rebuilt here. Let me refresh. And let's see if this will now disappear. and now that we have this solved let's create a nice loading element for this we have a nice skeleton here right so why not add a nice skeleton here as well so inside of the conversation id view let me go all the way to the bottom here and let's do export const conversation id view loading and let's go ahead and let's return a div with a class name and we're now basically recreating the header so flex height full flex column and background color muted then let's create the actual header so header class name flex item center justify between border bottom bg background padding 2.5 and we're just adding this button with more horizontal icons so the exact same thing that we are doing here right in the actual conversation id view i'm just recreating that header component now outside of that header let's add ai conversation here let's go ahead and give it a maximum class name of maximum height calculate 100 ph minus 180 pixels when you hover over it, it should look like this. If you have IntelliSense CSS Tailwind extension. And then in here, let's add AI conversation content. And then in here, let's go ahead and create imaginary messages. So array from length 8 dot, okay, not like this, length 8 underscore and then index and then in here let's check every second message will be from the user so let's use the modulus operator here then let's define some random widths so we randomize the skeleton width 48, width 60, width 72 in an array and let's now choose the random width from the number of items here so the width will be widths and then choose the by index and modulus of the width length array and then in here let's go ahead and let's return a div let's go ahead and add class name here and let's use cn from workspace lib utils group flex full width items and justify and gap to PUI to open square brackets and target the div maximum width 80% and then if is user let's go ahead and do is user class, otherwise is assistant and flex row reverse. So the reason we are adding these is because they are targeted by these components. They are not actually doing any CSS. They are just targeted within AI components. Let's give it key of index and inside let's render the skeleton component. Make sure you have imported the skeleton. Let's go ahead and let's give this a class name and let's do it like this. Height 9, pass in the width around the large BG neutral 200. Below that another skeleton and this one will have a class name size 8 rounded full BG neutral 200 like this and then outside of AI conversation open up a div and let's recreate the input. So padding to AI input inside of the AI input, AI input text area, which is a self-closing tag. Make sure it's disabled. Placeholder will be type your response as an operator. AI input toolbar. AI input tools. Let's just close it. AI input submit, disabled, status ready, and self-closing tag. That's it. And now let's go ahead and let's use this. So I'm going to go inside of the conversation ID view here. And before we return, let's do the following if conversation is undefined that means it's still loading or if messages.status is is loading loading first page return and let's use our conversation id view loading like this and now when you refresh you should have a nicer you saw it for a for a second it's It looks much nicer now. And this field is disabled. I like it more this way, right? So now it matches our skeleton here. Great. Amazing, amazing job. We added pagination here. We added a loading skeleton. We added tools. We added this tool. We added the statuses here. Amazing. I believe that's all we aimed for this chapter. We implemented manual status change, tool-based status change, enhance prompt function, infinite scroll, and loading to the dashboard chat. And just to clarify something, all of these prompts that we have inside of our AI, whoops, inside of our AI in the backend, so like support agent, all of these tools, all of these prompts, I will share you my prompts that I use ChatGPT to generate, which are much larger than this. but just for this example we're using the simplest prompts so it so it doesn't accidentally break these are super simple to understand for the ai and they seem to be working but i do have a constant prompt and i will share that with you in the assets folder but for now let's merge this so i'm going to add all of my changes here and i'm going to use 19 ai tool calling AI tool calling. I'm going to open a new branch 19 AI tool calling and I'm going to publish the branch and then I'm going to go ahead and review my pull request as usual just in case we had some security issue or some serious bug here. and here we have the code rabbit summary so let's walk through the uh walkthrough this time this update introduces conversation status management messages enhancement and ui improvements to the dashboard new back-end mutations and tools enable escalation and resolution of conversations as well as ai-powered message enhancement the front-end adds infinite scrolling, status toggling, tooltips, and improved loading states with new button styles and a reusable hint component. Down here we have a sequence diagram pretty much explaining the toggle cycle that we went through. Basically when user clicks on one state it becomes another and then full circle. And in here we have some bugs. Well in here it's telling us to start adding the toast messages we're going to start doing that from the next chapter i promise and in here it recommends throwing errors i'm not 100 sure of that because this is the create tool function and i think that the correct way is to return a string like this which automatically means that what is suggesting here to wrap that inside of try catch and then use that error that's been thrown doesn't really work. So I'm going to leave it as it is simply because that's how I developed it initially. So I don't want to do something that can break your app. And in here we have a bug, not a big one, but yes, I completely forgot to pass side and align to my tooltip content. I never passed it. So we will do that in the next chapter. For now, we can merge this pull request. Amazing, amazing job. Let's go ahead and go back inside of our main branch and let's click on the synchronize changes button and after our changes have synchronized as always let's double check inside of graph perfect so we did 19 and then we merged it amazing I believe that marks the end of this chapter and see you in the next one Thank you.",
  "transcript_chars": 538789,
  "transcript_filled_at": "2026-06-06T16:06:01.059449+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}